Support SystemMessage type for create_deep_agent(system_prompt=...) (align with langchain.agents.create_agent) #225

Closed
opened 2026-02-16 08:20:23 -05:00 by yindo · 6 comments
Owner

Originally created by @alario-tang on GitHub (Dec 21, 2025).

Originally assigned to: @sydney-runkle on GitHub.

Summary

langchain.agents.create_agent now supports:

system_prompt: str | SystemMessage | None = None

However, deepagents.create_deep_agent still exposes:

system_prompt: str | None = None

This means DeepAgents cannot directly consume a SystemMessage (or a pipeline that already models system prompts as SystemMessage), even though the underlying create_agent does support it.

It would be very helpful if create_deep_agent could accept both str and SystemMessage, and internally adapt to the existing DeepAgents prompt composition logic.


Motivation

  • In many codebases, system prompts are already modeled as structured objects (SystemMessage or custom prompt objects) rather than plain strings.
  • create_agent now allows passing SystemMessage directly, which makes it easy to reuse these higher-level models when building agents.
  • DeepAgents is positioned as a higher-level Agent Harness on top of LangGraph/LangChain. It would be natural if it did not “force” callers to downcast back to str while the lower-level create_agent already supports SystemMessage.

Concretely, in our project:

  • We maintain our own SystemPrompt / SystemMessage abstraction (with sections like role, constraints, style, etc.).
  • For create_agent, we can now pass a SystemMessage directly.
  • For create_deep_agent, we currently need a separate to_legacy_system_prompt_str() adapter solely to satisfy the str type, even though downstream everything ends up as messages anyway.

This makes DeepAgents feel “less typed” than the underlying create_agent, and it complicates sharing a common prompt model between DeepAgents and non-DeepAgents agents.


Current behavior

from deepagents import create_deep_agent
from langchain_core.messages import SystemMessage

sys_msg = SystemMessage(content="You are a deep research assistant...")

# ❌ Type mismatch: create_deep_agent only accepts `str | None`
agent = create_deep_agent(
    model="openai:gpt-4o",
    tools=[...],
    system_prompt=sys_msg,   # not allowed by the type signature
)

We instead have to do:

agent = create_deep_agent(
    model="openai:gpt-4o",
    tools=[...],
    system_prompt=sys_msg.content,  # manual flattening
)

or:

system_prompt = my_system_prompt.to_legacy_system_prompt_str()

agent = create_deep_agent(
    model="openai:gpt-4o",
    tools=[...],
    system_prompt=system_prompt,
)

Desired behavior

Ideally, create_deep_agent would have a signature like:

from langchain_core.messages import SystemMessage

def create_deep_agent(
    model: str | BaseChatModel | None = None,
    tools: Sequence[BaseTool | Callable | dict[str, Any]] | None = None,
    *,
    system_prompt: str | SystemMessage | None = None,
    ...
) -> CompiledStateGraph[...]:
    ...

Semantics:

  • If system_prompt is a str, keep the current behavior (prepend to the base DeepAgents system prompt).
  • If system_prompt is a SystemMessage, internally convert it to a string (or to a single system message block) following the existing DeepAgents composition logic.
  • If None, keep the current default behavior (use the built-in base prompt).

This keeps full backwards compatibility while allowing higher-level code to pass SystemMessage directly, just like create_agent does.


Alternatives considered

  • Keep a separate to_legacy_system_prompt_str() adapter on the user side

    This works today, but:

    • It duplicates logic that create_agent already encapsulates for SystemMessage.
    • It forces DeepAgents users to “downgrade” to str even if their internal representation is already SystemMessage.
  • Bypass create_deep_agent and manually compose middlewares + create_agent

    Technically possible, but it defeats the purpose of DeepAgents as a higher-level harness:

    • We would have to reimplement parts of DeepAgents behavior (planning, filesystem, subagents, self-management) just to support SystemMessage at the top level.
    • This increases complexity and fragmentation across projects.

