diff --git a/libs/deepagents/README.md b/libs/deepagents/README.md index 003a33fd1..e20b81920 100644 --- a/libs/deepagents/README.md +++ b/libs/deepagents/README.md @@ -107,23 +107,6 @@ const agent = createDeepAgent({ }); ``` -A string is placed before the built-in Deep Agent prompt. For full control over -prompt assembly, provide a structured configuration: - -```typescript -const agent = createDeepAgent({ - systemPrompt: { - prefix: "You are the support assistant for Acme.", - base: null, // Remove the built-in Deep Agent prompt. - suffix: "Follow Acme's escalation policy.", - }, -}); -``` - -Structured prompts are assembled as `prefix` → `base` → `suffix`, followed by -any model-specific harness profile suffix. Omit `base` to retain the active -base prompt, or set it to `null` to remove the base entirely. - See the [JavaScript Deep Agents docs](https://docs.langchain.com/oss/javascript/deepagents/overview) for full configuration options. ## LangGraph Native diff --git a/libs/deepagents/src/agent.test-d.ts b/libs/deepagents/src/agent.test-d.ts index 4a78aa5f1..5e6a0108f 100644 --- a/libs/deepagents/src/agent.test-d.ts +++ b/libs/deepagents/src/agent.test-d.ts @@ -21,7 +21,6 @@ import { import { StateSchema } from "@langchain/langgraph"; import { z } from "zod/v4"; import { createDeepAgent } from "./agent.js"; -import type { SystemPromptConfig } from "./index.js"; import type { MergedDeepAgentState, InferSubagentByName, @@ -78,50 +77,23 @@ const MemoryMiddleware = createMiddleware({ }); describe("createDeepAgent types", () => { - it("should allow legacy and structured system prompts", () => { + it("should allow systemPrompt to be a string or SystemMessage", () => { createDeepAgent({ systemPrompt: "Hello, world!", }); - const message = new SystemMessage({ - content: [ - { - type: "text", - text: "Hello, world!", - }, - ], - }); - createDeepAgent({ systemPrompt: message }); - createDeepAgent({ systemPrompt: {} }); - createDeepAgent({ - systemPrompt: { - prefix: message, - base: null, - suffix: "Follow the policy.", - }, - }); - - const config: SystemPromptConfig = { - prefix: null, - base: message, - suffix: null, - }; - createDeepAgent({ systemPrompt: config }); - createDeepAgent({ - // @ts-expect-error systemPrompt does not accept numbers - systemPrompt: 42, - }); - createDeepAgent({ - systemPrompt: { - // @ts-expect-error prompt parts do not accept numbers - prefix: 42, - }, + systemPrompt: new SystemMessage({ + content: [ + { + type: "text", + text: "Hello, world!", + }, + ], + }), }); createDeepAgent({ - systemPrompt: { - // @ts-expect-error unknown structured prompt field - unknownField: "value", - }, + // @ts-expect-error systemPrompt does not accept structured configurations + systemPrompt: { base: null }, }); }); diff --git a/libs/deepagents/src/agent.test.ts b/libs/deepagents/src/agent.test.ts index 3475404c2..67b29697f 100644 --- a/libs/deepagents/src/agent.test.ts +++ b/libs/deepagents/src/agent.test.ts @@ -1,6 +1,5 @@ import { describe, it, expect, vi } from "vitest"; import { createDeepAgent } from "./agent.js"; -import type { SystemPromptConfig } from "./types.js"; import { isAnthropicModel } from "./utils.js"; import { FakeListChatModel } from "@langchain/core/utils/testing"; import { @@ -12,10 +11,7 @@ import { MemorySaver, StateSchema } from "@langchain/langgraph"; import { createFileData } from "./backends/utils.js"; import { ConfigurationError } from "./errors.js"; import { assertAllDeepAgentQualities } from "./testing/utils.js"; -import { - _resetRegistryForTesting, - registerHarnessProfile, -} from "./profiles/harness/index.js"; +import { registerHarnessProfile } from "./profiles/harness/index.js"; import { z } from "zod/v4"; describe("isAnthropicModel", () => { @@ -64,7 +60,7 @@ describe("isAnthropicModel", () => { }); }); -describe("Structured system prompt configuration", () => { +describe("Legacy system prompt assembly", () => { function getLastSystemMessage( invokeSpy: ReturnType, ): SystemMessage { @@ -79,167 +75,64 @@ describe("Structured system prompt configuration", () => { return systemMessage; } - it("assembles configured prompt parts in order", async () => { + it("separates a string system prompt from the base prompt", async () => { const invokeSpy = vi.spyOn(FakeListChatModel.prototype, "invoke"); - const cases: Array<{ - systemPrompt: string | SystemPromptConfig; - ordered: string[]; - absent?: string[]; - }> = [ - { - systemPrompt: {}, - ordered: ["You are a Deep Agent"], - }, - { - systemPrompt: { base: "__base__" }, - ordered: ["__base__"], - absent: ["You are a Deep Agent"], - }, - { - systemPrompt: { prefix: "__prefix__" }, - ordered: ["__prefix__", "You are a Deep Agent"], - }, - { - systemPrompt: { suffix: "__suffix__" }, - ordered: ["You are a Deep Agent", "__suffix__"], - }, - { - systemPrompt: { - prefix: "__prefix__", - base: "__base__", - suffix: "__suffix__", - }, - ordered: ["__prefix__", "__base__", "__suffix__"], - absent: ["You are a Deep Agent"], - }, - { - systemPrompt: { base: null, suffix: "__only__" }, - ordered: ["__only__"], - absent: ["You are a Deep Agent"], - }, - { - systemPrompt: "__legacy__", - ordered: ["__legacy__", "You are a Deep Agent"], - }, - ]; try { - for (const testCase of cases) { - const model = new FakeListChatModel({ responses: ["Done"] }); - const agent = createDeepAgent({ - model, - systemPrompt: testCase.systemPrompt, - }); - await agent.invoke({ messages: [new HumanMessage("Hello")] }); - - const text = getLastSystemMessage(invokeSpy).text; - const positions = testCase.ordered.map((fragment) => - text.indexOf(fragment), - ); - expect(positions.every((position) => position >= 0)).toBe(true); - expect(positions).toEqual([...positions].sort((a, b) => a - b)); - for (const fragment of testCase.absent ?? []) { - expect(text).not.toContain(fragment); - } - } + const agent = createDeepAgent({ + model: new FakeListChatModel({ responses: ["Done"] }), + systemPrompt: "__custom_prompt__", + }); + await agent.invoke({ messages: [new HumanMessage("Hello")] }); + + const prompt = getLastSystemMessage(invokeSpy).text; + expect(prompt).toContain("__custom_prompt__\n\nYou are a Deep Agent"); } finally { invokeSpy.mockRestore(); } }); - it("preserves SystemMessage content blocks and cache control", async () => { + it("preserves SystemMessage content blocks before the base prompt", async () => { const invokeSpy = vi.spyOn(FakeListChatModel.prototype, "invoke"); - const cachedPrefix = new SystemMessage({ + const customPrompt = new SystemMessage({ content: [ { type: "text", - text: "__cached_prefix__", + text: "__cached_custom_prompt__", cache_control: { type: "ephemeral" }, }, ], }); try { - const model = new FakeListChatModel({ responses: ["Done"] }); const agent = createDeepAgent({ - model, - systemPrompt: { prefix: cachedPrefix, suffix: "__suffix__" }, + model: new FakeListChatModel({ responses: ["Done"] }), + systemPrompt: customPrompt, }); await agent.invoke({ messages: [new HumanMessage("Hello")] }); const blocks = getLastSystemMessage(invokeSpy).contentBlocks; - const cachedBlock = blocks.find( - (block) => block.type === "text" && block.text === "__cached_prefix__", + const customIndex = blocks.findIndex( + (block) => + block.type === "text" && block.text === "__cached_custom_prompt__", ); - expect(cachedBlock?.cache_control).toEqual({ type: "ephemeral" }); - expect(blocks.filter((block) => block.type === "text")).toEqual( - expect.arrayContaining([ - expect.objectContaining({ text: "\n\n" }), - expect.objectContaining({ text: "__suffix__" }), - ]), + const baseIndex = blocks.findIndex( + (block) => + block.type === "text" && block.text.includes("You are a Deep Agent"), ); - const text = getLastSystemMessage(invokeSpy).text; - expect(text.indexOf("__cached_prefix__")).toBeLessThan( - text.indexOf("You are a Deep Agent"), - ); - expect(text.indexOf("You are a Deep Agent")).toBeLessThan( - text.indexOf("__suffix__"), - ); - } finally { - invokeSpy.mockRestore(); - } - }); - it("gives configured base precedence over the harness profile base", async () => { - _resetRegistryForTesting(); - registerHarnessProfile("openai", { - baseSystemPrompt: "__profile_base__", - systemPromptSuffix: "__profile_suffix__", - }); - const invokeSpy = vi.spyOn(FakeListChatModel.prototype, "invoke"); - - async function invokeWithPrompt( - systemPrompt: SystemPromptConfig, - ): Promise { - const model = new FakeListChatModel({ responses: ["Done"] }); - vi.spyOn(model, "getName").mockReturnValue("ChatOpenAI"); - const agent = createDeepAgent({ model, systemPrompt }); - await agent.invoke({ messages: [new HumanMessage("Hello")] }); - return getLastSystemMessage(invokeSpy).text; - } - - try { - const profileBaseText = await invokeWithPrompt({ suffix: "__suffix__" }); - expect(profileBaseText.indexOf("__profile_base__")).toBeLessThan( - profileBaseText.indexOf("__suffix__"), - ); - expect(profileBaseText.indexOf("__suffix__")).toBeLessThan( - profileBaseText.indexOf("__profile_suffix__"), - ); - - const configuredBaseText = await invokeWithPrompt({ - base: "__configured_base__", - suffix: "__suffix__", + expect(blocks[customIndex]?.cache_control).toEqual({ + type: "ephemeral", }); - expect(configuredBaseText).not.toContain("__profile_base__"); - expect(configuredBaseText.indexOf("__configured_base__")).toBeLessThan( - configuredBaseText.indexOf("__suffix__"), - ); - expect(configuredBaseText.indexOf("__suffix__")).toBeLessThan( - configuredBaseText.indexOf("__profile_suffix__"), - ); - - const noBaseText = await invokeWithPrompt({ - base: null, - suffix: "__suffix__", - }); - expect(noBaseText).not.toContain("__profile_base__"); - expect(noBaseText.indexOf("__suffix__")).toBeLessThan( - noBaseText.indexOf("__profile_suffix__"), + expect(customIndex).toBeLessThan(baseIndex); + expect(blocks[baseIndex]).toEqual( + expect.objectContaining({ + type: "text", + text: expect.stringMatching(/^\n\n/), + }), ); } finally { invokeSpy.mockRestore(); - _resetRegistryForTesting(); } }); }); diff --git a/libs/deepagents/src/agent.ts b/libs/deepagents/src/agent.ts index b4db1906c..a2869d4ea 100644 --- a/libs/deepagents/src/agent.ts +++ b/libs/deepagents/src/agent.ts @@ -47,7 +47,6 @@ import type { FlattenSubAgentMiddleware, InferStructuredResponse, SupportedResponseFormat, - SystemPromptConfig, } from "./types.js"; /** * required for type inference @@ -103,52 +102,6 @@ const BASE_AGENT_PROMPT = context` For longer tasks, provide brief progress updates at reasonable intervals — a concise sentence recapping what you've done and what's next. `; -const PROMPT_SEPARATOR = "\n\n"; - -type SystemPromptPart = string | SystemMessage; -type SystemPromptContentBlock = SystemMessage["contentBlocks"][number]; - -/** Normalize legacy system prompt values to the structured representation. */ -function normalizeSystemPrompt( - systemPrompt: SystemPromptPart | SystemPromptConfig | undefined, -): SystemPromptConfig { - if (systemPrompt === undefined) { - return {}; - } - if ( - typeof systemPrompt === "string" || - SystemMessage.isInstance(systemPrompt) - ) { - return { prefix: systemPrompt }; - } - return systemPrompt; -} - -/** Assemble prompt parts while preserving structured message content blocks. */ -function assemblePromptParts( - parts: readonly SystemPromptPart[], -): string | SystemMessage { - if (parts.length === 0) { - return ""; - } - if (parts.every((part) => typeof part === "string")) { - return parts.join(PROMPT_SEPARATOR); - } - - const contentBlocks: SystemPromptContentBlock[] = []; - for (const [index, part] of parts.entries()) { - if (index > 0) { - contentBlocks.push({ type: "text", text: PROMPT_SEPARATOR }); - } - if (SystemMessage.isInstance(part)) { - contentBlocks.push(...part.contentBlocks); - } else { - contentBlocks.push({ type: "text", text: part }); - } - } - return new SystemMessage({ contentBlocks }); -} - const BUILTIN_TOOL_NAMES: ReadonlySet = new Set([ ...FILESYSTEM_TOOL_NAMES, ...ASYNC_TASK_TOOL_NAMES, @@ -501,31 +454,38 @@ export function createDeepAgent< ); } - // Assemble the main-agent prompt in this order: - // caller prefix -> active base -> caller suffix -> profile suffix. - const promptConfig = normalizeSystemPrompt(systemPrompt); - const promptParts: SystemPromptPart[] = []; - - if (promptConfig.prefix !== undefined && promptConfig.prefix !== null) { - promptParts.push(promptConfig.prefix); - } - - const activeBasePrompt = - promptConfig.base !== undefined - ? promptConfig.base - : (harnessProfile.baseSystemPrompt ?? BASE_AGENT_PROMPT); - if (activeBasePrompt !== null) { - promptParts.push(activeBasePrompt); - } - - if (promptConfig.suffix) { - promptParts.push(promptConfig.suffix); - } - if (harnessProfile.systemPromptSuffix) { - promptParts.push(harnessProfile.systemPromptSuffix); - } + // Combine system prompt parameter with profile-aware base prompt. + const effectiveBasePrompt = applyProfilePrompt( + harnessProfile, + BASE_AGENT_PROMPT, + ); - const finalSystemPrompt = assemblePromptParts(promptParts); + const basePromptBlocks = effectiveBasePrompt + ? ([ + { + type: "text" as const, + text: `\n\n${effectiveBasePrompt}`, + }, + ] as const) + : []; + + const finalSystemPrompt = + typeof systemPrompt === "string" + ? new SystemMessage({ + contentBlocks: [ + { type: "text", text: systemPrompt }, + ...basePromptBlocks, + ], + }) + : SystemMessage.isInstance(systemPrompt) + ? new SystemMessage({ + contentBlocks: [...systemPrompt.contentBlocks, ...basePromptBlocks], + }) + : new SystemMessage({ + contentBlocks: effectiveBasePrompt + ? [{ type: "text", text: effectiveBasePrompt }] + : [], + }); const agent = createAgent({ model, diff --git a/libs/deepagents/src/browser.ts b/libs/deepagents/src/browser.ts index daf4ed698..7db4d9b39 100644 --- a/libs/deepagents/src/browser.ts +++ b/libs/deepagents/src/browser.ts @@ -32,7 +32,6 @@ export type { DeepAgentRunStream, SubagentRunStream } from "./stream.js"; export type { AnySubAgent, CreateDeepAgentParams, - SystemPromptConfig, MergedDeepAgentState, // DeepAgent type bag and helper types DeepAgent, diff --git a/libs/deepagents/src/index.ts b/libs/deepagents/src/index.ts index 01317b16e..d8a5f5296 100644 --- a/libs/deepagents/src/index.ts +++ b/libs/deepagents/src/index.ts @@ -29,7 +29,6 @@ export type { DeepAgentRunStream, SubagentRunStream } from "./stream.js"; export type { AnySubAgent, CreateDeepAgentParams, - SystemPromptConfig, MergedDeepAgentState, // DeepAgent type bag and helper types DeepAgent, diff --git a/libs/deepagents/src/profiles/harness/registry.test.ts b/libs/deepagents/src/profiles/harness/registry.test.ts index 53f0e7902..f26d9ee67 100644 --- a/libs/deepagents/src/profiles/harness/registry.test.ts +++ b/libs/deepagents/src/profiles/harness/registry.test.ts @@ -145,6 +145,14 @@ describe("applyProfilePrompt", () => { "New base.\n\nAnd a suffix.", ); }); + + it("uses a suffix without a leading separator when the base is empty", () => { + const profile = createHarnessProfile({ + baseSystemPrompt: "", + systemPromptSuffix: "Suffix only.", + }); + expect(applyProfilePrompt(profile, "Default prompt.")).toBe("Suffix only."); + }); }); describe("resolveHarnessProfile", () => { diff --git a/libs/deepagents/src/profiles/harness/registry.ts b/libs/deepagents/src/profiles/harness/registry.ts index d5ae0477a..6dbd8fddb 100644 --- a/libs/deepagents/src/profiles/harness/registry.ts +++ b/libs/deepagents/src/profiles/harness/registry.ts @@ -301,7 +301,9 @@ export function applyProfilePrompt( ? profile.baseSystemPrompt : basePrompt; if (profile.systemPromptSuffix !== undefined) { - return `${prompt}\n\n${profile.systemPromptSuffix}`; + return prompt + ? `${prompt}\n\n${profile.systemPromptSuffix}` + : profile.systemPromptSuffix; } return prompt; } diff --git a/libs/deepagents/src/types.ts b/libs/deepagents/src/types.ts index d66942789..a4ec033c4 100644 --- a/libs/deepagents/src/types.ts +++ b/libs/deepagents/src/types.ts @@ -488,28 +488,6 @@ export type InferSubagentReactAgentType< > : never; -/** - * Structured system prompt configuration for {@link createDeepAgent}. - * - * Prompt parts are assembled in the order `prefix` → `base` → `suffix`, - * followed by any model-specific harness profile suffix. - */ -export interface SystemPromptConfig { - /** Content placed before the base prompt. */ - prefix?: string | SystemMessage | null; - - /** - * Replacement for the active base prompt. - * - * Omit this field to retain the harness profile base or built-in base prompt. - * Set it to `null` to omit the base prompt entirely. - */ - base?: string | SystemMessage | null; - - /** Content placed after the base prompt and before any harness profile suffix. */ - suffix?: string | SystemMessage | null; -} - /** * Configuration parameters for creating a Deep Agent * Matches Python's create_deep_agent parameters @@ -541,14 +519,8 @@ export interface CreateDeepAgentParams< model?: BaseLanguageModel | string; /** Tools the agent should have access to */ tools?: TTools | StructuredTool[]; - /** - * Custom system instructions for the agent. - * - * A string or {@link SystemMessage} is placed before the active base prompt. - * For more control, provide a {@link SystemPromptConfig} to replace or remove - * the base prompt and add content after it. - */ - systemPrompt?: string | SystemMessage | SystemPromptConfig; + /** Custom system prompt for the agent. This will be combined with the base agent prompt */ + systemPrompt?: string | SystemMessage; /** * Optional schema for custom agent state. Allows you to define custom state properties * beyond built-in `messages`, `todos`, and `files`. These properties can be accessed