Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 0 additions & 17 deletions libs/deepagents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 11 additions & 39 deletions libs/deepagents/src/agent.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 },
});
});

Expand Down
167 changes: 30 additions & 137 deletions libs/deepagents/src/agent.test.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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", () => {
Expand Down Expand Up @@ -64,7 +60,7 @@ describe("isAnthropicModel", () => {
});
});

describe("Structured system prompt configuration", () => {
describe("Legacy system prompt assembly", () => {
function getLastSystemMessage(
invokeSpy: ReturnType<typeof vi.spyOn>,
): SystemMessage {
Expand All @@ -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<string> {
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();
}
});
});
Expand Down
Loading
Loading