Backwards compatibility

  • The change is fully backward compatible if implemented as str | SystemMessage | None:

    • Existing code passing str continues to work.
    • New code can opt in to pass SystemMessage.
  • Internally, DeepAgents can still normalize everything to a single system prompt string if that’s what the current infrastructure expects.


Additional context

  • langchain.agents.create_agent already supports SystemMessage for system_prompt, so aligning DeepAgents with this behavior would make the ecosystem more consistent and easier to reason about.
  • Many of the emerging patterns around provider-specific system prompts (e.g. Anthropic multi-block system prompts, cache control, etc.) are modeled in user code as SystemMessage or higher-level prompt objects. Being able to pass these directly into DeepAgents would reduce friction and boilerplate.

Thanks for considering this! Happy to adjust the proposal if there’s a better place in the API to expose SystemMessage support (e.g. a separate system_message parameter, a builder pattern, etc.).

Originally created by @alario-tang on GitHub (Dec 21, 2025). Originally assigned to: @sydney-runkle on GitHub. #### Summary `langchain.agents.create_agent` now supports: ```python system_prompt: str | SystemMessage | None = None ``` However, `deepagents.create_deep_agent` still exposes: ```python system_prompt: str | None = None ``` This means DeepAgents cannot directly consume a `SystemMessage` (or a pipeline that already models system prompts as `SystemMessage`), even though the underlying `create_agent` does support it. It would be very helpful if `create_deep_agent` could accept both `str` **and** `SystemMessage`, and internally adapt to the existing DeepAgents prompt composition logic. --- #### Motivation * In many codebases, system prompts are already modeled as **structured objects** (`SystemMessage` or custom prompt objects) rather than plain strings. * `create_agent` now allows passing `SystemMessage` directly, which makes it easy to reuse these higher-level models when building agents. * DeepAgents is positioned as a higher-level **Agent Harness** on top of LangGraph/LangChain. It would be natural if it did not “force” callers to downcast back to `str` while the lower-level `create_agent` already supports `SystemMessage`. Concretely, in our project: * We maintain our own `SystemPrompt` / `SystemMessage` abstraction (with sections like *role*, *constraints*, *style*, etc.). * For `create_agent`, we can now pass a `SystemMessage` directly. * For `create_deep_agent`, we currently need a separate `to_legacy_system_prompt_str()` adapter solely to satisfy the `str` type, even though downstream everything ends up as messages anyway. This makes DeepAgents feel “less typed” than the underlying `create_agent`, and it complicates sharing a common prompt model between DeepAgents and non-DeepAgents agents. --- #### Current behavior ```python from deepagents import create_deep_agent from langchain_core.messages import SystemMessage sys_msg = SystemMessage(content="You are a deep research assistant...") # ❌ Type mismatch: create_deep_agent only accepts `str | None` agent = create_deep_agent( model="openai:gpt-4o", tools=[...], system_prompt=sys_msg, # not allowed by the type signature ) ``` We instead have to do: ```python agent = create_deep_agent( model="openai:gpt-4o", tools=[...], system_prompt=sys_msg.content, # manual flattening ) ``` or: ```python system_prompt = my_system_prompt.to_legacy_system_prompt_str() agent = create_deep_agent( model="openai:gpt-4o", tools=[...], system_prompt=system_prompt, ) ``` --- #### Desired behavior Ideally, `create_deep_agent` would have a signature like: ```python from langchain_core.messages import SystemMessage def create_deep_agent( model: str | BaseChatModel | None = None, tools: Sequence[BaseTool | Callable | dict[str, Any]] | None = None, *, system_prompt: str | SystemMessage | None = None, ... ) -> CompiledStateGraph[...]: ... ``` Semantics: * If `system_prompt` is a `str`, keep the current behavior (prepend to the base DeepAgents system prompt). * If `system_prompt` is a `SystemMessage`, internally convert it to a string (or to a single system message block) following the existing DeepAgents composition logic. * If `None`, keep the current default behavior (use the built-in base prompt). This keeps full backwards compatibility while allowing higher-level code to pass `SystemMessage` directly, just like `create_agent` does. --- #### Alternatives considered * **Keep a separate `to_legacy_system_prompt_str()` adapter on the user side** This works today, but: * It duplicates logic that `create_agent` already encapsulates for `SystemMessage`. * It forces DeepAgents users to “downgrade” to `str` even if their internal representation is already `SystemMessage`. * **Bypass `create_deep_agent` and manually compose middlewares + `create_agent`** Technically possible, but it defeats the purpose of DeepAgents as a higher-level harness: * We would have to reimplement parts of DeepAgents behavior (planning, filesystem, subagents, self-management) just to support `SystemMessage` at the top level. * This increases complexity and fragmentation across projects. --- #### Backwards compatibility * The change is **fully backward compatible** if implemented as `str | SystemMessage | None`: * Existing code passing `str` continues to work. * New code can opt in to pass `SystemMessage`. * Internally, DeepAgents can still normalize everything to a single system prompt string if that’s what the current infrastructure expects. --- #### Additional context * `langchain.agents.create_agent` already supports `SystemMessage` for `system_prompt`, so aligning DeepAgents with this behavior would make the ecosystem **more consistent** and easier to reason about. * Many of the emerging patterns around **provider-specific system prompts** (e.g. Anthropic multi-block system prompts, cache control, etc.) are modeled in user code as `SystemMessage` or higher-level prompt objects. Being able to pass these directly into DeepAgents would reduce friction and boilerplate. Thanks for considering this! Happy to adjust the proposal if there’s a better place in the API to expose `SystemMessage` support (e.g. a separate `system_message` parameter, a builder pattern, etc.).
yindo added the featureconfighelp wanted labels 2026-02-16 08:20:23 -05:00
yindo closed this issue 2026-02-16 08:20:23 -05:00
Author
Owner

