Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
c4e48b5
perf(backend): enable cross-user prompt caching via SystemPromptPreset
majdyz Apr 13, 2026
54f507b
fix(backend): address PR review — extract testable helper, add TypedD…
majdyz Apr 13, 2026
fa6cc99
fix(backend): format service.py and test files
majdyz Apr 13, 2026
0ab7c98
fix: remove accidentally committed worktree, add to gitignore
majdyz Apr 13, 2026
cd8079d
test: add CHAT_CLAUDE_AGENT_CROSS_USER_PROMPT_CACHE to _CONFIG_ENV_VARS
majdyz Apr 13, 2026
b7bec5d
fix(backend): align _SystemPromptPreset with SDK shape, drop unused m…
majdyz Apr 13, 2026
f6f70e1
fix(backend): add SystemPromptPreset compat test, move inline import …
majdyz Apr 13, 2026
34832ca
test(backend): compat-test the exact preset dict sent to ClaudeAgentO…
majdyz Apr 13, 2026
2cf737d
fix(backend): address review comments on cross-user prompt caching PR
majdyz Apr 13, 2026
ae8608a
fix(backend): make _SystemPromptPreset.append required, remove hand-r…
majdyz Apr 13, 2026
5f366f3
dx(backend): fix black formatting in sdk_compat_test.py
majdyz Apr 14, 2026
2ba2a2b
fix(backend): move _SystemPromptPreset TypedDict below all imports
majdyz Apr 14, 2026
7d48765
fix: resolve merge conflicts with dev
majdyz Apr 14, 2026
43b3377
fix(backend/copilot): disable excludeDynamicSections preset on --resu…
majdyz Apr 14, 2026
896ebea
refactor(backend/copilot): replace local _SystemPromptPreset with SDK…
majdyz Apr 14, 2026
f80d810
fix(backend/copilot): recompute system_prompt on retry when use_resum…
majdyz Apr 14, 2026
424e8e3
fix(backend/copilot): resolve merge conflict with dev (PR #12777 sess…
majdyz Apr 14, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,4 @@ test.db
.next
# Implementation plans (generated by AI agents)
plans/
.claude/worktrees/
Comment thread
majdyz marked this conversation as resolved.
Comment thread
majdyz marked this conversation as resolved.
Comment thread
majdyz marked this conversation as resolved.
9 changes: 9 additions & 0 deletions autogpt_platform/backend/backend/copilot/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,15 @@ class ChatConfig(BaseSettings):
description="Maximum number of retries for transient API errors "
"(429, 5xx, ECONNRESET) before surfacing the error to the user.",
)
claude_agent_cross_user_prompt_cache: bool = Field(
default=True,
description="Enable cross-user prompt caching via SystemPromptPreset. "
"The Claude Code default prompt becomes a cacheable prefix shared "
"across all users, and our custom prompt is appended after it. "
"Dynamic sections (working dir, git status, auto-memory) are excluded "
"from the prefix. Set to False to fall back to passing the system "
"prompt as a raw string.",
)
use_openrouter: bool = Field(
default=True,
description="Enable routing API calls through the OpenRouter proxy. "
Expand Down
53 changes: 51 additions & 2 deletions autogpt_platform/backend/backend/copilot/sdk/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,23 @@
import uuid
from collections.abc import AsyncGenerator, AsyncIterator
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, NamedTuple, cast
from typing import TYPE_CHECKING, Any, Literal, NamedTuple, TypedDict, cast
Comment thread
majdyz marked this conversation as resolved.
Outdated


Comment thread
majdyz marked this conversation as resolved.
Outdated
class _SystemPromptPreset(TypedDict):
Comment thread
majdyz marked this conversation as resolved.
Outdated
"""Local stand-in for the SDK's SystemPromptPreset.
Comment thread
majdyz marked this conversation as resolved.
Outdated

The production SDK (>=0.1.58) defines this type natively. We keep a
local copy so that:
1. The code type-checks on older SDK pins (e.g. 0.1.45 on dev).
2. Tests can import and verify the shape without an SDK upgrade.
"""

type: Literal["preset"]
preset: Literal["claude_code"]
append: str
Comment thread
majdyz marked this conversation as resolved.
Outdated
exclude_dynamic_sections: bool


if TYPE_CHECKING:
from backend.copilot.permissions import CopilotPermissions
Expand Down Expand Up @@ -699,6 +715,28 @@ def _is_fallback_stderr(line: str) -> bool:
return "fallback model" in line.lower()


def _build_system_prompt_value(
Comment thread
majdyz marked this conversation as resolved.
system_prompt: str,
cross_user_cache: bool,
) -> str | _SystemPromptPreset:
"""Build the ``system_prompt`` argument for :class:`ClaudeAgentOptions`.

When *cross_user_cache* is enabled, returns a :class:`_SystemPromptPreset`
dict so the Claude Code default prompt becomes a cacheable prefix shared
across all users; our custom *system_prompt* is appended after it.

When disabled, the raw *system_prompt* string is returned unchanged.
"""
if cross_user_cache:
return _SystemPromptPreset(
Comment thread
majdyz marked this conversation as resolved.
Outdated
type="preset",
preset="claude_code",
append=system_prompt,
exclude_dynamic_sections=True,
)
Comment thread
majdyz marked this conversation as resolved.
return system_prompt


def _make_sdk_cwd(session_id: str) -> str:
"""Create a safe, session-specific working directory path.

Expand Down Expand Up @@ -2220,8 +2258,19 @@ def _on_stderr(line: str) -> None:
sid,
)

# When cross-user prompt caching is enabled, use SystemPromptPreset
Comment thread
majdyz marked this conversation as resolved.
Outdated
# so the Claude Code default prompt is a cacheable prefix shared
# across all users. Our custom prompt is appended after it and
# dynamic sections (working dir, git status, auto-memory) are
# excluded from the prefix — giving us cross-user cache hits that
# reduce input token cost by ~90%.
Comment thread
majdyz marked this conversation as resolved.
Outdated
system_prompt_value = _build_system_prompt_value(
system_prompt,
cross_user_cache=config.claude_agent_cross_user_prompt_cache,
)

sdk_options_kwargs: dict[str, Any] = {
"system_prompt": system_prompt,
"system_prompt": system_prompt_value,
Comment thread
majdyz marked this conversation as resolved.
Comment thread
majdyz marked this conversation as resolved.
"mcp_servers": {"copilot": mcp_server},
"allowed_tools": allowed,
"disallowed_tools": disallowed,
Comment thread
majdyz marked this conversation as resolved.
Expand Down
42 changes: 42 additions & 0 deletions autogpt_platform/backend/backend/copilot/sdk/service_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import pytest

from .service import (
_build_system_prompt_value,
_is_sdk_disconnect_error,
_normalize_model_name,
_prepare_file_attachments,
Expand Down Expand Up @@ -397,6 +398,7 @@ async def test_rejects_path_outside_prefix(self, tmp_path):
"OPENAI_BASE_URL",
"CHAT_USE_CLAUDE_CODE_SUBSCRIPTION",
"CHAT_USE_CLAUDE_AGENT_SDK",
"CHAT_CLAUDE_AGENT_CROSS_USER_PROMPT_CACHE",
Comment thread
majdyz marked this conversation as resolved.
)


Expand Down Expand Up @@ -656,3 +658,43 @@ async def test_unrelated_value_error_propagates(self):
client.__aexit__ = AsyncMock(side_effect=ValueError("invalid argument"))
Comment thread
majdyz marked this conversation as resolved.
with pytest.raises(ValueError, match="invalid argument"):
await _safe_close_sdk_client(client, "[test]")


# ---------------------------------------------------------------------------
# SystemPromptPreset — cross-user prompt caching
# ---------------------------------------------------------------------------


class TestSystemPromptPreset:
Comment thread
majdyz marked this conversation as resolved.
"""Tests for _build_system_prompt_value — cross-user prompt caching."""

Comment thread
majdyz marked this conversation as resolved.
def test_preset_dict_structure_when_enabled(self):
"""When cross_user_cache is True, returns a _SystemPromptPreset dict."""
custom_prompt = "You are a helpful assistant."
result = _build_system_prompt_value(custom_prompt, cross_user_cache=True)

assert isinstance(result, dict)
assert result["type"] == "preset"
assert result["preset"] == "claude_code"
assert result["append"] == custom_prompt
assert result["exclude_dynamic_sections"] is True

def test_raw_string_when_disabled(self):
"""When cross_user_cache is False, returns the raw string."""
custom_prompt = "You are a helpful assistant."
result = _build_system_prompt_value(custom_prompt, cross_user_cache=False)

assert isinstance(result, str)
assert result == custom_prompt

def test_default_config_is_enabled(self, monkeypatch, _clean_config_env):
Comment thread
majdyz marked this conversation as resolved.
Outdated
"""The default value for claude_agent_cross_user_prompt_cache is True."""
from backend.copilot import config as cfg_mod

cfg = cfg_mod.ChatConfig(
use_openrouter=False,
api_key=None,
base_url=None,
use_claude_code_subscription=False,
)
assert cfg.claude_agent_cross_user_prompt_cache is True
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ def test_large_value(self):
assert usd_to_microdollars(1.0) == 1_000_000



class TestMaskEmail:
def test_typical_email(self):
assert _mask_email("user@example.com") == "us***@example.com"
Expand Down
Loading