Created src/prompt_constants.py (550+ lines) porting all constants from npm src/constants/:

┌──────────────────┬───────────────────────────┬───────────────────────────────────────────────┐
│ Category         │ npm Source                │ Items                                         │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Product metadata │ product.ts                │ URLs, base URLs                               │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ System prefixes  │ system.ts                 │ 3 prompt prefixes                             │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Cyber risk       │ cyberRiskInstruction.ts   │ Safety instruction                            │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ API limits       │ apiLimits.ts              │ 10 image/PDF/media limits                     │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Tool limits      │ toolLimits.ts             │ 6 result size constants                       │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Spinner verbs    │ spinnerVerbs.ts           │ 187 whimsical gerunds                         │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Completion verbs │ turnCompletionVerbs.ts    │ 8 past-tense verbs                            │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Figures/symbols  │ figures.ts                │ 25 Unicode UI symbols                         │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ XML tags         │ xml.ts                    │ 30+ tag constants                             │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Messages         │ messages.ts               │ NO_CONTENT_MESSAGE                            │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Date utilities   │ common.ts                 │ 4 functions                                   │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Section caching  │ systemPromptSections.ts   │ Memoized/volatile sections                    │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Output styles    │ outputStyles.ts           │ 3 built-in configs                            │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Prompt helpers   │ prompts.ts                │ Knowledge cutoff, language, scratchpad, hooks │
└──────────────────┴───────────────────────────┴───────────────────────────────────────────────┘

