feat(engine): add systemPromptLayers to AgentRuntimeOptions and wire into DefaultResourceLoader

Extends AgentRuntimeOptions and AgentOptions with an optional
systemPromptLayers field (SystemPromptLayers) for cross-session prompt
caching. DefaultResourceLoader now uses the stable layer as
systemPromptOverride and the dynamic layer as appendSystemPromptOverride
when layers are provided, falling back to the flat systemPrompt string
for backward compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Matthew Greenberg
2026-05-08 19:54:55 -04:00
parent a53a9a0802
commit e7ea1257ff
4 changed files with 107 additions and 2 deletions

View File

@@ -0,0 +1,31 @@
import { describe, it, expect } from "vitest";
import type { AgentRuntimeOptions } from "../agent-runtime.js";
import type { SystemPromptLayers } from "../prompt-layers.js";
describe("AgentRuntimeOptions.systemPromptLayers", () => {
it("accepts systemPromptLayers alongside systemPrompt", () => {
const layers: SystemPromptLayers = {
stable: "You are a reviewer.",
dynamic: "Check for bugs.",
};
const options: AgentRuntimeOptions = {
cwd: "/tmp/test",
systemPrompt: "You are a reviewer.\n\nCheck for bugs.",
systemPromptLayers: layers,
};
expect(options.systemPromptLayers).toBeDefined();
expect(options.systemPromptLayers!.stable).toBe("You are a reviewer.");
expect(options.systemPromptLayers!.dynamic).toBe("Check for bugs.");
});
it("works without systemPromptLayers (backward compatible)", () => {
const options: AgentRuntimeOptions = {
cwd: "/tmp/test",
systemPrompt: "You are a reviewer.",
};
expect(options.systemPromptLayers).toBeUndefined();
});
});

View File

@@ -0,0 +1,53 @@
import { describe, it, expect } from "vitest";
describe("createFnAgent prompt layer configuration", () => {
it("uses stable layer as systemPromptOverride when layers provided", () => {
const options = {
systemPrompt: "Stable.\n\nDynamic.",
systemPromptLayers: { stable: "Stable.", dynamic: "Dynamic." },
};
const systemPromptOverride =
options.systemPromptLayers?.stable ?? options.systemPrompt;
const appendSystemPromptOverride = options.systemPromptLayers?.dynamic
? [options.systemPromptLayers.dynamic]
: [];
expect(systemPromptOverride).toBe("Stable.");
expect(appendSystemPromptOverride).toEqual(["Dynamic."]);
});
it("falls back to full systemPrompt when layers not provided", () => {
const options = {
systemPrompt: "Full prompt.",
systemPromptLayers: undefined as
| { stable: string; dynamic: string }
| undefined,
};
const systemPromptOverride =
options.systemPromptLayers?.stable ?? options.systemPrompt;
const appendSystemPromptOverride = options.systemPromptLayers?.dynamic
? [options.systemPromptLayers.dynamic]
: [];
expect(systemPromptOverride).toBe("Full prompt.");
expect(appendSystemPromptOverride).toEqual([]);
});
it("handles empty dynamic layer", () => {
const options = {
systemPrompt: "Stable.",
systemPromptLayers: { stable: "Stable.", dynamic: "" },
};
const systemPromptOverride =
options.systemPromptLayers?.stable ?? options.systemPrompt;
const appendSystemPromptOverride = options.systemPromptLayers?.dynamic
? [options.systemPromptLayers.dynamic]
: [];
expect(systemPromptOverride).toBe("Stable.");
expect(appendSystemPromptOverride).toEqual([]);
});
});

View File

@@ -19,6 +19,7 @@ import type { PermanentAgentGatingContext } from "@fusion/core";
import type { SkillSelectionContext } from "./skill-resolver.js";
import type { FallbackModelUsedPayload } from "./pi.js";
import type { AgentActionGateContext } from "./agent-action-gate.js";
import type { SystemPromptLayers } from "./prompt-layers.js";
/**
* Options for creating an agent session.
@@ -36,6 +37,17 @@ export interface AgentRuntimeOptions {
cwd: string;
/** System prompt for the agent */
systemPrompt: string;
/**
* Optional structured prompt layers for cross-session caching.
* When present, runtimes that support prompt caching use the `stable`
* layer as a cacheable prefix and the `dynamic` layer as the per-session
* suffix. Runtimes that don't support caching ignore this and use
* `systemPrompt` (the collapsed string) instead.
*
* Callers MUST also provide `systemPrompt` as the collapsed equivalent
* for backward compatibility.
*/
systemPromptLayers?: SystemPromptLayers;
/** Tool set to use: "coding" for full tools, "readonly" for read-only access */
tools?: "coding" | "readonly";
/** Additional custom tools to merge with the base toolset */

View File

@@ -56,6 +56,7 @@ import {
type AgentActionGateContext,
} from "./agent-action-gate.js";
import { resolvePermanentAgentToolDecision } from "./permanent-agent-gating.js";
import type { SystemPromptLayers } from "./prompt-layers.js";
export interface AgentResult {
session: AgentSession;
@@ -711,6 +712,11 @@ export type BuiltinWebToolName = "WebSearch" | "WebFetch";
export interface AgentOptions {
cwd: string;
systemPrompt: string;
/** Structured prompt layers for cross-session caching. When provided,
* the stable layer is used as systemPromptOverride and the dynamic
* layer as appendSystemPromptOverride. Falls back to systemPrompt
* when not provided. */
systemPromptLayers?: SystemPromptLayers;
tools?: "coding" | "readonly";
customTools?: ToolDefinition[];
/** Optional allowlist of builtin runtime web tools to keep enabled. */
@@ -1715,8 +1721,11 @@ export async function createFnAgent(options: AgentOptions): Promise<AgentResult>
cwd: resolvedProjectRoot,
agentDir: getFusionAgentDir(),
settingsManager,
systemPromptOverride: () => options.systemPrompt,
appendSystemPromptOverride: () => [],
systemPromptOverride: () => options.systemPromptLayers?.stable ?? options.systemPrompt,
appendSystemPromptOverride: () =>
options.systemPromptLayers?.dynamic
? [options.systemPromptLayers.dynamic]
: [],
...(effectiveExtensionPaths.length > 0 ? { additionalExtensionPaths: [...effectiveExtensionPaths] } : {}),
...(skillsOverrideFn ? { skillsOverride: skillsOverrideFn } : {}),
});