From d40e64bf21c89b3a064185d0c12084c86451e984 Mon Sep 17 00:00:00 2001 From: VamsiKrishna0101 Date: Mon, 22 Jun 2026 15:31:43 +0530 Subject: [PATCH] Merge plugin system prompts for provider compatibility --- apps/server/package.json | 2 +- .../openwork-capabilities-knowledge.test.ts | 17 +++++++++ .../openwork-capabilities-knowledge.ts | 7 ++++ .../openwork-extensions-preview.test.ts | 18 +++++++++ .../openwork-extensions-preview.ts | 7 ++++ .../openwork-system-prompt-normalizer.test.ts | 37 +++++++++++++++++++ .../openwork-system-prompt-normalizer.ts | 20 ++++++++++ .../src/openwork-extensions-plugin-path.ts | 1 + apps/server/src/openwork-runtime-config.ts | 2 + 9 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 apps/server/src/opencode-plugins/openwork-extensions-preview.test.ts create mode 100644 apps/server/src/opencode-plugins/openwork-system-prompt-normalizer.test.ts create mode 100644 apps/server/src/opencode-plugins/openwork-system-prompt-normalizer.ts diff --git a/apps/server/package.json b/apps/server/package.json index 6d698fe56..a2113bf4b 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -10,7 +10,7 @@ "dev": "OPENWORK_DEV_MODE=1 bun src/cli.ts", "test": "bun test", "test:artifacts": "bun test src/artifact-files.e2e.test.ts src/workspace-init.test.ts src/server.normalizeWorkspaceRelativePath.test.ts", - "build": "tsc -p tsconfig.json && bun build src/opencode-plugins/openwork-extensions-preview.ts src/opencode-plugins/openwork-capabilities-knowledge.ts src/opencode-plugins/openwork-anthropic-adaptive-thinking.ts src/opencode-plugins/openwork-anthropic-tool-schema.ts --outdir dist/opencode-plugins --target node --format esm", + "build": "tsc -p tsconfig.json && bun build src/opencode-plugins/openwork-extensions-preview.ts src/opencode-plugins/openwork-capabilities-knowledge.ts src/opencode-plugins/openwork-anthropic-adaptive-thinking.ts src/opencode-plugins/openwork-anthropic-tool-schema.ts src/opencode-plugins/openwork-system-prompt-normalizer.ts --outdir dist/opencode-plugins --target node --format esm", "build:bin": "bun build --compile src/cli.ts --outfile dist/bin/openwork-server", "build:bin:all": "bun ./script/build.ts --outdir dist/bin --target bun-darwin-arm64 --target bun-darwin-x64 --target bun-linux-x64 --target bun-linux-arm64 --target bun-windows-x64", "start": "bun dist/cli.js", diff --git a/apps/server/src/opencode-plugins/openwork-capabilities-knowledge.test.ts b/apps/server/src/opencode-plugins/openwork-capabilities-knowledge.test.ts index dc905a6b9..583e818d0 100644 --- a/apps/server/src/opencode-plugins/openwork-capabilities-knowledge.test.ts +++ b/apps/server/src/opencode-plugins/openwork-capabilities-knowledge.test.ts @@ -3,6 +3,23 @@ import { resolve } from "node:path"; import { OpenWorkCapabilitiesKnowledge } from "./openwork-capabilities-knowledge.js"; describe("OpenWork capabilities knowledge plugin", () => { + test("adds capabilities knowledge to the system prompt", async () => { + const plugin = await OpenWorkCapabilitiesKnowledge(); + const output = { + system: [ + "You are OpenWork.", + "", + ], + }; + + await plugin["experimental.chat.system.transform"]({}, output); + + expect(output.system).toHaveLength(3); + expect(output.system[0]).toBe("You are OpenWork."); + expect(output.system[2]).toContain("You are running inside OpenWork"); + expect(output.system[2]).toContain("OpenWork product questions"); + }); + test("retrieves Slack connection guidance from bundled docs", async () => { process.env.OPENWORK_DOCS_DIR = resolve(import.meta.dir, "../../../../packages/docs"); diff --git a/apps/server/src/opencode-plugins/openwork-capabilities-knowledge.ts b/apps/server/src/opencode-plugins/openwork-capabilities-knowledge.ts index de06d7092..7bf81da82 100644 --- a/apps/server/src/opencode-plugins/openwork-capabilities-knowledge.ts +++ b/apps/server/src/opencode-plugins/openwork-capabilities-knowledge.ts @@ -214,6 +214,13 @@ function excerpt(content: string, query: string): string { export const OpenWorkCapabilitiesKnowledge = async () => ({ "experimental.chat.system.transform": async (_input: unknown, output: { system: string[] }) => { output.system.push(OPENWORK_CAPABILITIES_KNOWLEDGE); + + // Strict OpenAI-compatible proxies reject multiple system messages. + // Keep the knowledge, but send it as one provider-safe system prompt. + // Mutate the array in-place to ensure the framework sees the change + const merged = output.system.filter((entry) => entry.trim().length > 0).join("\n\n"); + output.system.length = 0; + if (merged) output.system.push(merged); }, tool: { openwork_docs_search: { diff --git a/apps/server/src/opencode-plugins/openwork-extensions-preview.test.ts b/apps/server/src/opencode-plugins/openwork-extensions-preview.test.ts new file mode 100644 index 000000000..11fba64fc --- /dev/null +++ b/apps/server/src/opencode-plugins/openwork-extensions-preview.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, test } from "bun:test"; +import { OpenWorkExtensionsPreview } from "./openwork-extensions-preview.js"; + +describe("OpenWork extensions preview plugin", () => { + test("adds extension guidance to the system prompt", async () => { + const plugin = await OpenWorkExtensionsPreview(); + const output = { + system: ["You are a title generator.", ""], + }; + + await plugin["experimental.chat.system.transform"]({}, output); + + expect(output.system).toHaveLength(4); + expect(output.system[0]).toBe("You are a title generator."); + expect(output.system[2]).toContain("check OpenWork extensions"); + expect(output.system[3]).toContain("openwork_ui_execute_action"); + }); +}); diff --git a/apps/server/src/opencode-plugins/openwork-extensions-preview.ts b/apps/server/src/opencode-plugins/openwork-extensions-preview.ts index e5b04ec19..a9ee5d239 100644 --- a/apps/server/src/opencode-plugins/openwork-extensions-preview.ts +++ b/apps/server/src/opencode-plugins/openwork-extensions-preview.ts @@ -204,6 +204,13 @@ export const OpenWorkExtensionsPreview = async () => ({ "experimental.chat.system.transform": async (_input: unknown, output: { system: string[] }) => { output.system.push(OPENWORK_EXTENSION_DISCOVERY_INSTRUCTION); output.system.push(OPENWORK_UI_CONTROL_INSTRUCTION); + + // Strict OpenAI-compatible proxies reject multiple system messages. + // Keep the guidance, but send it as one provider-safe system prompt. + // Mutate the array in-place to ensure the framework sees the change + const merged = output.system.filter((entry) => entry.trim().length > 0).join("\n\n"); + output.system.length = 0; + if (merged) output.system.push(merged); }, tool: { openwork_extension_list_actions: { diff --git a/apps/server/src/opencode-plugins/openwork-system-prompt-normalizer.test.ts b/apps/server/src/opencode-plugins/openwork-system-prompt-normalizer.test.ts new file mode 100644 index 000000000..fc6707670 --- /dev/null +++ b/apps/server/src/opencode-plugins/openwork-system-prompt-normalizer.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, test } from "bun:test"; +import { OpenWorkCapabilitiesKnowledge } from "./openwork-capabilities-knowledge.js"; +import { OpenWorkExtensionsPreview } from "./openwork-extensions-preview.js"; +import { mergeSystemPromptsInPlace, OpenWorkSystemPromptNormalizer } from "./openwork-system-prompt-normalizer.js"; + +describe("OpenWork system prompt normalizer", () => { + test("merges multiple system prompts in place", () => { + const system = [" base prompt ", "", " plugin prompt "]; + const original = system; + + mergeSystemPromptsInPlace(system); + + expect(system).toBe(original); + expect(system).toEqual(["base prompt\n\nplugin prompt"]); + }); + + test("runs after OpenWork plugin transforms to produce one provider-safe system prompt", async () => { + const capabilities = await OpenWorkCapabilitiesKnowledge(); + const extensions = await OpenWorkExtensionsPreview(); + const normalizer = await OpenWorkSystemPromptNormalizer(); + const output = { + system: ["You are OpenWork.", ""], + }; + + await capabilities["experimental.chat.system.transform"]({}, output); + await extensions["experimental.chat.system.transform"]({}, output); + expect(output.system.length).toBeGreaterThan(1); + + await normalizer["experimental.chat.system.transform"]({}, output); + + expect(output.system).toHaveLength(1); + expect(output.system[0]).toContain("You are OpenWork."); + expect(output.system[0]).toContain("You are running inside OpenWork"); + expect(output.system[0]).toContain("check OpenWork extensions"); + expect(output.system[0]).toContain("openwork_ui_execute_action"); + }); +}); diff --git a/apps/server/src/opencode-plugins/openwork-system-prompt-normalizer.ts b/apps/server/src/opencode-plugins/openwork-system-prompt-normalizer.ts new file mode 100644 index 000000000..cb2ac64a8 --- /dev/null +++ b/apps/server/src/opencode-plugins/openwork-system-prompt-normalizer.ts @@ -0,0 +1,20 @@ +type SystemTransformOutput = { + system: string[]; +}; + +export function mergeSystemPromptsInPlace(system: string[]): void { + const merged = system + .map((entry) => entry.trim()) + .filter(Boolean) + .join("\n\n"); + + // Keep the same array reference for hook callers that hold onto it. + system.length = 0; + if (merged) system.push(merged); +} + +export const OpenWorkSystemPromptNormalizer = async () => ({ + "experimental.chat.system.transform": async (_input: unknown, output: SystemTransformOutput) => { + mergeSystemPromptsInPlace(output.system); + }, +}); diff --git a/apps/server/src/openwork-extensions-plugin-path.ts b/apps/server/src/openwork-extensions-plugin-path.ts index 3ccdd808a..5e5a857d2 100644 --- a/apps/server/src/openwork-extensions-plugin-path.ts +++ b/apps/server/src/openwork-extensions-plugin-path.ts @@ -27,5 +27,6 @@ export function openworkPluginPath(name: string, here = dirname(fileURLToPath(im export const openworkExtensionsPreviewPluginPath = () => openworkPluginPath("openwork-extensions-preview"); export const openworkCapabilitiesKnowledgePluginPath = () => openworkPluginPath("openwork-capabilities-knowledge"); +export const openworkSystemPromptNormalizerPluginPath = () => openworkPluginPath("openwork-system-prompt-normalizer"); export const openworkAnthropicAdaptiveThinkingPluginPath = () => openworkPluginPath("openwork-anthropic-adaptive-thinking"); export const openworkAnthropicToolSchemaPluginPath = () => openworkPluginPath("openwork-anthropic-tool-schema"); diff --git a/apps/server/src/openwork-runtime-config.ts b/apps/server/src/openwork-runtime-config.ts index 882ce5731..d59648bf9 100644 --- a/apps/server/src/openwork-runtime-config.ts +++ b/apps/server/src/openwork-runtime-config.ts @@ -18,6 +18,7 @@ import { randomUUID } from "node:crypto"; import { openworkExtensionsPreviewPluginPath, openworkCapabilitiesKnowledgePluginPath, + openworkSystemPromptNormalizerPluginPath, openworkAnthropicAdaptiveThinkingPluginPath, openworkAnthropicToolSchemaPluginPath, } from "./openwork-extensions-plugin-path.js"; @@ -89,6 +90,7 @@ export async function buildOpenworkRuntimeConfigObject( openworkAnthropicAdaptiveThinkingPluginPath(), openworkAnthropicToolSchemaPluginPath(), ...runtimePluginList(runtimeConfig), + openworkSystemPromptNormalizerPluginPath(), ], ...(disabledProviders.length ? { disabled_providers: disabledProviders } : {}), mcp: runtimeMcpMap(runtimeConfig),