91 new tests in tests/test_prompt_constants.py. All 17 SQL todos done.
This commit is contained in:
Abdelrahman Abdallah
2026-04-08 00:02:53 +02:00
parent 90489e7bfc
commit aacf0a212a
13 changed files with 5872 additions and 304 deletions
+9
View File
@@ -99,6 +99,8 @@ class LocalCodingAgent:
worktree_runtime: WorktreeRuntime | None = None
last_session: AgentSessionState | None = field(default=None, init=False, repr=False)
last_run_result: AgentRunResult | None = field(default=None, init=False, repr=False)
cumulative_usage: UsageStats = field(default_factory=UsageStats, init=False, repr=False)
cumulative_cost_usd: float = field(default=0.0, init=False, repr=False)
active_session_id: str | None = field(default=None, init=False, repr=False)
last_session_path: str | None = field(default=None, init=False, repr=False)
managed_agent_id: str | None = field(default=None, init=False, repr=False)
@@ -321,6 +323,7 @@ class LocalCodingAgent:
scratchpad_directory=scratchpad_directory,
existing_file_history=(),
)
self._accumulate_usage(result)
self._finalize_managed_agent(result)
return result
@@ -357,6 +360,7 @@ class LocalCodingAgent:
scratchpad_directory=scratchpad_directory,
existing_file_history=stored_session.file_history,
)
self._accumulate_usage(result)
self._finalize_managed_agent(result)
return result
@@ -3363,6 +3367,11 @@ class LocalCodingAgent:
)
self.resume_source_session_id = None
def _accumulate_usage(self, result: AgentRunResult) -> None:
"""Add a run's usage to the cumulative session totals."""
self.cumulative_usage = self.cumulative_usage + result.usage
self.cumulative_cost_usd += result.total_cost_usd
def _refresh_runtime_views_for_tool_result(
self,
tool_name: str,
+476
View File
@@ -294,6 +294,71 @@ def get_slash_command_specs() -> tuple[SlashCommandSpec, ...]:
description='Clear ephemeral Python runtime state for this process.',
handler=_handle_clear,
),
SlashCommandSpec(
names=('compact',),
description='Summarise and compact the conversation to free context space.',
handler=_handle_compact,
),
SlashCommandSpec(
names=('cost',),
description='Show the total cost and duration of the current session.',
handler=_handle_cost,
),
SlashCommandSpec(
names=('exit', 'quit'),
description='Exit the REPL.',
handler=_handle_exit,
),
SlashCommandSpec(
names=('diff',),
description='View uncommitted changes (git diff) in the working directory.',
handler=_handle_diff,
),
SlashCommandSpec(
names=('files',),
description='List files currently loaded in the session context.',
handler=_handle_files,
),
SlashCommandSpec(
names=('copy',),
description='Copy the last assistant response to a temp file.',
handler=_handle_copy,
),
SlashCommandSpec(
names=('export',),
description='Export the conversation to a text file.',
handler=_handle_export,
),
SlashCommandSpec(
names=('stats',),
description='Show session usage statistics.',
handler=_handle_stats,
),
SlashCommandSpec(
names=('tag',),
description='Add or remove a searchable tag on the current session.',
handler=_handle_tag,
),
SlashCommandSpec(
names=('rename',),
description='Rename the current conversation.',
handler=_handle_rename,
),
SlashCommandSpec(
names=('branch',),
description='Create a fork/branch of the current conversation.',
handler=_handle_branch,
),
SlashCommandSpec(
names=('effort',),
description='Show or set the model effort level (low, medium, high, max, auto).',
handler=_handle_effort,
),
SlashCommandSpec(
names=('doctor',),
description='Diagnose and verify the claw-code installation and settings.',
handler=_handle_doctor,
),
)
@@ -620,6 +685,417 @@ def _handle_clear(agent: 'LocalCodingAgent', _args: str, input_text: str) -> Sla
)
def _handle_compact(agent: 'LocalCodingAgent', args: str, input_text: str) -> SlashCommandResult:
from .compact import compact_conversation
custom_instructions = args.strip() if args.strip() else None
result = compact_conversation(agent, custom_instructions)
if result.error:
return _local_result(input_text, f'Compact failed: {result.error}')
lines = ['Conversation compacted.']
if result.pre_compact_token_count:
lines.append(
f' Tokens before: ~{result.pre_compact_token_count:,} '
f'→ after: ~{result.post_compact_token_count:,}'
)
return _local_result(input_text, '\n'.join(lines))
def _handle_cost(agent: 'LocalCodingAgent', _args: str, input_text: str) -> SlashCommandResult:
usage = agent.cumulative_usage
cost = agent.cumulative_cost_usd
def _fmt_cost(usd: float) -> str:
if usd < 0.01:
return f'${usd:.4f}'
return f'${usd:.2f}'
lines = [
f'Total cost: {_fmt_cost(cost)}',
f'Total input tokens: {usage.input_tokens:,}',
f'Total output tokens: {usage.output_tokens:,}',
]
if usage.cache_read_input_tokens:
lines.append(f'Cache read tokens: {usage.cache_read_input_tokens:,}')
if usage.cache_creation_input_tokens:
lines.append(f'Cache creation tokens: {usage.cache_creation_input_tokens:,}')
if usage.reasoning_tokens:
lines.append(f'Reasoning tokens: {usage.reasoning_tokens:,}')
lines.append(f'Total tokens: {usage.total_tokens:,}')
return _local_result(input_text, '\n'.join(lines))
def _handle_exit(agent: 'LocalCodingAgent', _args: str, input_text: str) -> SlashCommandResult:
import random
import sys
messages = ['Goodbye!', 'See ya!', 'Bye!', 'Catch you later!']
output = random.choice(messages)
# Build the result first so the transcript is recorded, then exit.
result = _local_result(input_text, output)
print(output)
sys.exit(0)
return result # unreachable, but satisfies the type checker
def _handle_diff(agent: 'LocalCodingAgent', _args: str, input_text: str) -> SlashCommandResult:
import subprocess
cwd = str(agent.runtime_config.cwd)
try:
proc = subprocess.run(
['git', 'diff'],
cwd=cwd,
capture_output=True,
text=True,
timeout=15,
)
diff_output = proc.stdout.strip()
if not diff_output:
# Also check staged changes
proc_staged = subprocess.run(
['git', 'diff', '--staged'],
cwd=cwd,
capture_output=True,
text=True,
timeout=15,
)
diff_output = proc_staged.stdout.strip()
if not diff_output:
return _local_result(input_text, 'No uncommitted changes.')
return _local_result(input_text, f'Staged changes:\n{diff_output}')
return _local_result(input_text, diff_output)
except FileNotFoundError:
return _local_result(input_text, 'git is not available.')
except subprocess.TimeoutExpired:
return _local_result(input_text, 'git diff timed out.')
except Exception as exc:
return _local_result(input_text, f'Error running git diff: {exc}')
def _handle_files(agent: 'LocalCodingAgent', _args: str, input_text: str) -> SlashCommandResult:
"""List files loaded in the session context (from readFileState)."""
session = agent.last_session
if session is None:
return _local_result(input_text, 'No active session.')
# Collect file paths mentioned in tool results
file_paths: list[str] = []
for msg in session.messages:
if msg.role == 'tool' and msg.name in ('Read', 'read_file', 'ReadFile'):
# Extract path from content or metadata
path = msg.metadata.get('path')
if isinstance(path, str):
file_paths.append(path)
elif msg.content and msg.content.startswith('/'):
# First line might be the path
first_line = msg.content.split('\n', 1)[0].strip()
if '/' in first_line and len(first_line) < 256:
file_paths.append(first_line)
# Also look at tool_calls in assistant messages
for msg in session.messages:
if msg.role == 'assistant' and msg.tool_calls:
for tc in msg.tool_calls:
func = tc.get('function', {}) if isinstance(tc, dict) else {}
if func.get('name') in ('Read', 'read_file', 'ReadFile', 'View'):
import json as _json
try:
args = _json.loads(func.get('arguments', '{}'))
path = args.get('file_path') or args.get('path')
if isinstance(path, str):
file_paths.append(path)
except (ValueError, TypeError):
pass
# Deduplicate preserving order
seen: set[str] = set()
unique_paths: list[str] = []
for p in file_paths:
if p not in seen:
seen.add(p)
unique_paths.append(p)
if not unique_paths:
return _local_result(input_text, 'No files loaded in context.')
cwd = str(agent.runtime_config.cwd)
relative_paths = []
for p in unique_paths:
if p.startswith(cwd):
relative_paths.append(p[len(cwd):].lstrip('/'))
else:
relative_paths.append(p)
lines = [f'Files in context ({len(relative_paths)}):']
for p in relative_paths:
lines.append(f' {p}')
return _local_result(input_text, '\n'.join(lines))
def _handle_copy(agent: 'LocalCodingAgent', args: str, input_text: str) -> SlashCommandResult:
"""Copy the last assistant response to a temp file."""
import tempfile as _tempfile
session = agent.last_session
if session is None:
return _local_result(input_text, 'No active session.')
# Find the Nth most recent assistant message (default N=0 = latest)
n = 0
if args.strip().isdigit():
n = min(int(args.strip()), 20)
assistant_messages = [
msg for msg in session.messages
if msg.role == 'assistant' and msg.content.strip()
]
if not assistant_messages:
return _local_result(input_text, 'No assistant responses to copy.')
index = len(assistant_messages) - 1 - n
if index < 0:
return _local_result(
input_text,
f'Only {len(assistant_messages)} assistant responses available.',
)
content = assistant_messages[index].content
# Write to temp file
from pathlib import Path as _Path
tmp_dir = _Path(_tempfile.gettempdir()) / 'claw-code'
tmp_dir.mkdir(parents=True, exist_ok=True)
out_path = tmp_dir / 'response.md'
out_path.write_text(content, encoding='utf-8')
char_count = len(content)
line_count = content.count('\n') + 1
return _local_result(
input_text,
f'Copied {char_count:,} chars ({line_count} lines) to {out_path}',
)
def _handle_export(agent: 'LocalCodingAgent', args: str, input_text: str) -> SlashCommandResult:
"""Export the conversation transcript to a text file."""
from pathlib import Path as _Path
import time as _time
session = agent.last_session
if session is None:
return _local_result(input_text, 'No active session to export.')
# Build plain-text transcript
lines: list[str] = []
for msg in session.messages:
label = msg.role.upper()
if msg.role == 'tool' and msg.name:
label = f'TOOL:{msg.name}'
lines.append(f'--- {label} ---')
lines.append(msg.content)
lines.append('')
text = '\n'.join(lines)
# Determine output path
filename = args.strip()
if not filename:
timestamp = _time.strftime('%Y%m%d_%H%M%S')
filename = f'conversation_{timestamp}.txt'
if not filename.endswith('.txt'):
filename += '.txt'
out_path = _Path(str(agent.runtime_config.cwd)) / filename
out_path.write_text(text, encoding='utf-8')
return _local_result(
input_text,
f'Exported {len(session.messages)} messages to {out_path}',
)
def _handle_stats(agent: 'LocalCodingAgent', _args: str, input_text: str) -> SlashCommandResult:
"""Show session usage statistics."""
usage = agent.cumulative_usage
cost = agent.cumulative_cost_usd
session = agent.last_session
msg_count = len(session.messages) if session else 0
user_msgs = sum(1 for m in (session.messages if session else []) if m.role == 'user')
assistant_msgs = sum(1 for m in (session.messages if session else []) if m.role == 'assistant')
tool_msgs = sum(1 for m in (session.messages if session else []) if m.role == 'tool')
lines = [
'## Session Statistics',
'',
f'Messages: {msg_count} total ({user_msgs} user, {assistant_msgs} assistant, {tool_msgs} tool)',
f'Input tokens: {usage.input_tokens:,}',
f'Output tokens: {usage.output_tokens:,}',
f'Total tokens: {usage.total_tokens:,}',
f'Cost: ${cost:.4f}',
f'Model: {agent.model_config.model}',
]
if agent.active_session_id:
lines.append(f'Session ID: {agent.active_session_id}')
return _local_result(input_text, '\n'.join(lines))
def _handle_tag(agent: 'LocalCodingAgent', args: str, input_text: str) -> SlashCommandResult:
"""Add or remove a tag on the current session."""
tag = args.strip()
if not tag:
# Show current tags
tags = getattr(agent, '_session_tags', set())
if tags:
return _local_result(input_text, f'Session tags: {", ".join(sorted(tags))}')
return _local_result(input_text, 'No tags set. Usage: /tag <tag-name>')
# Toggle tag
if not hasattr(agent, '_session_tags'):
agent._session_tags = set()
if tag in agent._session_tags:
agent._session_tags.discard(tag)
return _local_result(input_text, f'Removed tag: {tag}')
agent._session_tags.add(tag)
return _local_result(input_text, f'Added tag: {tag}')
def _handle_rename(agent: 'LocalCodingAgent', args: str, input_text: str) -> SlashCommandResult:
"""Rename the current conversation."""
name = args.strip()
if not name:
return _local_result(input_text, 'Usage: /rename <name>')
if not hasattr(agent, '_session_name'):
agent._session_name = None
agent._session_name = name
return _local_result(input_text, f'Session renamed to: {name}')
def _handle_branch(agent: 'LocalCodingAgent', args: str, input_text: str) -> SlashCommandResult:
"""Create a fork/branch of the current conversation."""
import json as _json
from uuid import uuid4
session = agent.last_session
if session is None:
return _local_result(input_text, 'No active session to branch.')
branch_name = args.strip() or f'branch-{uuid4().hex[:8]}'
new_session_id = uuid4().hex
# Save a copy of the current transcript as a new session file
session_dir = agent.runtime_config.session_directory
session_dir.mkdir(parents=True, exist_ok=True)
session_path = session_dir / f'{new_session_id}.json'
transcript = [msg.to_transcript_entry() for msg in session.messages]
branch_data = {
'session_id': new_session_id,
'branch_name': branch_name,
'branched_from': agent.active_session_id,
'messages': transcript,
'model': agent.model_config.model,
}
try:
session_path.write_text(_json.dumps(branch_data, indent=2), encoding='utf-8')
return _local_result(
input_text,
f'Created branch "{branch_name}" (session: {new_session_id})\n'
f'Saved to: {session_path}',
)
except Exception as exc:
return _local_result(input_text, f'Error creating branch: {exc}')
def _handle_effort(agent: 'LocalCodingAgent', args: str, input_text: str) -> SlashCommandResult:
"""Show or set the model effort level."""
import os
valid_levels = ('low', 'medium', 'high', 'max', 'auto')
current = getattr(agent.runtime_config, 'effort_level', None)
env_override = os.environ.get('CLAUDE_CODE_EFFORT_LEVEL')
if not args.strip():
level = current or env_override or 'auto'
msg = f'Current effort level: {level}'
if env_override:
msg += f' (from CLAUDE_CODE_EFFORT_LEVEL env var)'
return _local_result(input_text, msg)
level = args.strip().lower()
if level not in valid_levels:
return _local_result(
input_text,
f'Invalid effort level: {level}\nValid levels: {", ".join(valid_levels)}',
)
if env_override:
return _local_result(
input_text,
f'Cannot change effort level — overridden by '
f'CLAUDE_CODE_EFFORT_LEVEL={env_override}',
)
# Store effort level on the runtime config
object.__setattr__(agent.runtime_config, 'effort_level', level)
return _local_result(input_text, f'Set effort level to: {level}')
def _handle_doctor(agent: 'LocalCodingAgent', _args: str, input_text: str) -> SlashCommandResult:
"""Diagnose and verify the claw-code installation."""
import os
import shutil
import sys
from pathlib import Path as _Path
checks: list[str] = []
# Python version
py_ver = f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}'
ok = sys.version_info >= (3, 10)
checks.append(f'{"" if ok else ""} Python version: {py_ver} (need ≥3.10)')
# Git available
git_ok = shutil.which('git') is not None
checks.append(f'{"" if git_ok else ""} git: {"found" if git_ok else "NOT FOUND"}')
# Model config
checks.append(f'✓ Model: {agent.model_config.model}')
checks.append(f'✓ Base URL: {agent.model_config.base_url}')
# Working directory
cwd = agent.runtime_config.cwd
checks.append(f'✓ Working directory: {cwd}')
checks.append(f'{"" if cwd.exists() else ""} Working directory exists: {cwd.exists()}')
# Session directory
sess_dir = agent.runtime_config.session_directory
checks.append(f'✓ Session directory: {sess_dir}')
checks.append(f'{"" if sess_dir.exists() else ""} Session directory exists: {sess_dir.exists()}')
# API key
has_key = bool(agent.model_config.api_key)
checks.append(f'{"" if has_key else ""} API key: {"set" if has_key else "NOT SET"}')
# Tools
tool_count = len(agent.tool_registry) if agent.tool_registry else 0
checks.append(f'✓ Registered tools: {tool_count}')
# Memory files (CLAUDE.md)
claude_md = cwd / 'CLAUDE.md'
checks.append(
f'{"" if claude_md.exists() else ""} CLAUDE.md: '
f'{"found" if claude_md.exists() else "not found (optional)"}'
)
output = '## Doctor Report\n\n' + '\n'.join(checks)
return _local_result(input_text, output)
def _local_result(input_text: str, output: str) -> SlashCommandResult:
transcript = (
{'role': 'user', 'content': input_text},
+1261
View File
File diff suppressed because it is too large Load Diff
+438
View File
@@ -0,0 +1,438 @@
"""Conversation compaction service.
Mirrors the npm ``src/services/compact/compact.ts`` and
``src/services/compact/prompt.ts`` modules. Provides:
- The 9-section summarisation prompt (``get_compact_prompt``).
- XML-tag formatting/stripping (``format_compact_summary``).
- The post-compact user summary message builder
(``get_compact_user_summary_message``).
- The core ``compact_conversation`` entry point that an
``/compact`` slash command or auto-compact subsystem can call.
"""
from __future__ import annotations
import re
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any
from .agent_context_usage import estimate_tokens
from .agent_session import AgentMessage
if TYPE_CHECKING:
from .agent_runtime import LocalCodingAgent
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
AUTOCOMPACT_BUFFER_TOKENS = 13_000
"""How many tokens to reserve below the effective context window before
auto-compact fires (same as the npm ``AUTOCOMPACT_BUFFER_TOKENS``)."""
ERROR_NOT_ENOUGH_MESSAGES = 'Not enough messages to compact.'
ERROR_INCOMPLETE_RESPONSE = (
'The summary response was incomplete. '
'The conversation was not compacted.'
)
ERROR_USER_ABORT = 'Compaction canceled.'
MAX_COMPACT_FAILURES = 3
"""Circuit-breaker stop retrying auto-compact after this many consecutive
failures (mirrors the npm implementation)."""
# ---------------------------------------------------------------------------
# Prompt construction (npm ``src/services/compact/prompt.ts``)
# ---------------------------------------------------------------------------
_NO_TOOLS_PREAMBLE = """\
CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.
- Do NOT use Read, Bash, Grep, Glob, Edit, Write, or ANY other tool.
- You already have all the context you need in the conversation above.
- Tool calls will be REJECTED and will waste your only turn — you will fail the task.
- Your entire response must be plain text: an <analysis> block followed by a <summary> block.
"""
_DETAILED_ANALYSIS_INSTRUCTION = """\
Before providing your final summary, wrap your analysis in <analysis> tags to \
organize your thoughts and ensure you've covered all necessary points. In your \
analysis process:
1. Chronologically analyze each message and section of the conversation. \
For each section thoroughly identify:
- The user's explicit requests and intents
- Your approach to addressing the user's requests
- Key decisions, technical concepts and code patterns
- Specific details like:
- file names
- full code snippets
- function signatures
- file edits
- Errors that you ran into and how you fixed them
- Pay special attention to specific user feedback that you received, \
especially if the user told you to do something differently.
2. Double-check for technical accuracy and completeness, addressing each \
required element thoroughly."""
_BASE_COMPACT_PROMPT = f"""\
Your task is to create a detailed summary of the conversation so far, paying \
close attention to the user's explicit requests and your previous actions.
This summary should be thorough in capturing technical details, code patterns, \
and architectural decisions that would be essential for continuing development \
work without losing context.
{_DETAILED_ANALYSIS_INSTRUCTION}
Your summary should include the following sections:
1. Primary Request and Intent: Capture all of the user's explicit requests \
and intents in detail
2. Key Technical Concepts: List all important technical concepts, technologies, \
and frameworks discussed.
3. Files and Code Sections: Enumerate specific files and code sections examined, \
modified, or created. Pay special attention to the most recent messages and \
include full code snippets where applicable and include a summary of why this \
file read or edit is important.
4. Errors and fixes: List all errors that you ran into, and how you fixed them. \
Pay special attention to specific user feedback that you received, especially if \
the user told you to do something differently.
5. Problem Solving: Document problems solved and any ongoing troubleshooting \
efforts.
6. All user messages: List ALL user messages that are not tool results. These \
are critical for understanding the users' feedback and changing intent.
7. Pending Tasks: Outline any pending tasks that you have explicitly been asked \
to work on.
8. Current Work: Describe in detail precisely what was being worked on \
immediately before this summary request, paying special attention to the most \
recent messages from both user and assistant. Include file names and code \
snippets where applicable.
9. Optional Next Step: List the next step that you will take that is related to \
the most recent work you were doing. IMPORTANT: ensure that this step is \
DIRECTLY in line with the user's most recent explicit requests, and the task \
you were working on immediately before this summary request. If your last task \
was concluded, then only list next steps if they are explicitly in line with the \
users request. Do not start on tangential requests or really old requests that \
were already completed without confirming with the user first.
If there is a next step, include direct quotes from the \
most recent conversation showing exactly what task you were working on and where \
you left off. This should be verbatim to ensure there's no drift in task \
interpretation.
Here's an example of how your output should be structured:
<example>
<analysis>
[Your thought process, ensuring all points are covered thoroughly and accurately]
</analysis>
<summary>
1. Primary Request and Intent:
[Detailed description]
2. Key Technical Concepts:
- [Concept 1]
- [Concept 2]
- [...]
3. Files and Code Sections:
- [File Name 1]
- [Summary of why this file is important]
- [Summary of the changes made to this file, if any]
- [Important Code Snippet]
- [File Name 2]
- [Important Code Snippet]
- [...]
4. Errors and fixes:
- [Detailed description of error 1]:
- [How you fixed the error]
- [User feedback on the error if any]
- [...]
5. Problem Solving:
[Description of solved problems and ongoing troubleshooting]
6. All user messages:
- [Detailed non tool use user message]
- [...]
7. Pending Tasks:
- [Task 1]
- [Task 2]
- [...]
8. Current Work:
[Precise description of current work]
9. Optional Next Step:
[Optional Next step to take]
</summary>
</example>
Please provide your summary based on the conversation so far, following this \
structure and ensuring precision and thoroughness in your response.
There may be additional summarization instructions provided in the included \
context. If so, remember to follow these instructions when creating the above \
summary. Examples of instructions include:
<example>
## Compact Instructions
When summarizing the conversation focus on typescript code changes and also \
remember the mistakes you made and how you fixed them.
</example>
<example>
# Summary instructions
When you are using compact - please focus on test output and code changes. \
Include file reads verbatim.
</example>
"""
_NO_TOOLS_TRAILER = (
'\n\nREMINDER: Do NOT call any tools. Respond with plain text only — '
'an <analysis> block followed by a <summary> block. '
'Tool calls will be rejected and you will fail the task.'
)
def get_compact_prompt(custom_instructions: str | None = None) -> str:
"""Build the full compact prompt, optionally appending user instructions."""
prompt = _NO_TOOLS_PREAMBLE + _BASE_COMPACT_PROMPT
if custom_instructions and custom_instructions.strip():
prompt += f'\n\nAdditional Instructions:\n{custom_instructions}'
prompt += _NO_TOOLS_TRAILER
return prompt
# ---------------------------------------------------------------------------
# Summary formatting
# ---------------------------------------------------------------------------
def format_compact_summary(summary: str) -> str:
"""Strip the ``<analysis>`` scratchpad and unwrap ``<summary>`` tags.
Mirrors the npm ``formatCompactSummary`` helper.
"""
formatted = re.sub(r'<analysis>[\s\S]*?</analysis>', '', summary)
match = re.search(r'<summary>([\s\S]*?)</summary>', formatted)
if match:
content = match.group(1).strip()
formatted = re.sub(
r'<summary>[\s\S]*?</summary>',
f'Summary:\n{content}',
formatted,
)
# Collapse runs of blank lines.
formatted = re.sub(r'\n\n+', '\n\n', formatted)
return formatted.strip()
def get_compact_user_summary_message(
summary: str,
*,
suppress_follow_up: bool = False,
transcript_path: str | None = None,
) -> str:
"""Build the user-facing summary that replaces compacted messages.
Mirrors the npm ``getCompactUserSummaryMessage`` helper.
"""
formatted = format_compact_summary(summary)
base = (
'This session is being continued from a previous conversation that '
'ran out of context. The summary below covers the earlier portion '
f'of the conversation.\n\n{formatted}'
)
if transcript_path:
base += (
'\n\nIf you need specific details from before compaction '
'(like exact code snippets, error messages, or content you '
'generated), read the full transcript at: '
f'{transcript_path}'
)
if suppress_follow_up:
base += (
'\nContinue the conversation from where it left off without '
'asking the user any further questions. Resume directly — do '
'not acknowledge the summary, do not recap what was happening, '
'do not preface with "I\'ll continue" or similar. Pick up the '
'last task as if the break never happened.'
)
return base
# ---------------------------------------------------------------------------
# Compaction result
# ---------------------------------------------------------------------------
@dataclass
class CompactionResult:
"""Outcome of a ``compact_conversation`` call."""
boundary_message: AgentMessage
summary_messages: list[AgentMessage] = field(default_factory=list)
messages_to_keep: list[AgentMessage] = field(default_factory=list)
pre_compact_token_count: int = 0
post_compact_token_count: int = 0
summary_text: str = ''
error: str | None = None
# ---------------------------------------------------------------------------
# Core compaction logic
# ---------------------------------------------------------------------------
def compact_conversation(
agent: 'LocalCodingAgent',
custom_instructions: str | None = None,
) -> CompactionResult:
"""Perform an LLM-backed conversation compaction.
1. Build the compact prompt (9-section template).
2. Collect the session messages to summarise.
3. Send them + the compact prompt to the model.
4. Parse ``<summary>`` from the response.
5. Replace session messages with:
boundary marker → summary user message → preserved tail.
Returns a :class:`CompactionResult` with diagnostics.
"""
session = agent.last_session
if session is None or len(session.messages) == 0:
return CompactionResult(
boundary_message=_build_boundary('No session to compact.'),
error=ERROR_NOT_ENOUGH_MESSAGES,
)
# ---- Determine which messages to compact vs preserve ----
# We keep the most recent ``preserve_count`` messages untouched.
preserve_count = max(
getattr(agent.runtime_config, 'compact_preserve_messages', 4), 1
)
# Identify the prefix count (system-injected messages that precede the
# real conversation, e.g. a compaction-replay boundary).
prefix_count = 0
for msg in session.messages:
if msg.metadata.get('kind') == 'compact_boundary':
prefix_count += 1
else:
break
total = len(session.messages)
tail_count = min(preserve_count, max(total - prefix_count, 0))
compact_end = total - tail_count
if compact_end <= prefix_count:
return CompactionResult(
boundary_message=_build_boundary('Not enough messages after prefix.'),
error=ERROR_NOT_ENOUGH_MESSAGES,
)
candidates = session.messages[prefix_count:compact_end]
preserved_tail = list(session.messages[compact_end:])
if not candidates:
return CompactionResult(
boundary_message=_build_boundary('Nothing to compact.'),
error=ERROR_NOT_ENOUGH_MESSAGES,
)
# ---- Estimate pre-compact token count ----
model = agent.model_config.model
pre_tokens = sum(estimate_tokens(m.content, model) for m in session.messages)
# ---- Build the compact request messages ----
compact_prompt = get_compact_prompt(custom_instructions)
# We send the system prompt + candidate messages + the compact prompt as
# a user message. The model returns the summary.
api_messages: list[dict[str, Any]] = []
# System prompt (from session)
for part in session.system_prompt_parts:
if part.strip():
api_messages.append({'role': 'system', 'content': part})
# Candidate messages (the ones to be summarised)
for msg in candidates:
api_messages.append(msg.to_openai_message())
# The compact prompt as the final user turn
api_messages.append({'role': 'user', 'content': compact_prompt})
# ---- Call the model ----
try:
turn = agent.client.complete(api_messages, tools=[])
except Exception as exc:
return CompactionResult(
boundary_message=_build_boundary(f'Compact API call failed: {exc}'),
error=str(exc),
)
raw_summary = turn.content or ''
if not raw_summary.strip():
return CompactionResult(
boundary_message=_build_boundary('Model returned empty summary.'),
error=ERROR_INCOMPLETE_RESPONSE,
)
# ---- Format the summary ----
summary_text = format_compact_summary(raw_summary)
user_summary_content = get_compact_user_summary_message(raw_summary)
# ---- Build post-compact messages ----
boundary = _build_boundary(
f'Earlier conversation ({len(candidates)} messages, ~{pre_tokens} tokens) '
f'was compacted.',
)
summary_msg = AgentMessage(
role='user',
content=user_summary_content,
message_id='compact_summary',
metadata={'kind': 'compact_summary', 'is_compact_summary': True},
)
# Replace session messages in-place
session.messages = (
session.messages[:prefix_count]
+ [boundary, summary_msg]
+ preserved_tail
)
# ---- Post-compact token estimate ----
post_tokens = sum(estimate_tokens(m.content, model) for m in session.messages)
return CompactionResult(
boundary_message=boundary,
summary_messages=[summary_msg],
messages_to_keep=preserved_tail,
pre_compact_token_count=pre_tokens,
post_compact_token_count=post_tokens,
summary_text=summary_text,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _build_boundary(note: str) -> AgentMessage:
"""Create a compact-boundary system message."""
return AgentMessage(
role='user',
content=f'<system-reminder>\n{note}\n</system-reminder>',
message_id='compact_boundary',
metadata={'kind': 'compact_boundary'},
)
+709
View File
@@ -0,0 +1,709 @@
"""Prompt constants ported from npm src/constants/.
Covers: product metadata, API limits, tool limits, spinner verbs,
turn-completion verbs, figures/symbols, XML tags, message constants,
date utilities, system prompt section caching, output-style configs,
and cyber-risk instruction.
npm sources:
src/constants/product.ts
src/constants/apiLimits.ts
src/constants/toolLimits.ts
src/constants/spinnerVerbs.ts
src/constants/turnCompletionVerbs.ts
src/constants/figures.ts
src/constants/xml.ts
src/constants/messages.ts
src/constants/common.ts
src/constants/systemPromptSections.ts
src/constants/outputStyles.ts
src/constants/cyberRiskInstruction.ts
src/constants/system.ts
src/constants/prompts.ts (selected sections)
"""
from __future__ import annotations
import os
import platform
import threading
from dataclasses import dataclass, field
from datetime import date, datetime
from typing import Any, Callable
# ---------------------------------------------------------------------------
# Product metadata (product.ts)
# ---------------------------------------------------------------------------
PRODUCT_URL = "https://claude.com/claude-code"
CLAUDE_AI_BASE_URL = "https://claude.ai"
# ---------------------------------------------------------------------------
# System prompt prefixes (system.ts)
# ---------------------------------------------------------------------------
DEFAULT_SYSPROMPT_PREFIX = (
"You are Claude Code, Anthropic's official CLI for Claude."
)
AGENT_SDK_CLAUDE_CODE_PRESET_PREFIX = (
"You are Claude Code, Anthropic's official CLI for Claude, "
"running within the Claude Agent SDK."
)
AGENT_SDK_PREFIX = (
"You are a Claude agent, built on Anthropic's Claude Agent SDK."
)
CLI_SYSPROMPT_PREFIXES = frozenset(
{
DEFAULT_SYSPROMPT_PREFIX,
AGENT_SDK_CLAUDE_CODE_PRESET_PREFIX,
AGENT_SDK_PREFIX,
}
)
# ---------------------------------------------------------------------------
# Cyber-risk instruction (cyberRiskInstruction.ts)
# ---------------------------------------------------------------------------
CYBER_RISK_INSTRUCTION = (
"IMPORTANT: Assist with authorized security testing, defensive security, "
"CTF challenges, and educational contexts. Refuse requests for destructive "
"techniques, DoS attacks, mass targeting, supply chain compromise, or "
"detection evasion for malicious purposes. Dual-use security tools (C2 "
"frameworks, credential testing, exploit development) require clear "
"authorization context: pentesting engagements, CTF competitions, security "
"research, or defensive use cases."
)
# ---------------------------------------------------------------------------
# API limits (apiLimits.ts)
# ---------------------------------------------------------------------------
# Image limits
API_IMAGE_MAX_BASE64_SIZE = 5 * 1024 * 1024 # 5 MB base64
IMAGE_TARGET_RAW_SIZE = (API_IMAGE_MAX_BASE64_SIZE * 3) // 4 # ~3.75 MB
IMAGE_MAX_WIDTH = 2000
IMAGE_MAX_HEIGHT = 2000
# PDF limits
PDF_TARGET_RAW_SIZE = 20 * 1024 * 1024 # 20 MB
API_PDF_MAX_PAGES = 100
PDF_EXTRACT_SIZE_THRESHOLD = 3 * 1024 * 1024 # 3 MB
PDF_MAX_EXTRACT_SIZE = 100 * 1024 * 1024 # 100 MB
PDF_MAX_PAGES_PER_READ = 20
PDF_AT_MENTION_INLINE_THRESHOLD = 10
# Media limits
API_MAX_MEDIA_PER_REQUEST = 100
# ---------------------------------------------------------------------------
# Tool limits (toolLimits.ts)
# ---------------------------------------------------------------------------
DEFAULT_MAX_RESULT_SIZE_CHARS = 50_000
MAX_TOOL_RESULT_TOKENS = 100_000
BYTES_PER_TOKEN = 4
MAX_TOOL_RESULT_BYTES = MAX_TOOL_RESULT_TOKENS * BYTES_PER_TOKEN # 400 KB
MAX_TOOL_RESULTS_PER_MESSAGE_CHARS = 200_000
TOOL_SUMMARY_MAX_LENGTH = 50
# ---------------------------------------------------------------------------
# Spinner verbs (spinnerVerbs.ts) — 204 whimsical gerunds
# ---------------------------------------------------------------------------
SPINNER_VERBS: tuple[str, ...] = (
"Accomplishing",
"Actioning",
"Actualizing",
"Architecting",
"Baking",
"Beaming",
"Beboppin'",
"Befuddling",
"Billowing",
"Blanching",
"Bloviating",
"Boogieing",
"Boondoggling",
"Booping",
"Bootstrapping",
"Brewing",
"Bunning",
"Burrowing",
"Calculating",
"Canoodling",
"Caramelizing",
"Cascading",
"Catapulting",
"Cerebrating",
"Channeling",
"Channelling",
"Choreographing",
"Churning",
"Clauding",
"Coalescing",
"Cogitating",
"Combobulating",
"Composing",
"Computing",
"Concocting",
"Considering",
"Contemplating",
"Cooking",
"Crafting",
"Creating",
"Crunching",
"Crystallizing",
"Cultivating",
"Deciphering",
"Deliberating",
"Determining",
"Dilly-dallying",
"Discombobulating",
"Doing",
"Doodling",
"Drizzling",
"Ebbing",
"Effecting",
"Elucidating",
"Embellishing",
"Enchanting",
"Envisioning",
"Evaporating",
"Fermenting",
"Fiddle-faddling",
"Finagling",
"Flambéing",
"Flibbertigibbeting",
"Flowing",
"Flummoxing",
"Fluttering",
"Forging",
"Forming",
"Frolicking",
"Frosting",
"Gallivanting",
"Galloping",
"Garnishing",
"Generating",
"Gesticulating",
"Germinating",
"Gitifying",
"Grooving",
"Gusting",
"Harmonizing",
"Hashing",
"Hatching",
"Herding",
"Honking",
"Hullaballooing",
"Hyperspacing",
"Ideating",
"Imagining",
"Improvising",
"Incubating",
"Inferring",
"Infusing",
"Ionizing",
"Jitterbugging",
"Julienning",
"Kneading",
"Leavening",
"Levitating",
"Lollygagging",
"Manifesting",
"Marinating",
"Meandering",
"Metamorphosing",
"Misting",
"Moonwalking",
"Moseying",
"Mulling",
"Mustering",
"Musing",
"Nebulizing",
"Nesting",
"Newspapering",
"Noodling",
"Nucleating",
"Orbiting",
"Orchestrating",
"Osmosing",
"Perambulating",
"Percolating",
"Perusing",
"Philosophising",
"Photosynthesizing",
"Pollinating",
"Pondering",
"Pontificating",
"Pouncing",
"Precipitating",
"Prestidigitating",
"Processing",
"Proofing",
"Propagating",
"Puttering",
"Puzzling",
"Quantumizing",
"Razzle-dazzling",
"Razzmatazzing",
"Recombobulating",
"Reticulating",
"Roosting",
"Ruminating",
"Sautéing",
"Scampering",
"Schlepping",
"Scurrying",
"Seasoning",
"Shenaniganing",
"Shimmying",
"Simmering",
"Skedaddling",
"Sketching",
"Slithering",
"Smooshing",
"Sock-hopping",
"Spelunking",
"Spinning",
"Sprouting",
"Stewing",
"Sublimating",
"Swirling",
"Swooping",
"Symbioting",
"Synthesizing",
"Tempering",
"Thinking",
"Thundering",
"Tinkering",
"Tomfoolering",
"Topsy-turvying",
"Transfiguring",
"Transmuting",
"Twisting",
"Undulating",
"Unfurling",
"Unravelling",
"Vibing",
"Waddling",
"Wandering",
"Warping",
"Whatchamacalliting",
"Whirlpooling",
"Whirring",
"Whisking",
"Wibbling",
"Working",
"Wrangling",
"Zesting",
"Zigzagging",
)
# ---------------------------------------------------------------------------
# Turn-completion verbs (turnCompletionVerbs.ts) — 8 past-tense verbs
# ---------------------------------------------------------------------------
TURN_COMPLETION_VERBS: tuple[str, ...] = (
"Baked",
"Brewed",
"Churned",
"Cogitated",
"Cooked",
"Crunched",
"Sautéed",
"Worked",
)
# ---------------------------------------------------------------------------
# Figures / UI symbols (figures.ts)
# ---------------------------------------------------------------------------
BLACK_CIRCLE = "\u23fa" if platform.system() == "Darwin" else "\u25cf" # ⏺ / ●
BULLET_OPERATOR = "\u2219" # ∙
TEARDROP_ASTERISK = "\u273b" # ✻
UP_ARROW = "\u2191" # ↑
DOWN_ARROW = "\u2193" # ↓
LIGHTNING_BOLT = "\u21af" # ↯
EFFORT_LOW = "\u25cb" # ○
EFFORT_MEDIUM = "\u25d0" # ◐
EFFORT_HIGH = "\u25cf" # ●
EFFORT_MAX = "\u25c9" # ◉
PLAY_ICON = "\u25b6" # ▶
PAUSE_ICON = "\u23f8" # ⏸
REFRESH_ARROW = "\u21bb" # ↻
CHANNEL_ARROW = "\u2190" # ←
INJECTED_ARROW = "\u2192" # →
FORK_GLYPH = "\u2442" # ⑂
DIAMOND_OPEN = "\u25c7" # ◇
DIAMOND_FILLED = "\u25c6" # ◆
REFERENCE_MARK = "\u203b" # ※
FLAG_ICON = "\u2691" # ⚑
BLOCKQUOTE_BAR = "\u258e" # ▎
HEAVY_HORIZONTAL = "\u2501" # ━
BRIDGE_SPINNER_FRAMES: tuple[str, ...] = (
"\u00b7|\u00b7",
"\u00b7/\u00b7",
"\u00b7\u2014\u00b7",
"\u00b7\\\u00b7",
)
BRIDGE_READY_INDICATOR = "\u00b7\u2714\ufe0e\u00b7"
BRIDGE_FAILED_INDICATOR = "\u00d7"
# ---------------------------------------------------------------------------
# XML tag constants (xml.ts)
# ---------------------------------------------------------------------------
COMMAND_NAME_TAG = "command-name"
COMMAND_MESSAGE_TAG = "command-message"
COMMAND_ARGS_TAG = "command-args"
BASH_INPUT_TAG = "bash-input"
BASH_STDOUT_TAG = "bash-stdout"
BASH_STDERR_TAG = "bash-stderr"
LOCAL_COMMAND_STDOUT_TAG = "local-command-stdout"
LOCAL_COMMAND_STDERR_TAG = "local-command-stderr"
LOCAL_COMMAND_CAVEAT_TAG = "local-command-caveat"
TERMINAL_OUTPUT_TAGS: tuple[str, ...] = (
BASH_INPUT_TAG,
BASH_STDOUT_TAG,
BASH_STDERR_TAG,
LOCAL_COMMAND_STDOUT_TAG,
LOCAL_COMMAND_STDERR_TAG,
LOCAL_COMMAND_CAVEAT_TAG,
)
TICK_TAG = "tick"
TASK_NOTIFICATION_TAG = "task-notification"
TASK_ID_TAG = "task-id"
TOOL_USE_ID_TAG = "tool-use-id"
TASK_TYPE_TAG = "task-type"
OUTPUT_FILE_TAG = "output-file"
STATUS_TAG = "status"
SUMMARY_TAG = "summary"
REASON_TAG = "reason"
WORKTREE_TAG = "worktree"
WORKTREE_PATH_TAG = "worktreePath"
WORKTREE_BRANCH_TAG = "worktreeBranch"
ULTRAPLAN_TAG = "ultraplan"
REMOTE_REVIEW_TAG = "remote-review"
REMOTE_REVIEW_PROGRESS_TAG = "remote-review-progress"
TEAMMATE_MESSAGE_TAG = "teammate-message"
CHANNEL_MESSAGE_TAG = "channel-message"
CHANNEL_TAG = "channel"
CROSS_SESSION_MESSAGE_TAG = "cross-session-message"
FORK_BOILERPLATE_TAG = "fork-boilerplate"
FORK_DIRECTIVE_PREFIX = "Your directive: "
COMMON_HELP_ARGS: tuple[str, ...] = ("help", "-h", "--help")
COMMON_INFO_ARGS: tuple[str, ...] = (
"list",
"show",
"display",
"current",
"view",
"get",
"check",
"describe",
"print",
"version",
"about",
"status",
"?",
)
# ---------------------------------------------------------------------------
# Message constants (messages.ts)
# ---------------------------------------------------------------------------
NO_CONTENT_MESSAGE = "(no content)"
# ---------------------------------------------------------------------------
# Date utilities (common.ts)
# ---------------------------------------------------------------------------
def get_local_iso_date() -> str:
"""Return the local date in YYYY-MM-DD format.
Respects ``CLAUDE_CODE_OVERRIDE_DATE`` env var.
"""
override = os.environ.get("CLAUDE_CODE_OVERRIDE_DATE")
if override:
return override
return date.today().isoformat()
_session_start_date_lock = threading.Lock()
_session_start_date: str | None = None
def get_session_start_date() -> str:
"""Memoised local date — captured once per session."""
global _session_start_date
if _session_start_date is not None:
return _session_start_date
with _session_start_date_lock:
if _session_start_date is None:
_session_start_date = get_local_iso_date()
return _session_start_date
def reset_session_start_date() -> None:
"""Reset the memoised date (for tests)."""
global _session_start_date
_session_start_date = None
def get_local_month_year() -> str:
"""Return ``"Month YYYY"`` (e.g. ``"February 2026"``)."""
override = os.environ.get("CLAUDE_CODE_OVERRIDE_DATE")
if override:
d = datetime.fromisoformat(override)
else:
d = datetime.now()
return d.strftime("%B %Y")
# ---------------------------------------------------------------------------
# System prompt section caching (systemPromptSections.ts)
# ---------------------------------------------------------------------------
ComputeFn = Callable[[], str | None]
@dataclass
class SystemPromptSection:
"""A named section of the system prompt with lazy compute."""
name: str
compute: ComputeFn
cache_break: bool = False
def system_prompt_section(name: str, compute: ComputeFn) -> SystemPromptSection:
"""Create a memoised prompt section (cached until /clear or /compact)."""
return SystemPromptSection(name=name, compute=compute, cache_break=False)
def dangerous_uncached_system_prompt_section(
name: str,
compute: ComputeFn,
_reason: str = "",
) -> SystemPromptSection:
"""Prompt section that recomputes every turn (breaks prompt cache)."""
return SystemPromptSection(name=name, compute=compute, cache_break=True)
_section_cache: dict[str, str | None] = {}
def resolve_system_prompt_sections(
sections: list[SystemPromptSection],
) -> list[str | None]:
"""Resolve sections, caching non-volatile ones."""
results: list[str | None] = []
for section in sections:
if not section.cache_break and section.name in _section_cache:
results.append(_section_cache[section.name])
continue
value = section.compute()
_section_cache[section.name] = value
results.append(value)
return results
def clear_system_prompt_sections() -> None:
"""Clear cached prompt sections (called on /clear and /compact)."""
_section_cache.clear()
# ---------------------------------------------------------------------------
# Output style configuration (outputStyles.ts)
# ---------------------------------------------------------------------------
DEFAULT_OUTPUT_STYLE_NAME = "default"
@dataclass(frozen=True)
class OutputStyleConfig:
name: str
description: str
prompt: str
source: str = "built-in"
keep_coding_instructions: bool = True
force_for_plugin: bool = False
# Built-in output styles matching npm
OUTPUT_STYLE_CONFIGS: dict[str, OutputStyleConfig | None] = {
DEFAULT_OUTPUT_STYLE_NAME: None,
"Explanatory": OutputStyleConfig(
name="Explanatory",
description="Claude explains its implementation choices and codebase patterns",
prompt=(
"You are an interactive CLI tool that helps users with software "
"engineering tasks. In addition to software engineering tasks, you "
"should provide educational insights about the codebase along the way.\n\n"
"You should be clear and educational, providing helpful explanations "
"while remaining focused on the task. Balance educational content "
"with task completion."
),
),
"Learning": OutputStyleConfig(
name="Learning",
description="Claude pauses and asks you to write small pieces of code for hands-on practice",
prompt=(
"You are an interactive CLI tool that helps users with software "
"engineering tasks. In addition to software engineering tasks, you "
"should help users learn more about the codebase through hands-on "
"practice and educational insights.\n\n"
"You should be collaborative and encouraging. Balance task completion "
"with learning by requesting user input for meaningful design "
"decisions while handling routine implementation yourself."
),
),
}
# ---------------------------------------------------------------------------
# Knowledge cutoff (prompts.ts)
# ---------------------------------------------------------------------------
FRONTIER_MODEL_NAME = "Claude Opus 4.6"
_KNOWLEDGE_CUTOFFS: dict[str, str] = {
"claude-sonnet-4-6": "August 2025",
"claude-opus-4-6": "May 2025",
"claude-opus-4-5": "May 2025",
"claude-haiku-4": "February 2025",
"claude-opus-4": "January 2025",
"claude-sonnet-4": "January 2025",
}
def get_knowledge_cutoff(model_id: str) -> str | None:
"""Return knowledge cutoff date for a model, or None."""
canonical = model_id.lower()
for pattern, cutoff in _KNOWLEDGE_CUTOFFS.items():
if pattern in canonical:
return cutoff
return None
# ---------------------------------------------------------------------------
# Model family IDs (prompts.ts)
# ---------------------------------------------------------------------------
CLAUDE_MODEL_IDS = {
"opus": "claude-opus-4-6",
"sonnet": "claude-sonnet-4-6",
"haiku": "claude-haiku-4-5-20251001",
}
# ---------------------------------------------------------------------------
# Hooks section (prompts.ts)
# ---------------------------------------------------------------------------
HOOKS_SECTION = (
"Users may configure 'hooks', shell commands that execute in response to "
"events like tool calls, in settings. Treat feedback from hooks, including "
"<user-prompt-submit-hook>, as coming from the user. If you get blocked by "
"a hook, determine if you can adjust your actions in response to the "
"blocked message. If not, ask the user to check their hooks configuration."
)
# ---------------------------------------------------------------------------
# System reminders section (prompts.ts)
# ---------------------------------------------------------------------------
SYSTEM_REMINDERS_SECTION = (
"- Tool results and user messages may include <system-reminder> tags. "
"<system-reminder> tags contain useful information and reminders. They are "
"automatically added by the system, and bear no direct relation to the "
"specific tool results or user messages in which they appear.\n"
"- The conversation has unlimited context through automatic summarization."
)
# ---------------------------------------------------------------------------
# Summarize tool results (prompts.ts)
# ---------------------------------------------------------------------------
SUMMARIZE_TOOL_RESULTS_SECTION = (
"When working with tool results, write down any important information you "
"might need later in your response, as the original tool result may be "
"cleared later."
)
# ---------------------------------------------------------------------------
# Default agent prompt (prompts.ts)
# ---------------------------------------------------------------------------
DEFAULT_AGENT_PROMPT = (
"You are an agent for Claude Code, Anthropic's official CLI for Claude. "
"Given the user's message, you should use the tools available to complete "
"the task. Complete the task fully\u2014don't gold-plate, but don't leave "
"it half-done. When you complete the task, respond with a concise report "
"covering what was done and any key findings \u2014 the caller will relay "
"this to the user, so it only needs the essentials."
)
# ---------------------------------------------------------------------------
# Error IDs (errorIds.ts)
# ---------------------------------------------------------------------------
E_TOOL_USE_SUMMARY_GENERATION_FAILED = 344
# ---------------------------------------------------------------------------
# Dynamic boundary marker (prompts.ts)
# ---------------------------------------------------------------------------
SYSTEM_PROMPT_DYNAMIC_BOUNDARY = "__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__"
# ---------------------------------------------------------------------------
# Convenience helpers for use in prompt building
# ---------------------------------------------------------------------------
def get_language_section(language_preference: str | None) -> str | None:
"""Return the language preference prompt section, or None."""
if not language_preference:
return None
return (
f"# Language\n"
f"Always respond in {language_preference}. Use {language_preference} "
f"for all explanations, comments, and communications with the user. "
f"Technical terms and code identifiers should remain in their original form."
)
def get_output_style_section(config: OutputStyleConfig | None) -> str | None:
"""Return the output-style prompt section, or None."""
if config is None:
return None
return f"# Output Style: {config.name}\n{config.prompt}"
def get_scratchpad_instructions(scratchpad_dir: str | None) -> str | None:
"""Return scratchpad instructions, or None if no scratchpad is configured."""
if not scratchpad_dir:
return None
return (
f"# Scratchpad Directory\n\n"
f"IMPORTANT: Always use this scratchpad directory for temporary files "
f"instead of `/tmp` or other system temp directories:\n"
f"`{scratchpad_dir}`\n\n"
f"Use this directory for ALL temporary file needs:\n"
f"- Storing intermediate results or data during multi-step tasks\n"
f"- Writing temporary scripts or configuration files\n"
f"- Saving outputs that don't belong in the user's project\n"
f"- Creating working files during analysis or processing\n"
f"- Any file that would otherwise go to `/tmp`\n\n"
f"Only use `/tmp` if the user explicitly requests it.\n\n"
f"The scratchpad directory is session-specific, isolated from the "
f"user's project, and can be used freely without permission prompts."
)