@eyurtsev commented on GitHub (Dec 23, 2025):

If someone from the community wants to make a PR for this and match the implementation in langchain.agent.create_agent I'll be happy to review

@eyurtsev commented on GitHub (Dec 23, 2025): If someone from the community wants to make a PR for this and match the implementation in `langchain.agent.create_agent` I'll be happy to review
Author
Owner

@ps06756 commented on GitHub (Dec 23, 2025):

@eyurtsev I will take this up and raise a PR.

@ps06756 commented on GitHub (Dec 23, 2025): @eyurtsev I will take this up and raise a PR.
Author
Owner

@RussellLuo commented on GitHub (Dec 23, 2025):

@ps06756 Oh, sorry, I didn't see your reply before submitting the PR.

@RussellLuo commented on GitHub (Dec 23, 2025): @ps06756 Oh, sorry, I didn't see your reply before submitting the PR.
Author
Owner

@ps06756 commented on GitHub (Dec 25, 2025):

@RussellLuo I have linked a new PR on top of your's to add unit tests.

@ps06756 commented on GitHub (Dec 25, 2025): @RussellLuo I have linked a new PR on top of your's to add unit tests.
Author
Owner

@alario-tang commented on GitHub (Jan 6, 2026):

@RussellLuo

Thanks for the update — aligning deepagents.create_deep_agent(system_prompt=...) with langchain.agents.create_agent and accepting SystemMessage is definitely the right direction.

One concern: the current implementation assumes SystemMessage.content is always a str and does string concatenation (content + "\n\n" + BASE_AGENT_PROMPT). In our case we pass SystemMessage(content=[...blocks...]) (i.e., block/list content), and this will either:

  1. TypeError at runtime when attempting list + str, or
  2. Force users to flatten structured system content into a plain string.

Also, by creating a new SystemMessage(content=...), you may inadvertently drop message metadata (additional_kwargs, name/id, provider-specific fields, etc.) that can matter for some integrations.

Suggestion: handle both string and block/list content and preserve the original SystemMessage fields when possible. For example (rough sketch):

from langchain_core.messages import SystemMessage

