diff --git a/packages/core/src/plugin/identity.ts b/packages/core/src/plugin/identity.ts new file mode 100644 index 000000000000..0881db870475 --- /dev/null +++ b/packages/core/src/plugin/identity.ts @@ -0,0 +1,40 @@ +export * as IdentityPlugin from "./identity.js" + +import { SystemPart } from "@opencode/ai" +import { define } from "@opencode/plugin/effect/plugin" +import type { SessionHooks } from "@opencode/plugin/effect/session" +import { Model } from "@opencode/schema/model" +import { Effect } from "effect" + +export function identity(model: { readonly provider: string; readonly name: string; readonly ref: Model.Ref }) { + return [ + "# Your Model", + `- Provider: ${model.provider}`, + `- Name: ${model.name}`, + `- ID: ${model.ref.providerID}/${model.ref.id}`, + ].join("\n") +} + +export const Plugin = define({ + id: "opencode.prompt.identity", + effect: Effect.fn("IdentityPlugin")(function* (ctx) { + const hook = (event: SessionHooks["context"]) => + Effect.gen(function* () { + const model = + (yield* ctx.model.list()).data.find( + (model) => model.providerID === event.model.providerID && model.id === event.model.id, + ) ?? Model.Info.default(event.model.providerID, event.model.id) + const provider = (yield* ctx.provider.list()).data.find((provider) => provider.id === event.model.providerID) + event.system.splice( + 1, + 0, + SystemPart.make( + identity({ provider: provider?.name ?? event.model.providerID, name: model.name, ref: event.model }), + ), + ) + }).pipe(Effect.catch(() => Effect.void)) + yield* ctx.session.hook("context", hook) + yield* ctx.session.hook("compaction", hook) + yield* ctx.session.hook("generate", hook) + }), +}) diff --git a/packages/core/src/plugin/internal.ts b/packages/core/src/plugin/internal.ts index 072148322dab..e450b82b77c5 100644 --- a/packages/core/src/plugin/internal.ts +++ b/packages/core/src/plugin/internal.ts @@ -84,6 +84,7 @@ import { WriteTool } from "../tool/plugin/write.js" import { AgentPlugin } from "./agent.js" import BrowserPlugin from "@opencode/plugin-browser" import { CommandPlugin } from "./command.js" +import { IdentityPlugin } from "./identity.js" import { PlanPlugin } from "./plan.js" import { ModelsDevPlugin } from "./models-dev.js" import { McpCodeModeExclusionPlugin } from "./mcp-codemode-exclusion.js" @@ -216,6 +217,7 @@ const pre = [ PatchTool.Plugin, // Render model prompts after the patch plugin selects the available editing tools. ...OptimizePlugin.Plugins, + IdentityPlugin.Plugin, EditTool.Plugin, GlobTool.Plugin, GrepTool.Plugin, diff --git a/packages/core/src/plugin/system-prompt/gpt-astra.txt b/packages/core/src/plugin/system-prompt/gpt-astra.txt index 0f63bc4056ff..cb48b61a984a 100644 --- a/packages/core/src/plugin/system-prompt/gpt-astra.txt +++ b/packages/core/src/plugin/system-prompt/gpt-astra.txt @@ -1,4 +1,4 @@ -You are an AI agent powered by OpenCode, a coding agent harness. Help the user accomplish their goals using the tools you have available. +You are an AI agent running in OpenCode, a coding agent harness. Help the user accomplish their goals using the tools you have available. # Harness - Responses are rendered as GitHub-flavored Markdown. diff --git a/packages/core/src/plugin/system-prompt/gpt.txt b/packages/core/src/plugin/system-prompt/gpt.txt index 84da1646b84c..4a30126f768e 100644 --- a/packages/core/src/plugin/system-prompt/gpt.txt +++ b/packages/core/src/plugin/system-prompt/gpt.txt @@ -1,4 +1,4 @@ -You are an AI agent powered by OpenCode, a coding agent harness. Help the user accomplish their goals using the tools you have available. +You are an AI agent running in OpenCode, a coding agent harness. Help the user accomplish their goals using the tools you have available. # Harness - Responses are rendered as GitHub-flavored Markdown. diff --git a/packages/core/src/session/runner/prompt/system.txt b/packages/core/src/session/runner/prompt/system.txt index f28057a4f91a..0e02ced78d7a 100644 --- a/packages/core/src/session/runner/prompt/system.txt +++ b/packages/core/src/session/runner/prompt/system.txt @@ -1,4 +1,4 @@ -You are an AI agent powered by OpenCode, a coding agent harness. Help the user accomplish their goals using the tools you have available. +You are an AI agent running in OpenCode, a coding agent harness. Help the user accomplish their goals using the tools you have available. # Harness - Responses are rendered as GitHub-flavored Markdown. diff --git a/packages/core/test/fixtures/recordings/session-runner/openai-chat-streams-text.json b/packages/core/test/fixtures/recordings/session-runner/openai-chat-streams-text.json index 24f75c60bd46..53f8805931d3 100644 --- a/packages/core/test/fixtures/recordings/session-runner/openai-chat-streams-text.json +++ b/packages/core/test/fixtures/recordings/session-runner/openai-chat-streams-text.json @@ -13,7 +13,7 @@ "headers": { "content-type": "application/json" }, - "body": "{\"model\":\"gpt-4o-mini\",\"messages\":[{\"role\":\"system\",\"content\":\"You are an AI agent powered by OpenCode, a coding agent harness. Help the user accomplish their goals using the tools you have available.\\n\\n# Harness\\n- Responses are rendered as GitHub-flavored Markdown.\\n- `` blocks are harness instructions, not user-authored content. Read and follow them.\\n- Prefer parallelizing independent tool calls.\\n\\n\\n# Communication\\n\\nUse clear file paths when referring to files. Keep responses clear and concise, and avoid unnecessary technical jargon.\\n\\n## Intermediate Commentary\\n\\nAs you work, you send messages to the commentary channel. These are how you collaborate with the user while you work: stating assumptions and providing updates. Keep them concise and quickly scannable, and send them only when they add real information, such as a discovery, a tradeoff, or a blocker. Do not narrate routine reads, searches, or edits.\\n\\nBy default, treat new messages received during ongoing work as steering the active task rather than replacing it. Incorporate corrections and constraints, and answer questions briefly in commentary before continuing. Replace the task only when the user clearly cancels it or requests an incompatible objective.\\n\\nDo not put a final response, such as a blocking or clarifying question, in the commentary channel. The final answer must always be fully self-contained.\\n\\n## Final Answer\\n\\nIn the final answer, lead with the outcome, not the steps you took to reach it. Cover the most important information, use only as much structure as the answer needs, and avoid long-winded explanations unless necessary. Include technical detail only where it helps.\\n\\n# Working in codebases\\n- Keep changes consistent with the structure, naming, style, and patterns of the surrounding code.\\n- Treat unfamiliar files or changes as potential user work and investigate before deleting or overwriting them.\\n\\n# Delegation\\n\\nDo not spawn subagents unless the user or applicable AGENTS.md/skill instructions explicitly ask for subagents, delegation, or parallel agent work.\\n\\n# Destructive actions\\n\\nDo not revert, reset, or discard changes you did not make. Never run destructive commands such as `git reset --hard`, `git checkout --`, or recursive deletes on broad paths unless the user clearly asked for that operation; if the target or scope is unclear, ask first. Prefer non-interactive git commands.\\n\\n# Autonomy\\n\\nDo not infer authorization for work beyond the user's request. Assumptions that help you make progress are fine as long as they stay within the user's intent and the scope of the task.\\n\"},{\"role\":\"user\",\"content\":\"Say hello in one short sentence.\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"store\":false,\"prompt_cache_key\":\"ses_runner_recorded\",\"max_completion_tokens\":20,\"temperature\":0}" + "body": "{\"model\":\"gpt-4o-mini\",\"messages\":[{\"role\":\"system\",\"content\":\"You are an AI agent running in OpenCode, a coding agent harness. Help the user accomplish their goals using the tools you have available.\\n\\n# Harness\\n- Responses are rendered as GitHub-flavored Markdown.\\n- `` blocks are harness instructions, not user-authored content. Read and follow them.\\n- Prefer parallelizing independent tool calls.\\n\\n\\n# Communication\\n\\nUse clear file paths when referring to files. Keep responses clear and concise, and avoid unnecessary technical jargon.\\n\\n## Intermediate Commentary\\n\\nAs you work, you send messages to the commentary channel. These are how you collaborate with the user while you work: stating assumptions and providing updates. Keep them concise and quickly scannable, and send them only when they add real information, such as a discovery, a tradeoff, or a blocker. Do not narrate routine reads, searches, or edits.\\n\\nBy default, treat new messages received during ongoing work as steering the active task rather than replacing it. Incorporate corrections and constraints, and answer questions briefly in commentary before continuing. Replace the task only when the user clearly cancels it or requests an incompatible objective.\\n\\nDo not put a final response, such as a blocking or clarifying question, in the commentary channel. The final answer must always be fully self-contained.\\n\\n## Final Answer\\n\\nIn the final answer, lead with the outcome, not the steps you took to reach it. Cover the most important information, use only as much structure as the answer needs, and avoid long-winded explanations unless necessary. Include technical detail only where it helps.\\n\\n# Working in codebases\\n- Keep changes consistent with the structure, naming, style, and patterns of the surrounding code.\\n- Treat unfamiliar files or changes as potential user work and investigate before deleting or overwriting them.\\n\\n# Delegation\\n\\nDo not spawn subagents unless the user or applicable AGENTS.md/skill instructions explicitly ask for subagents, delegation, or parallel agent work.\\n\\n# Destructive actions\\n\\nDo not revert, reset, or discard changes you did not make. Never run destructive commands such as `git reset --hard`, `git checkout --`, or recursive deletes on broad paths unless the user clearly asked for that operation; if the target or scope is unclear, ask first. Prefer non-interactive git commands.\\n\\n# Autonomy\\n\\nDo not infer authorization for work beyond the user's request. Assumptions that help you make progress are fine as long as they stay within the user's intent and the scope of the task.\\n\\n# Your Model\\n- Provider: openai\\n- Name: gpt-4o-mini\\n- ID: openai/gpt-4o-mini\"},{\"role\":\"user\",\"content\":\"Say hello in one short sentence.\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"store\":false,\"prompt_cache_key\":\"ses_runner_recorded\",\"max_completion_tokens\":20,\"temperature\":0}" }, "response": { "status": 200, diff --git a/packages/core/test/plugin/host.ts b/packages/core/test/plugin/host.ts index 776185947300..fd5b3b293a02 100644 --- a/packages/core/test/plugin/host.ts +++ b/packages/core/test/plugin/host.ts @@ -252,6 +252,14 @@ export function providerHost(providers: Provider.Interface): Plugin.Context["pro } } +/** Empty catalog for hosts that only need provider lookups to fall back. */ +export const noProviders: Plugin.Context["provider"] = { + list: () => Effect.succeed(located([])), + get: () => Effect.die("unused provider.get"), + transform: () => Effect.die("unused provider.transform"), + reload: () => Effect.die("unused provider.reload"), +} + export function modelHost(models: Model.Interface): Plugin.Context["model"] { return { list: () => models.available().pipe(Effect.map(located)), diff --git a/packages/core/test/plugin/identity.test.ts b/packages/core/test/plugin/identity.test.ts new file mode 100644 index 000000000000..406cf1e837b2 --- /dev/null +++ b/packages/core/test/plugin/identity.test.ts @@ -0,0 +1,73 @@ +import { expect, test } from "bun:test" +import { SystemPart } from "@opencode/ai" +import { Agent } from "@opencode/core/agent" +import { IdentityPlugin } from "@opencode/core/plugin/identity" +import { Plugin } from "@opencode/core/plugin" +import { PluginHooks } from "@opencode/core/plugin/hooks" +import { PluginHost } from "@opencode/core/plugin/host" +import { Session } from "@opencode/core/session" +import type { SessionHooks } from "@opencode/plugin/effect/session" +import { Model } from "@opencode/schema/model" +import { Provider } from "@opencode/core/provider" +import { Effect } from "effect" +import { testEffect } from "../lib/effect" +import { PluginTestLayer } from "./fixture" + +const it = testEffect(PluginTestLayer) + +test("formats the model identity part", () => { + expect( + IdentityPlugin.identity({ + provider: "OpenAI", + name: "GPT-4o mini", + ref: Model.Ref.make({ providerID: Provider.ID.make("openai"), id: Model.ID.make("gpt-4o-mini") }), + }), + ).toBe(["# Your Model", "- Provider: OpenAI", "- Name: GPT-4o mini", "- ID: openai/gpt-4o-mini"].join("\n")) +}) + +const identity = (provider: string, name: string, id: string) => + ["# Your Model", `- Provider: ${provider}`, `- Name: ${name}`, `- ID: test/${id}`].join("\n") + +const context = (id: string): SessionHooks["context"] => ({ + sessionID: Session.ID.make("ses_model_identity"), + agent: Agent.ID.make("build"), + model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make(id) }), + system: [SystemPart.make("Agent prompt"), SystemPart.make("Initial context")], + messages: [], + tools: {}, + options: {}, +}) + +it.effect("inserts the structured model block after the agent prompt", () => + Effect.gen(function* () { + const catalog = yield* Provider.Service + const hooks = yield* PluginHooks.Service + const plugins = yield* Plugin.Service + const pluginHost = yield* PluginHost.make(plugins) + yield* catalog.transform((editor) => { + editor.update(Provider.ID.make("test"), (provider) => { + provider.name = "Test Provider" + }) + editor.models.update(Provider.ID.make("test"), Model.ID.make("meta/muse-spark-1.1"), (model) => { + model.name = "Muse Spark" + }) + }) + yield* IdentityPlugin.Plugin.effect(pluginHost) + + const named = context("meta/muse-spark-1.1") + yield* hooks.trigger("session", "context", named) + expect(named.system.map((part) => part.text)).toEqual([ + "Agent prompt", + identity("Test Provider", "Muse Spark", "meta/muse-spark-1.1"), + "Initial context", + ]) + + const fallback = context("unknown-model") + yield* hooks.trigger("session", "context", fallback) + expect(fallback.system.map((part) => part.text)).toEqual([ + "Agent prompt", + identity("Test Provider", "unknown-model", "unknown-model"), + "Initial context", + ]) + }), +) diff --git a/packages/core/test/session-runner-recorded.test.ts b/packages/core/test/session-runner-recorded.test.ts index 9a0273be0943..3ca8cfa9ce4f 100644 --- a/packages/core/test/session-runner-recorded.test.ts +++ b/packages/core/test/session-runner-recorded.test.ts @@ -36,6 +36,7 @@ import { PluginSupervisor } from "@opencode/core/plugin/supervisor" import { Plugin } from "@opencode/core/plugin" import { PluginHooks } from "@opencode/core/plugin/hooks" import { OptimizePlugin } from "@opencode/core/plugin/optimize" +import { IdentityPlugin } from "@opencode/core/plugin/identity" import { describe, expect } from "bun:test" import { eq } from "drizzle-orm" import { Effect, Layer } from "effect" @@ -44,7 +45,7 @@ import { testEffect } from "./lib/effect" import { LocationServiceMap } from "@opencode/core/location-service-map" import { promptLocationNode } from "./fixture/prompt-location" import { permissionLayer } from "./lib/permission" -import { agentHost, modelHost, host } from "./plugin/host" +import { agentHost, modelHost, host, noProviders } from "./plugin/host" const cassetteName = "session-runner/openai-chat-streams-text" const cassetteDirectory = path.resolve(import.meta.dir, "fixtures/recordings") @@ -184,9 +185,11 @@ describe("SessionRunnerLLM recorded", () => { const pluginHost = host({ agent: agentHost(agents), model: modelHost(models), + provider: noProviders, session: { hook: (name, callback) => hooks.register("session", name, callback) }, }) yield* Effect.forEach(OptimizePlugin.Plugins, (plugin) => plugin.effect(pluginHost), { discard: true }) + yield* IdentityPlugin.Plugin.effect(pluginHost) const { db } = yield* Database.Service yield* db .insert(ProjectTable) diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index 80a6bd652fef..196cb37286e6 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -55,6 +55,7 @@ import { PluginSupervisor } from "@opencode/core/plugin/supervisor" import { Plugin } from "@opencode/core/plugin" import { PluginHooks } from "@opencode/core/plugin/hooks" import { OptimizePlugin } from "@opencode/core/plugin/optimize" +import { IdentityPlugin } from "@opencode/core/plugin/identity" import { QuestionTool } from "@opencode/core/tool/plugin/question" import { Agent } from "@opencode/core/agent" import { Config } from "@opencode/core/config" @@ -88,7 +89,7 @@ import { promptLocationNode } from "./fixture/prompt-location" import { LocationServiceMap } from "@opencode/core/location-service-map" import { Expected } from "./lib/session-message" import { permissionLayer } from "./lib/permission" -import { agentHost, modelHost, host } from "./plugin/host" +import { agentHost, modelHost, host, noProviders } from "./plugin/host" import { CodeModeInstructions } from "@opencode/core/codemode/instructions" const emptyCodeMode = `\n\n${CodeModeInstructions.render({ total: 0, shown: 0, namespaces: [] })}` @@ -119,6 +120,11 @@ const testModel = (id: string, limit: ModelLimit = defaultModelLimit) => { } const model = testModel("fake-model") const defaultSystem = SessionSystemPrompt.make([]) +const identity = (providerID: string, id: string) => + ["# Your Model", `- Provider: ${providerID}`, `- Name: ${id}`, `- ID: ${providerID}/${id}`].join("\n") +const fakeIdentity = identity("fake", "fake-model") +const replacementIdentity = identity("fake", "replacement") +const gptIdentity = identity("openai", "gpt-5") const replacementModel = testModel("replacement") const compactModel = testModel("compact", { context: 4_000, output: 50 }) const fullOutputModel = testModel("full-output", { context: 262_144, output: 262_144 }) @@ -515,11 +521,13 @@ const setup = Effect.gen(function* () { const pluginHost = host({ agent: agentHost(agents), model: modelHost(models), + provider: noProviders, session: { hook: (name, callback) => hooks.register("session", name, callback) }, }) yield* Effect.forEach(OptimizePlugin.Plugins, (plugin) => plugin.effect(pluginHost), { discard: true, }) + yield* IdentityPlugin.Plugin.effect(pluginHost) yield* agents.transform((editor) => editor.update(Agent.ID.make("build"), (agent) => { agent.mode = "primary" @@ -1554,7 +1562,7 @@ describe("SessionRunnerLLM", () => { yield* s.session.prompt({ sessionID: forked.id, text: "Forked", resume: false }) yield* s.session.resume(forked.id) - expect(s.requests.at(-1)?.system.map((part) => part.text)).toEqual([defaultSystem, "Latest context"]) + expect(s.requests.at(-1)?.system.map((part) => part.text)).toEqual([defaultSystem, fakeIdentity, "Latest context"]) // Copied history keeps the frozen chronological update; no new update is emitted. expect(systemTexts(s.requests.at(-1)!)).toContain("Changed context") expect(systemTexts(s.requests.at(-1)!)).not.toContain("Latest context") @@ -1617,7 +1625,7 @@ describe("SessionRunnerLLM", () => { yield* s.resume expect(s.requests).toHaveLength(1) - expect(s.requests[0]?.system.map((part) => part.text)).toEqual([defaultSystem, "Initial context"]) + expect(s.requests[0]?.system.map((part) => part.text)).toEqual([defaultSystem, fakeIdentity, "Initial context"]) expect(messageRoles(s.requests[0])).toEqual(["user", "user"]) // The projected row is authoritative: a missing row admits a fresh baseline // instead of rebuilding from durable events. @@ -1653,8 +1661,8 @@ describe("SessionRunnerLLM", () => { expect(s.requests[0].messages).toHaveLength(1) expect(s.requests[1].messages.slice(0, 1)).toEqual([...s.requests[0].messages]) expect(s.requests.map((request) => request.system.map((part) => part.text))).toEqual([ - [defaultSystem, "Initial context"], - [defaultSystem, "Initial context"], + [defaultSystem, fakeIdentity, "Initial context"], + [defaultSystem, fakeIdentity, "Initial context"], ]) expect(messageRoles(s.requests[1])).toEqual(["user", "system", "user"]) expect(s.requests[1]?.messages.at(1)?.content).toEqual([Expected.text("Changed context")]) @@ -1693,6 +1701,7 @@ describe("SessionRunnerLLM", () => { expect(s.requests.at(-1)?.system.map((part) => part.text)).toEqual([ expect.stringContaining("# Delegation"), + gptIdentity, "Initial context", ]) }) @@ -1713,6 +1722,7 @@ describe("SessionRunnerLLM", () => { expect(s.requests.at(-1)?.system.map((part) => part.text)).toEqual([ expect.stringContaining("# Delegation"), + gptIdentity, "Initial context", ]) }) @@ -1730,7 +1740,7 @@ describe("SessionRunnerLLM", () => { yield* s.llm.push(TestLLM.text("Done", "text-build")) yield* s.resume - expect(s.requests.at(-1)?.system.map((part) => part.text)).toEqual(["Build agent instructions", "Initial context"]) + expect(s.requests.at(-1)?.system.map((part) => part.text)).toEqual(["Build agent instructions", fakeIdentity, "Initial context"]) }) scenario("uses the configured default agent system for omitted-agent sessions", function* (s) { @@ -1751,7 +1761,7 @@ describe("SessionRunnerLLM", () => { yield* s.llm.push(TestLLM.text("Done", "text-reviewer")) yield* s.resume - expect(s.requests.at(-1)?.system.map((part) => part.text)).toEqual(["Reviewer instructions", "Initial context"]) + expect(s.requests.at(-1)?.system.map((part) => part.text)).toEqual(["Reviewer instructions", fakeIdentity, "Initial context"]) expect((yield* s.messages)[0]).toMatchObject({ type: "assistant", agent: "reviewer" }) }) @@ -1774,7 +1784,7 @@ describe("SessionRunnerLLM", () => { yield* s.llm.push(TestLLM.text("Done", "text-selected")) yield* s.resume - expect(s.requests.at(-1)?.system.map((part) => part.text)).toEqual(["Reviewer instructions", "Initial context"]) + expect(s.requests.at(-1)?.system.map((part) => part.text)).toEqual(["Reviewer instructions", fakeIdentity, "Initial context"]) expect((yield* s.messages)[0]).toMatchObject({ type: "assistant", agent: "reviewer" }) }) @@ -1816,8 +1826,8 @@ describe("SessionRunnerLLM", () => { yield* s.runPrompt("Second") expect(s.requests.map((request) => request.system.map((part) => part.text))).toEqual([ - [defaultSystem, "Initial context\n\nBuild skills"], - [defaultSystem, "Initial context\n\nBuild skills"], + [defaultSystem, fakeIdentity, "Initial context\n\nBuild skills"], + [defaultSystem, fakeIdentity, "Initial context\n\nBuild skills"], ]) expect(systemTexts(s.requests[1])).toContainEqual(expect.stringContaining("Reviewer skills")) }) @@ -1839,7 +1849,7 @@ describe("SessionRunnerLLM", () => { yield* s.runPrompt("First") expect(s.requests.map((request) => request.system.map((part) => part.text))).toEqual([ - [defaultSystem, "Initial context\n\nBuild skills"], + [defaultSystem, fakeIdentity, "Initial context\n\nBuild skills"], ]) }) @@ -1858,7 +1868,7 @@ describe("SessionRunnerLLM", () => { yield* s.runPrompt("First") expect(s.requests.map((request) => request.model)).toEqual([model]) expect(s.requests.map((request) => request.system.map((part) => part.text))).toEqual([ - [defaultSystem, "Initial context"], + [defaultSystem, fakeIdentity, "Initial context"], ]) }) @@ -1882,6 +1892,7 @@ describe("SessionRunnerLLM", () => { // String values render verbatim inside the initial tagged block. expect(s.requests[0]?.system.map((part) => part.text)).toEqual([ defaultSystem, + fakeIdentity, ["Initial context", "", '', "production", ""].join("\n"), ]) @@ -1969,9 +1980,9 @@ describe("SessionRunnerLLM", () => { yield* s.runPrompt("Third") expect(s.requests.map((request) => request.system.map((part) => part.text))).toEqual([ - [defaultSystem, "Initial context"], - [defaultSystem, "Initial context"], - [defaultSystem, "Initial context"], + [defaultSystem, fakeIdentity, "Initial context"], + [defaultSystem, fakeIdentity, "Initial context"], + [defaultSystem, replacementIdentity, "Initial context"], ]) expect(messageRoles(s.requests[1])).toEqual(["user", "system", "user"]) expect(s.requests[2]?.messages.filter((message) => message.role === "system")).toHaveLength(2) @@ -2033,9 +2044,9 @@ describe("SessionRunnerLLM", () => { yield* s.runPrompt("Third") expect(s.requests.map((request) => request.system.map((part) => part.text))).toEqual([ - [defaultSystem, "Initial context"], - [defaultSystem, "Initial context"], - [defaultSystem, "Initial context"], + [defaultSystem, fakeIdentity, "Initial context"], + [defaultSystem, replacementIdentity, "Initial context"], + [defaultSystem, replacementIdentity, "Initial context"], ]) }) @@ -2051,9 +2062,9 @@ describe("SessionRunnerLLM", () => { yield* s.runPrompt("Second") expect(s.requests.map((request) => request.system.map((part) => part.text))).toEqual([ - [defaultSystem, "Initial context"], - [defaultSystem, "Initial context"], - [defaultSystem, "Initial context"], + [defaultSystem, fakeIdentity, "Initial context"], + [defaultSystem, fakeIdentity, "Initial context"], + [defaultSystem, fakeIdentity, "Initial context"], ]) expect(messageRoles(s.requests[2])).toEqual(["user", "system", "user"]) expect(s.requests[2]?.messages.at(1)?.content).toEqual([Expected.text("Replacement context")]) @@ -3015,7 +3026,7 @@ describe("SessionRunnerLLM", () => { expect(resolutions).toBe(2) expect(s.requests).toHaveLength(3) expect(s.requests[2]?.model).toBe(replacementModel) - expect(s.requests[2]?.system.map((part) => part.text)).toEqual([defaultSystem, "Initial context"]) + expect(s.requests[2]?.system.map((part) => part.text)).toEqual([defaultSystem, replacementIdentity, "Initial context"]) expect(systemTexts(s.requests[2])).toContain("Changed during compaction") expect(userTexts(s.requests[2])[0]).toContain("\n## Objective\n- Overflow summary\n") expect(userTexts(s.requests[2]).join("\n")).not.toContain("Queued during compaction") @@ -3206,7 +3217,7 @@ describe("SessionRunnerLLM", () => { yield* s.runPrompt("Third") // Compaction already moved current values into the new epoch before the unavailable read. - expect(s.requests.at(-1)?.system.map((part) => part.text)).toEqual([defaultSystem, "Changed context"]) + expect(s.requests.at(-1)?.system.map((part) => part.text)).toEqual([defaultSystem, fakeIdentity, "Changed context"]) expect(systemTexts(s.requests.at(-1)!)).not.toContain("Changed context") }) @@ -3343,8 +3354,8 @@ describe("SessionRunnerLLM", () => { expect(s.requests.map((request) => request.model)).toEqual([model, replacementModel]) expect(s.requests.map((request) => request.system.map((part) => part.text))).toEqual([ - [defaultSystem, "Initial context"], - [defaultSystem, "Initial context"], + [defaultSystem, fakeIdentity, "Initial context"], + [defaultSystem, replacementIdentity, "Initial context"], ]) expect(systemTexts(s.requests[1])).toContain("Replacement context") })