def _merge_system_prompt(system_prompt: str | SystemMessage | None) -> SystemMessage:
    base = BASE_AGENT_PROMPT

    if system_prompt is None:
        return SystemMessage(content=base)

    if isinstance(system_prompt, SystemMessage):
        content = system_prompt.content
        if isinstance(content, str):
            new_content = f"{content}\n\n{base}"
        elif isinstance(content, list):
            # Append base as an additional text block (block schema may vary by provider)
            new_content = content + [{"type": "text", "text": base}]
        else:
            # Fallback: coerce to string
            new_content = f"{str(content)}\n\n{base}"

        # Preserve other fields on the original SystemMessage
        return system_prompt.model_copy(update={"content": new_content})

    # system_prompt is a str
    return SystemMessage(content=f"{system_prompt}\n\n{base}")

It would also be great to add tests for:

  • system_prompt as str
  • system_prompt as SystemMessage(content="...")
  • system_prompt as SystemMessage(content=[...blocks...]) (ensuring it doesn’t crash and preserves blocks + metadata)

Happy to help tailor this to the exact “block” schema you’re targeting (Anthropic-style blocks vs other formats), but at minimum we need to avoid assuming content is always a string.

@alario-tang commented on GitHub (Jan 6, 2026): @RussellLuo Thanks for the update — aligning `deepagents.create_deep_agent(system_prompt=...)` with `langchain.agents.create_agent` and accepting `SystemMessage` is definitely the right direction. One concern: the current implementation assumes `SystemMessage.content` is always a `str` and does string concatenation (`content + "\n\n" + BASE_AGENT_PROMPT`). In our case we pass `SystemMessage(content=[...blocks...])` (i.e., block/list content), and this will either: 1. **TypeError** at runtime when attempting `list + str`, or 2. Force users to flatten structured system content into a plain string. Also, by creating a new `SystemMessage(content=...)`, you may inadvertently **drop message metadata** (`additional_kwargs`, name/id, provider-specific fields, etc.) that can matter for some integrations. **Suggestion:** handle both string and block/list content and preserve the original `SystemMessage` fields when possible. For example (rough sketch): ```python from langchain_core.messages import SystemMessage def _merge_system_prompt(system_prompt: str | SystemMessage | None) -> SystemMessage: base = BASE_AGENT_PROMPT if system_prompt is None: return SystemMessage(content=base) if isinstance(system_prompt, SystemMessage): content = system_prompt.content if isinstance(content, str): new_content = f"{content}\n\n{base}" elif isinstance(content, list): # Append base as an additional text block (block schema may vary by provider) new_content = content + [{"type": "text", "text": base}] else: # Fallback: coerce to string new_content = f"{str(content)}\n\n{base}" # Preserve other fields on the original SystemMessage return system_prompt.model_copy(update={"content": new_content}) # system_prompt is a str return SystemMessage(content=f"{system_prompt}\n\n{base}") ``` It would also be great to add tests for: * `system_prompt` as `str` * `system_prompt` as `SystemMessage(content="...")` * `system_prompt` as `SystemMessage(content=[...blocks...])` (ensuring it doesn’t crash and preserves blocks + metadata) Happy to help tailor this to the exact “block” schema you’re targeting (Anthropic-style blocks vs other formats), but at minimum we need to avoid assuming `content` is always a string.
Author
Owner

@ps06756 commented on GitHub (Jan 11, 2026):

@alario-tang I have created a new PR, since I don't have access to edit the current PR.

It addresses the comments discussed here: https://github.com/langchain-ai/deepagents/pull/713 , @RussellLuo Added you as co-author since you had started with the PR initially.

It has tests as well

@ps06756 commented on GitHub (Jan 11, 2026): @alario-tang I have created a new PR, since I don't have access to edit the current PR. It addresses the comments discussed here: https://github.com/langchain-ai/deepagents/pull/713 , @RussellLuo Added you as co-author since you had started with the PR initially. It has tests as well
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/deepagents#225