mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-07 10:36:05 -10:00
feat(context): per-scope instruction budgets with visible truncation (#7086)
Thanks @7jrxt42BxFZo4iAnN4CX for the design and implementation of per-scope instruction budgets (#6526).
This commit is contained in:
36 files changed
+4433
-805
No files matched your search
@@ -48,6 +48,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
cancellation wiring stays in place; its stale unused-code suppressions are
|
cancellation wiring stays in place; its stale unused-code suppressions are
|
||||||
removed. No supported command or feature is retired by this cleanup.
|
removed. No supported command or feature is retired by this cleanup.
|
||||||
|
|
||||||
|
- Standing instructions are budgeted per scope (`global` and `project`) in
|
||||||
|
estimated tokens instead of one 48 KiB byte ceiling shared by everything, and
|
||||||
|
every trim, skip, and refusal is reported — as a system cell at session start,
|
||||||
|
on stderr in `exec`, to the model as a runtime-provenance message, and per
|
||||||
|
source in `/context report`. `AGENTS.md` still wins; a directory without one
|
||||||
|
now falls back to its `CLAUDE.md` (`claude_fallback = false` opts out).
|
||||||
|
Configured `instructions = [...]` entries are charged to the global scope.
|
||||||
|
New `[instruction_budget]` table (user config only) with per-scope token
|
||||||
|
budgets, percentages of the usable window, `file_max_bytes`, `policy`, the
|
||||||
|
static-prefix guard, and `claude_fallback` (#6526).
|
||||||
- Local Ollama defaults now come from chat-capable tags, and `/provider ollama`
|
- Local Ollama defaults now come from chat-capable tags, and `/provider ollama`
|
||||||
refreshes the live catalog before switching. Exec and Runtime API defaults use
|
refreshes the live catalog before switching. Exec and Runtime API defaults use
|
||||||
the same discovery path. Fresh context observations come from loaded models'
|
the same discovery path. Fresh context observations come from loaded models'
|
||||||
|
|||||||
+18
-5
@@ -168,11 +168,24 @@ sandbox_mode = "workspace-write" # read-only | workspace-write | danger-full-acc
|
|||||||
# "muse", or "all". Env: CODEWHALE_PROJECT_INSTRUCTION_IMPORTS (comma-separated).
|
# "muse", or "all". Env: CODEWHALE_PROJECT_INSTRUCTION_IMPORTS (comma-separated).
|
||||||
# project_instruction_imports = ["claude"]
|
# project_instruction_imports = ["claude"]
|
||||||
#
|
#
|
||||||
# Everything that reaches the model as standing project instruction authority —
|
# Standing instructions are budgeted per scope, in estimated tokens, and every
|
||||||
# the repository-root -> workspace AGENTS.md chain, the global fallback layer,
|
# trim or skip is reported at session start, to the model, and in /context.
|
||||||
# .codewhale/rules/*.md, and any imported foreign files — shares one 48 KiB
|
# `global` covers ~/.codewhale/AGENTS.md (or ~/.agents/AGENTS.md), the global
|
||||||
# aggregate budget. Instructions claim it first and are trimmed from the
|
# instructions.md fallbacks, ~/.claude/CLAUDE.md (last resort) and the
|
||||||
# broadest scope inward, so the nearest-scope file is the last thing dropped.
|
# `instructions = [...]` array. `project` covers the repository-root ->
|
||||||
|
# workspace AGENTS.md chain, co-read foreign files, and .codewhale/rules/*.md.
|
||||||
|
# A scope is capped at the smaller of its absolute token budget and its share
|
||||||
|
# of the usable window (context window minus the response ceiling and a 10%
|
||||||
|
# safety reserve). User config only: a cloned repository cannot change it.
|
||||||
|
# [instruction_budget]
|
||||||
|
# global_budget_tokens = 100000 # positive; absolute cap for the global scope
|
||||||
|
# project_budget_tokens = 100000 # positive; absolute cap for the project scope
|
||||||
|
# global_budget_percent = 25 # 1..=100, share of the usable window
|
||||||
|
# project_budget_percent = 25 # 1..=100, share of the usable window
|
||||||
|
# static_prefix_guard_percent = 80 # 1..=100; guard must leave >=10 points
|
||||||
|
# file_max_bytes = 8388608 # hard per-file ceiling; larger files skip
|
||||||
|
# policy = "trim" # trim | refuse
|
||||||
|
# claude_fallback = true # AGENTS.md wins; else CLAUDE.md
|
||||||
# prompt_suggestion = true # opt-in: show ghost-text follow-up question in composer after each turn
|
# prompt_suggestion = true # opt-in: show ghost-text follow-up question in composer after each turn
|
||||||
|
|
||||||
# Optional tab/window title shown as `[title] …` in front of the terminal
|
# Optional tab/window title shown as `[title] …` in front of the terminal
|
||||||
|
|||||||
@@ -48,6 +48,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
cancellation wiring stays in place; its stale unused-code suppressions are
|
cancellation wiring stays in place; its stale unused-code suppressions are
|
||||||
removed. No supported command or feature is retired by this cleanup.
|
removed. No supported command or feature is retired by this cleanup.
|
||||||
|
|
||||||
|
- Standing instructions are budgeted per scope (`global` and `project`) in
|
||||||
|
estimated tokens instead of one 48 KiB byte ceiling shared by everything, and
|
||||||
|
every trim, skip, and refusal is reported — as a system cell at session start,
|
||||||
|
on stderr in `exec`, to the model as a runtime-provenance message, and per
|
||||||
|
source in `/context report`. `AGENTS.md` still wins; a directory without one
|
||||||
|
now falls back to its `CLAUDE.md` (`claude_fallback = false` opts out).
|
||||||
|
Configured `instructions = [...]` entries are charged to the global scope.
|
||||||
|
New `[instruction_budget]` table (user config only) with per-scope token
|
||||||
|
budgets, percentages of the usable window, `file_max_bytes`, `policy`, the
|
||||||
|
static-prefix guard, and `claude_fallback` (#6526).
|
||||||
- Local Ollama defaults now come from chat-capable tags, and `/provider ollama`
|
- Local Ollama defaults now come from chat-capable tags, and `/provider ollama`
|
||||||
refreshes the live catalog before switching. Exec and Runtime API defaults use
|
refreshes the live catalog before switching. Exec and Runtime API defaults use
|
||||||
the same discovery path. Fresh context observations come from loaded models'
|
the same discovery path. Fresh context observations come from loaded models'
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ pub(crate) fn assert_fixture(name: &str, actual: &str) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The captured Linux environment contributes 21 tokens to the 2367 total.
|
/// The captured Linux environment contributes 21 tokens to the 2506 total.
|
||||||
/// Keep its text frozen independently of the production prompt renderer, but
|
/// Keep its text frozen independently of the production prompt renderer, but
|
||||||
/// substitute real host facts: Windows/pwsh.exe contributes 22, for example.
|
/// substitute real host facts: Windows/pwsh.exe contributes 22, for example.
|
||||||
/// Never derive expectations from the report being tested or erase its counts.
|
/// Never derive expectations from the report being tested or erase its counts.
|
||||||
@@ -130,7 +130,7 @@ fn context_fixture_for_host(expected: &str, platform: &str, shell: &str) -> Stri
|
|||||||
let environment =
|
let environment =
|
||||||
format!("## Environment\n\n- lang: en\n- platform: {platform}\n- shell: {shell}");
|
format!("## Environment\n\n- lang: en\n- platform: {platform}\n- shell: {shell}");
|
||||||
let tokens = environment.chars().count().div_ceil(3);
|
let tokens = environment.chars().count().div_ceil(3);
|
||||||
let total = 2367 - 21 + tokens;
|
let total = 2506 - 21 + tokens;
|
||||||
expected
|
expected
|
||||||
.replace(
|
.replace(
|
||||||
"EnvironmentBlock: Runtime environment [<workspace>] - 21 tokens",
|
"EnvironmentBlock: Runtime environment [<workspace>] - 21 tokens",
|
||||||
@@ -145,11 +145,11 @@ fn context_fixture_for_host(expected: &str, platform: &str, shell: &str) -> Stri
|
|||||||
&format!("\"estimated_tokens\": {tokens},"),
|
&format!("\"estimated_tokens\": {tokens},"),
|
||||||
)
|
)
|
||||||
.replace(
|
.replace(
|
||||||
"Source-entry total: 2367 tokens",
|
"Source-entry total: 2506 tokens",
|
||||||
&format!("Source-entry total: {total} tokens"),
|
&format!("Source-entry total: {total} tokens"),
|
||||||
)
|
)
|
||||||
.replace(
|
.replace(
|
||||||
"\"total_estimated_tokens\": 2367,",
|
"\"total_estimated_tokens\": 2506,",
|
||||||
&format!("\"total_estimated_tokens\": {total},"),
|
&format!("\"total_estimated_tokens\": {total},"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -290,13 +290,21 @@ fn context_expectations_preserve_counts_for_each_host() {
|
|||||||
.replace("- 21 tokens", "- 22 tokens")
|
.replace("- 21 tokens", "- 22 tokens")
|
||||||
.replace("environment (21)", "environment (22)")
|
.replace("environment (21)", "environment (22)")
|
||||||
.replace("\"estimated_tokens\": 21,", "\"estimated_tokens\": 22,")
|
.replace("\"estimated_tokens\": 21,", "\"estimated_tokens\": 22,")
|
||||||
.replace("2367", "2368");
|
.replace("2506", "2507");
|
||||||
let expected = context_fixture_for_host(&baseline, "windows", "pwsh.exe");
|
let expected = context_fixture_for_host(&baseline, "windows", "pwsh.exe");
|
||||||
assert_eq!(expected, windows, "{name}");
|
assert_eq!(expected, windows, "{name}");
|
||||||
assert_ne!(expected, baseline, "{name} must retain the host difference");
|
// A fixture that carries no host-dependent number (the top-source list
|
||||||
// A regression in another source or the environment count still fails.
|
// can stop naming the environment block) is identical on both hosts, so
|
||||||
|
// only the host-carrying fixtures can prove the difference is kept.
|
||||||
|
let host_dependent = baseline.contains("- 21 tokens")
|
||||||
|
|| baseline.contains("environment (21)")
|
||||||
|
|| baseline.contains("\"estimated_tokens\": 21,");
|
||||||
|
if host_dependent {
|
||||||
|
assert_ne!(expected, baseline, "{name} must retain the host difference");
|
||||||
|
assert_ne!(expected, windows.replace("22", "23"));
|
||||||
|
}
|
||||||
|
// A regression in another source still fails on every fixture.
|
||||||
assert_ne!(expected, windows.replace("1803", "1804"));
|
assert_ne!(expected, windows.replace("1803", "1804"));
|
||||||
assert_ne!(expected, windows.replace("22", "23"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,16 @@ message:
|
|||||||
"authority_tier": 5,
|
"authority_tier": 5,
|
||||||
"truncation_reason": null
|
"truncation_reason": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"source_kind": "project_context",
|
||||||
|
"label": "Instruction budget",
|
||||||
|
"source_path": null,
|
||||||
|
"activation_reason": "config_enabled",
|
||||||
|
"estimated_tokens": 0,
|
||||||
|
"counting_confidence": "high",
|
||||||
|
"authority_tier": 5,
|
||||||
|
"truncation_reason": "global 0 / 100000 estimated tokens; project 139 / 100000 estimated tokens; policy trim; usable window 516000 tokens; static-prefix guard 412800 tokens"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"source_kind": "project_context_pack",
|
"source_kind": "project_context_pack",
|
||||||
"label": "Project context pack",
|
"label": "Project context pack",
|
||||||
|
|||||||
@@ -31,6 +31,16 @@ message:
|
|||||||
"authority_tier": 5,
|
"authority_tier": 5,
|
||||||
"truncation_reason": null
|
"truncation_reason": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"source_kind": "project_context",
|
||||||
|
"label": "Instruction budget",
|
||||||
|
"source_path": null,
|
||||||
|
"activation_reason": "config_enabled",
|
||||||
|
"estimated_tokens": 0,
|
||||||
|
"counting_confidence": "high",
|
||||||
|
"authority_tier": 5,
|
||||||
|
"truncation_reason": "global 0 / 100000 estimated tokens; project 139 / 100000 estimated tokens; policy trim; usable window 516000 tokens; static-prefix guard 412800 tokens"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"source_kind": "project_context_pack",
|
"source_kind": "project_context_pack",
|
||||||
"label": "Project context pack",
|
"label": "Project context pack",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ Manage standing law: /constitution (status/preview), /constitution repo (repo-lo
|
|||||||
Sources:
|
Sources:
|
||||||
- Constitution: Bundled constitution, language policy, and output policy [bundled in this codewhale-tui build (BASE_PROMPT, compiled in)] - 1803 tokens (High, tier 1)
|
- Constitution: Bundled constitution, language policy, and output policy [bundled in this codewhale-tui build (BASE_PROMPT, compiled in)] - 1803 tokens (High, tier 1)
|
||||||
- ProjectContext: Project instructions - 160 tokens (High, tier 5)
|
- ProjectContext: Project instructions - 160 tokens (High, tier 5)
|
||||||
|
- ProjectContext: Instruction budget - 0 tokens (High, tier 5) - global 0 / 100000 estimated tokens; project 139 / 100000 estimated tokens; policy trim; usable window 516000 tokens; static-prefix guard 412800 tokens
|
||||||
- ProjectContextPack: Project context pack [<workspace>] - 0 tokens (High, tier 5) - disabled; project_map provides this information on demand
|
- ProjectContextPack: Project context pack [<workspace>] - 0 tokens (High, tier 5) - disabled; project_map provides this information on demand
|
||||||
- SkillsBlock: Available skills [<skills>] - 0 tokens (High, tier 5) - no skills discovered
|
- SkillsBlock: Available skills [<skills>] - 0 tokens (High, tier 5) - no skills discovered
|
||||||
- ContextManagement: WORK runtime mode - 0 tokens (High, tier 3) - mode enforced by runtime policy and the live tool catalog; no prompt doctrine
|
- ContextManagement: WORK runtime mode - 0 tokens (High, tier 3) - mode enforced by runtime policy and the live tool catalog; no prompt doctrine
|
||||||
|
|||||||
@@ -2093,6 +2093,13 @@ pub struct Config {
|
|||||||
/// `cline`, `windsurf`, `gemini`, `copilot`, `muse`, or `all`.
|
/// `cline`, `windsurf`, `gemini`, `copilot`, `muse`, or `all`.
|
||||||
#[serde(default, alias = "projectInstructionImports")]
|
#[serde(default, alias = "projectInstructionImports")]
|
||||||
pub project_instruction_imports: Vec<String>,
|
pub project_instruction_imports: Vec<String>,
|
||||||
|
/// Per-scope standing-instruction budgets (#6526).
|
||||||
|
///
|
||||||
|
/// User config only, like `instructions`: the project-scope overlay
|
||||||
|
/// cannot set it, so a cloned repository cannot shrink the user's global
|
||||||
|
/// budget or force `policy = "refuse"`.
|
||||||
|
#[serde(default, alias = "instructionBudget")]
|
||||||
|
pub instruction_budget: Option<crate::project_context::InstructionBudgetConfig>,
|
||||||
/// `telemetry` as written to the config file, before environment and
|
/// `telemetry` as written to the config file, before environment and
|
||||||
/// default resolution. Kept so doctor and config displays can state the
|
/// default resolution. Kept so doctor and config displays can state the
|
||||||
/// *resolved* consent with its source (default | env | config) instead of
|
/// *resolved* consent with its source (default | env | config) instead of
|
||||||
@@ -10087,6 +10094,10 @@ fn merge_config(base: Config, override_cfg: Config) -> Config {
|
|||||||
// replace the instruction array. Project-scope config is filtered in
|
// replace the instruction array. Project-scope config is filtered in
|
||||||
// main.rs and cannot set instruction paths.
|
// main.rs and cannot set instruction paths.
|
||||||
instructions: override_cfg.instructions.or(base.instructions),
|
instructions: override_cfg.instructions.or(base.instructions),
|
||||||
|
// #6526: the instruction-budget table is user authority for the same
|
||||||
|
// reason `instructions` is — a workspace does not get to decide how
|
||||||
|
// much of the user's context its own files may claim.
|
||||||
|
instruction_budget: override_cfg.instruction_budget.or(base.instruction_budget),
|
||||||
stop_words: override_cfg.stop_words.or(base.stop_words),
|
stop_words: override_cfg.stop_words.or(base.stop_words),
|
||||||
allow_shell: override_cfg.allow_shell.or(base.allow_shell),
|
allow_shell: override_cfg.allow_shell.or(base.allow_shell),
|
||||||
prompt_suggestion: override_cfg.prompt_suggestion.or(base.prompt_suggestion),
|
prompt_suggestion: override_cfg.prompt_suggestion.or(base.prompt_suggestion),
|
||||||
|
|||||||
@@ -282,6 +282,7 @@ pub fn build_context_report(app: &App) -> PromptSourceMap {
|
|||||||
app.mode,
|
app.mode,
|
||||||
Some(app.plugin_registry.as_ref()),
|
Some(app.plugin_registry.as_ref()),
|
||||||
Some(context_window.tokens),
|
Some(context_window.tokens),
|
||||||
|
&[],
|
||||||
);
|
);
|
||||||
add_app_runtime_entries(&mut builder, app);
|
add_app_runtime_entries(&mut builder, app);
|
||||||
builder.finish(
|
builder.finish(
|
||||||
@@ -395,6 +396,7 @@ pub fn build_headless_context_report(config: &Config, workspace: &Path) -> Promp
|
|||||||
AppMode::Agent,
|
AppMode::Agent,
|
||||||
None,
|
None,
|
||||||
Some(context_window.tokens),
|
Some(context_window.tokens),
|
||||||
|
&config.instructions_paths(),
|
||||||
);
|
);
|
||||||
let memory_path = config.memory_path();
|
let memory_path = config.memory_path();
|
||||||
let memory_enabled = config.memory_enabled();
|
let memory_enabled = config.memory_enabled();
|
||||||
@@ -463,6 +465,7 @@ fn base_source_entries(
|
|||||||
mode: AppMode,
|
mode: AppMode,
|
||||||
plugin_registry: Option<&crate::plugins::PluginRegistry>,
|
plugin_registry: Option<&crate::plugins::PluginRegistry>,
|
||||||
context_window_tokens: Option<u32>,
|
context_window_tokens: Option<u32>,
|
||||||
|
configured_instruction_paths: &[std::path::PathBuf],
|
||||||
) -> ReportBuilder {
|
) -> ReportBuilder {
|
||||||
let mut builder = ReportBuilder::new();
|
let mut builder = ReportBuilder::new();
|
||||||
|
|
||||||
@@ -507,18 +510,48 @@ fn base_source_entries(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(content) = project_context.instructions.as_deref() {
|
// #6526: assemble once, so the source row counts the content the prompt
|
||||||
// Same helper as ProjectContext::as_system_block, so the report's
|
// keeps, and the budget/loss rows below carry no token cost of their own.
|
||||||
// source token derives from the same label the prompt shows. (The
|
// Counting the raw text here and the retained text again there would
|
||||||
// entry below still displays the absolute source path for operators;
|
// include discarded content and inflate the total even without a trim.
|
||||||
// only the prompt label is relativized.)
|
let instruction_assembly = crate::project_context::active_instruction_budgets(
|
||||||
|
context_window_tokens.map(|tokens| tokens as usize),
|
||||||
|
Some(model),
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
.map(|budgets| {
|
||||||
|
let mut instructions = crate::project_context::load_project_context_with_parents(workspace);
|
||||||
|
let configured: Vec<crate::prompts::InstructionSource> = configured_instruction_paths
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(Into::into)
|
||||||
|
.collect();
|
||||||
|
crate::project_context::assemble_instruction_block(&mut instructions, &configured, &budgets)
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(block) = instruction_assembly
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|assembly| assembly.block.as_deref())
|
||||||
|
{
|
||||||
|
builder.push(SourceEntry::text(
|
||||||
|
SourceKind::ProjectContext,
|
||||||
|
"Project instructions",
|
||||||
|
project_context
|
||||||
|
.source_path
|
||||||
|
.as_ref()
|
||||||
|
.map(|path| path.display().to_string()),
|
||||||
|
ActivationReason::FilePresent,
|
||||||
|
block,
|
||||||
|
CountingConfidence::High,
|
||||||
|
Some(5),
|
||||||
|
));
|
||||||
|
} else if let Some(content) = project_context.instructions.as_deref() {
|
||||||
let source = crate::project_context::project_instructions_source_label(
|
let source = crate::project_context::project_instructions_source_label(
|
||||||
project_context.source_path.as_deref(),
|
project_context.source_path.as_deref(),
|
||||||
);
|
);
|
||||||
let mut block = format!(
|
let mut block = format!(
|
||||||
"<project_instructions source=\"{source}\">\n{content}\n</project_instructions>"
|
"<project_instructions source=\"{source}\">\n{content}\n</project_instructions>"
|
||||||
);
|
);
|
||||||
// Include rules in the report when present
|
|
||||||
if let Some(rules) = &project_context.rules_block {
|
if let Some(rules) = &project_context.rules_block {
|
||||||
block.push('\n');
|
block.push('\n');
|
||||||
block.push_str(rules);
|
block.push_str(rules);
|
||||||
@@ -536,7 +569,6 @@ fn base_source_entries(
|
|||||||
Some(5),
|
Some(5),
|
||||||
));
|
));
|
||||||
} else if let Some(rules) = &project_context.rules_block {
|
} else if let Some(rules) = &project_context.rules_block {
|
||||||
// Rules exist without main instructions
|
|
||||||
builder.push(SourceEntry::text(
|
builder.push(SourceEntry::text(
|
||||||
SourceKind::ProjectContext,
|
SourceKind::ProjectContext,
|
||||||
"Project rules",
|
"Project rules",
|
||||||
@@ -557,6 +589,39 @@ fn base_source_entries(
|
|||||||
"no project context block available",
|
"no project context block available",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(assembly) = instruction_assembly.as_ref() {
|
||||||
|
builder.push(SourceEntry::diagnostic(
|
||||||
|
SourceKind::ProjectContext,
|
||||||
|
"Instruction budget",
|
||||||
|
None::<String>,
|
||||||
|
ActivationReason::ConfigEnabled,
|
||||||
|
format!(
|
||||||
|
"global {} / {} estimated tokens; project {} / {} estimated tokens; policy {}; usable window {} tokens; static-prefix guard {} tokens",
|
||||||
|
assembly.global_tokens,
|
||||||
|
assembly.budgets.global_tokens,
|
||||||
|
assembly.project_tokens,
|
||||||
|
assembly.budgets.project_tokens,
|
||||||
|
assembly.budgets.policy.as_str(),
|
||||||
|
assembly.budgets.usable_window_tokens,
|
||||||
|
assembly.budgets.static_guard_tokens,
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
Some(5),
|
||||||
|
));
|
||||||
|
for notice in assembly.notices.iter().filter(|notice| notice.is_loss()) {
|
||||||
|
builder.push(SourceEntry::diagnostic(
|
||||||
|
SourceKind::ProjectContextWarning,
|
||||||
|
format!("Instruction source: {}", notice.label()),
|
||||||
|
notice.path.as_ref().map(|path| path.display().to_string()),
|
||||||
|
ActivationReason::RuntimeState,
|
||||||
|
notice.to_string(),
|
||||||
|
0,
|
||||||
|
Some(5),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !project_context.warnings.is_empty() {
|
if !project_context.warnings.is_empty() {
|
||||||
let warnings = project_context.warnings.join("\n");
|
let warnings = project_context.warnings.join("\n");
|
||||||
let estimated_tokens = estimate_text_tokens_conservative(&warnings);
|
let estimated_tokens = estimate_text_tokens_conservative(&warnings);
|
||||||
@@ -1017,6 +1082,40 @@ mod tests {
|
|||||||
assert!(json.contains("branch_policy appears stale"));
|
assert!(json.contains("branch_policy appears stale"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn context_report_shows_actions_and_budgets() {
|
||||||
|
let tmp = tempdir().expect("workspace");
|
||||||
|
std::fs::write(tmp.path().join("AGENTS.md"), "T".repeat(20_000)).expect("write agents");
|
||||||
|
crate::project_context::with_test_instruction_budget_config(
|
||||||
|
crate::project_context::InstructionBudgetConfig {
|
||||||
|
project_budget_tokens: Some(50),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
|| {
|
||||||
|
let report = build_headless_context_report(&Config::default(), tmp.path());
|
||||||
|
assert!(
|
||||||
|
report
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.any(|entry| entry.label == "Instruction budget"),
|
||||||
|
"the resolved per-scope budgets are a source-map row: {:?}",
|
||||||
|
report.entries
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
report.entries.iter().any(|entry| {
|
||||||
|
entry.label.starts_with("Instruction source:")
|
||||||
|
&& entry
|
||||||
|
.truncation_reason
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|reason| reason.contains("scope_budget"))
|
||||||
|
}),
|
||||||
|
"each loss names its action and limit kind: {:?}",
|
||||||
|
report.entries
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn headless_context_report_uses_kimi_code_k3_route_context() {
|
fn headless_context_report_uses_kimi_code_k3_route_context() {
|
||||||
let tmp = tempdir().expect("workspace");
|
let tmp = tempdir().expect("workspace");
|
||||||
|
|||||||
+233
-37
@@ -1017,6 +1017,16 @@ pub struct Engine {
|
|||||||
codewhale_client_error: Option<String>,
|
codewhale_client_error: Option<String>,
|
||||||
api_key_env_only_recovery: Option<String>,
|
api_key_env_only_recovery: Option<String>,
|
||||||
session: Session,
|
session: Session,
|
||||||
|
/// One-shot instruction-budget notice for the model (#6526), delivered as
|
||||||
|
/// a synthetic user message on the first turn and then cleared.
|
||||||
|
pending_instruction_notice: Option<String>,
|
||||||
|
/// Whether the static-prefix guard has already run for this session. The
|
||||||
|
/// guard is a safety net with a one-shot notice, not a per-turn meter.
|
||||||
|
static_prefix_guard_checked: bool,
|
||||||
|
/// The ceiling the guard settled on, kept so every later composition of
|
||||||
|
/// this session honours it: without it a plain turn would recompose with
|
||||||
|
/// the configured budget and put the trimmed files back.
|
||||||
|
active_guard_trim: Option<crate::project_context::InstructionGuardTrim>,
|
||||||
/// One lazy, session-scoped working kernel for inline `repl` blocks.
|
/// One lazy, session-scoped working kernel for inline `repl` blocks.
|
||||||
/// Its context is refreshed before each run, while user-created Python
|
/// Its context is refreshed before each run, while user-created Python
|
||||||
/// state stays alive across model turns.
|
/// state stays alive across model turns.
|
||||||
@@ -2260,12 +2270,15 @@ impl Engine {
|
|||||||
} else {
|
} else {
|
||||||
prompts::PromptHost::Headless
|
prompts::PromptHost::Headless
|
||||||
};
|
};
|
||||||
let system_prompt = if let Some(host) = host.as_ref() {
|
let (system_prompt, instruction_assembly) = if let Some(host) = host.as_ref() {
|
||||||
host.system_prompt().clone()
|
(host.system_prompt().clone(), None)
|
||||||
} else if api_config.runtime_chat_isolated {
|
} else if api_config.runtime_chat_isolated {
|
||||||
SystemPrompt::Text(ISOLATED_CHAT_SYSTEM_PROMPT.to_string())
|
(
|
||||||
|
SystemPrompt::Text(ISOLATED_CHAT_SYSTEM_PROMPT.to_string()),
|
||||||
|
None,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
prompts::system_prompt_for_mode_with_context_skills_session_and_approval_for_host(
|
let (prompt, assembly) = prompts::system_prompt_with_instruction_assembly_for_host(
|
||||||
&config.workspace,
|
&config.workspace,
|
||||||
None,
|
None,
|
||||||
Some(&config.skills_dir),
|
Some(&config.skills_dir),
|
||||||
@@ -2293,8 +2306,13 @@ impl Engine {
|
|||||||
mode: AppMode::Agent,
|
mode: AppMode::Agent,
|
||||||
},
|
},
|
||||||
prompt_host,
|
prompt_host,
|
||||||
)
|
None,
|
||||||
|
);
|
||||||
|
(prompt, Some(assembly))
|
||||||
};
|
};
|
||||||
|
let pending_instruction_notice = instruction_assembly
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|assembly| crate::project_context::render_notice_body(&assembly.notices));
|
||||||
let stable_prompt = Some(system_prompt);
|
let stable_prompt = Some(system_prompt);
|
||||||
session.last_system_prompt_hash = Some(system_prompt_hash(stable_prompt.as_ref()));
|
session.last_system_prompt_hash = Some(system_prompt_hash(stable_prompt.as_ref()));
|
||||||
session.system_prompt = stable_prompt;
|
session.system_prompt = stable_prompt;
|
||||||
@@ -2472,6 +2490,9 @@ impl Engine {
|
|||||||
codewhale_client_error,
|
codewhale_client_error,
|
||||||
api_key_env_only_recovery,
|
api_key_env_only_recovery,
|
||||||
session,
|
session,
|
||||||
|
pending_instruction_notice,
|
||||||
|
static_prefix_guard_checked: false,
|
||||||
|
active_guard_trim: None,
|
||||||
repl_kernel: None,
|
repl_kernel: None,
|
||||||
subagent_manager,
|
subagent_manager,
|
||||||
subagent_execution_limits,
|
subagent_execution_limits,
|
||||||
@@ -4193,6 +4214,11 @@ impl Engine {
|
|||||||
workspace,
|
workspace,
|
||||||
mode,
|
mode,
|
||||||
} => {
|
} => {
|
||||||
|
// A fresh conversation arrives with its own
|
||||||
|
// sources: re-arm the guard and rebuild the notice
|
||||||
|
// from the assembly that installs its header.
|
||||||
|
self.static_prefix_guard_checked = false;
|
||||||
|
self.active_guard_trim = None;
|
||||||
let plugin_workspace_changed =
|
let plugin_workspace_changed =
|
||||||
self.plugin_registry.workspace() != workspace.as_path();
|
self.plugin_registry.workspace() != workspace.as_path();
|
||||||
let previous_session_id = self.session.id.clone();
|
let previous_session_id = self.session.id.clone();
|
||||||
@@ -9122,7 +9148,17 @@ impl Engine {
|
|||||||
reason: &str,
|
reason: &str,
|
||||||
) {
|
) {
|
||||||
self.record_project_trust_warning();
|
self.record_project_trust_warning();
|
||||||
let stable_prompt = self.compose_stable_system_prompt(context);
|
let (stable_prompt, assembly) =
|
||||||
|
self.compose_stable_system_prompt_with_assembly(context, self.active_guard_trim);
|
||||||
|
// The assembly that installs the header is what the operator and the
|
||||||
|
// model must hear about: a route change can drop files that the
|
||||||
|
// construction-time assembly did not.
|
||||||
|
if let Some(body) = assembly
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|assembly| crate::project_context::render_notice_body(&assembly.notices))
|
||||||
|
{
|
||||||
|
self.pending_instruction_notice = Some(body);
|
||||||
|
}
|
||||||
|
|
||||||
let stable_hash = system_prompt_hash(stable_prompt.as_ref());
|
let stable_hash = system_prompt_hash(stable_prompt.as_ref());
|
||||||
if self.session.system_prompt_override {
|
if self.session.system_prompt_override {
|
||||||
@@ -9208,17 +9244,38 @@ impl Engine {
|
|||||||
&self,
|
&self,
|
||||||
context: &NextTurnPromptContext,
|
context: &NextTurnPromptContext,
|
||||||
) -> Option<SystemPrompt> {
|
) -> Option<SystemPrompt> {
|
||||||
|
self.compose_stable_system_prompt_with_assembly(context, self.active_guard_trim)
|
||||||
|
.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compose the stable system prompt together with the instruction assembly
|
||||||
|
/// that produced it, optionally under a static-prefix guard trim.
|
||||||
|
///
|
||||||
|
/// The assembly carries the budgets and every trim/skip notice, so the
|
||||||
|
/// guard can trim the largest scope and the caller can tell the user and
|
||||||
|
/// the model what was lost.
|
||||||
|
pub(super) fn compose_stable_system_prompt_with_assembly(
|
||||||
|
&self,
|
||||||
|
context: &NextTurnPromptContext,
|
||||||
|
guard_trim: Option<crate::project_context::InstructionGuardTrim>,
|
||||||
|
) -> (
|
||||||
|
Option<SystemPrompt>,
|
||||||
|
Option<crate::project_context::InstructionAssembly>,
|
||||||
|
) {
|
||||||
if let Some(rlm) = self.rlm_host.as_ref() {
|
if let Some(rlm) = self.rlm_host.as_ref() {
|
||||||
return Some(rlm.system_prompt.clone());
|
return (Some(rlm.system_prompt.clone()), None);
|
||||||
}
|
}
|
||||||
if let Some(child) = self.child_host.as_ref() {
|
if let Some(child) = self.child_host.as_ref() {
|
||||||
// The configured role is input to this same composer, and is
|
// The configured role is input to this same composer, and is
|
||||||
// retained across route/mode/header refreshes. Volatile Native
|
// retained across route/mode/header refreshes. Volatile Native
|
||||||
// contributions and work facts still append to Core Session.
|
// contributions and work facts still append to Core Session.
|
||||||
return Some(child.system_prompt.clone());
|
return (Some(child.system_prompt.clone()), None);
|
||||||
}
|
}
|
||||||
if self.api_config.runtime_chat_isolated {
|
if self.api_config.runtime_chat_isolated {
|
||||||
return Some(SystemPrompt::Text(ISOLATED_CHAT_SYSTEM_PROMPT.to_string()));
|
return (
|
||||||
|
Some(SystemPrompt::Text(ISOLATED_CHAT_SYSTEM_PROMPT.to_string())),
|
||||||
|
None,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let user_memory_block = codewhale_runtime::native_memory::native_prompt_block_traced(
|
let user_memory_block = codewhale_runtime::native_memory::native_prompt_block_traced(
|
||||||
self.config.memory_enabled,
|
self.config.memory_enabled,
|
||||||
@@ -9237,35 +9294,174 @@ impl Engine {
|
|||||||
&self.config.workspace,
|
&self.config.workspace,
|
||||||
Some(self.session.id.as_str()),
|
Some(self.session.id.as_str()),
|
||||||
);
|
);
|
||||||
let base =
|
let (base, assembly) = prompts::system_prompt_with_instruction_assembly_for_host(
|
||||||
prompts::system_prompt_for_mode_with_context_skills_session_and_approval_for_host(
|
&self.config.workspace,
|
||||||
&self.config.workspace,
|
None,
|
||||||
None,
|
Some(&self.config.skills_dir),
|
||||||
Some(&self.config.skills_dir),
|
Some(&self.config.instructions),
|
||||||
Some(&self.config.instructions),
|
prompts::PromptSessionContext {
|
||||||
prompts::PromptSessionContext {
|
user_memory_block: user_memory_block.as_deref(),
|
||||||
user_memory_block: user_memory_block.as_deref(),
|
goal_objective: context.goal_objective.as_deref(),
|
||||||
goal_objective: context.goal_objective.as_deref(),
|
project_context_pack_enabled: self.config.project_context_pack_enabled,
|
||||||
project_context_pack_enabled: self.config.project_context_pack_enabled,
|
locale_tag: &self.config.locale_tag,
|
||||||
locale_tag: &self.config.locale_tag,
|
translation_enabled: context.translation_enabled,
|
||||||
translation_enabled: context.translation_enabled,
|
model_id: &context.model,
|
||||||
model_id: &context.model,
|
context_window_override: Some(crate::route_budget::route_context_window_tokens(
|
||||||
context_window_override: Some(
|
context.provider,
|
||||||
crate::route_budget::route_context_window_tokens(
|
&context.model,
|
||||||
context.provider,
|
context.route_limits,
|
||||||
&context.model,
|
)),
|
||||||
context.route_limits,
|
verbosity: context.verbosity.as_deref(),
|
||||||
),
|
recovery_hint: recovery_hint.as_deref(),
|
||||||
),
|
skills_discovery_mode: self.config.skills_discovery_mode,
|
||||||
verbosity: context.verbosity.as_deref(),
|
plugin_registry: Some(self.plugin_registry.as_ref()),
|
||||||
recovery_hint: recovery_hint.as_deref(),
|
mode: context.mode,
|
||||||
skills_discovery_mode: self.config.skills_discovery_mode,
|
},
|
||||||
plugin_registry: Some(self.plugin_registry.as_ref()),
|
prompt_host,
|
||||||
mode: context.mode,
|
guard_trim,
|
||||||
},
|
);
|
||||||
prompt_host,
|
(Some(base), Some(assembly))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deliver the one-shot instruction-budget notice and, once per session,
|
||||||
|
/// enforce the static-prefix guard (#6526).
|
||||||
|
///
|
||||||
|
/// The notice reaches the model as a synthetic user message with runtime
|
||||||
|
/// provenance — the same mechanism LSP diagnostics use — so it is part of
|
||||||
|
/// the logged transcript rather than an invisible prompt edit.
|
||||||
|
pub(super) async fn apply_instruction_budget_notices(
|
||||||
|
&mut self,
|
||||||
|
tools: &[codewhale_models::Tool],
|
||||||
|
) {
|
||||||
|
if let Some(body) = self.pending_instruction_notice.take() {
|
||||||
|
self.add_session_message(self.runtime_text_message_with_turn_metadata(
|
||||||
|
body,
|
||||||
|
crate::core::ops::UserInputProvenance::Runtime,
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
if self.static_prefix_guard_checked {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(body) = self.enforce_static_prefix_guard(tools).await {
|
||||||
|
self.add_session_message(self.runtime_text_message_with_turn_metadata(
|
||||||
|
body,
|
||||||
|
crate::core::ops::UserInputProvenance::Runtime,
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Keep the stable prefix — base prompt, instructions, skills index and
|
||||||
|
/// tool schemas — under `static_prefix_guard_percent` of the usable
|
||||||
|
/// window.
|
||||||
|
///
|
||||||
|
/// Trims the largest instruction scope by the overage, recomposes once,
|
||||||
|
/// re-measures once. No loop: when every scope is already zero and the
|
||||||
|
/// prefix is still over, the session continues under `policy = "trim"`
|
||||||
|
/// with an explicit `static_guard_unsatisfied` notice (`policy = "refuse"`
|
||||||
|
/// is enforced at session start, before the engine exists).
|
||||||
|
async fn enforce_static_prefix_guard(
|
||||||
|
&mut self,
|
||||||
|
tools: &[codewhale_models::Tool],
|
||||||
|
) -> Option<String> {
|
||||||
|
if self.session.system_prompt_override {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let context = self.session.pinned_prompt_context.clone()?;
|
||||||
|
let window = crate::route_budget::route_context_window_tokens(
|
||||||
|
self.api_provider,
|
||||||
|
&self.session.model,
|
||||||
|
self.active_route_limits,
|
||||||
|
) as usize;
|
||||||
|
let budgets = crate::project_context::active_instruction_budgets(
|
||||||
|
Some(window),
|
||||||
|
Some(&self.session.model),
|
||||||
|
)
|
||||||
|
.unwrap_or_else(|_| crate::project_context::InstructionBudgets::defaults_for(Some(window)));
|
||||||
|
let measured = prompts::measure_static_prefix(self.session.system_prompt.as_ref(), tools);
|
||||||
|
if measured <= budgets.static_guard_tokens {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let over = measured - budgets.static_guard_tokens;
|
||||||
|
|
||||||
|
// The assembly reads directories and configured files: run it on a
|
||||||
|
// blocking worker so a slow disk cannot stall the engine's runtime
|
||||||
|
// thread before a provider request.
|
||||||
|
let workspace = self.config.workspace.clone();
|
||||||
|
let configured = self.config.instructions.clone();
|
||||||
|
let assembly = tokio::task::spawn_blocking(move || {
|
||||||
|
crate::project_context::instruction_assembly_for(&workspace, &configured, &budgets)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
let scope = assembly
|
||||||
|
.largest_scope
|
||||||
|
.unwrap_or(crate::project_context::InstructionScope::Global);
|
||||||
|
let retained = match scope {
|
||||||
|
crate::project_context::InstructionScope::Global => assembly.global_tokens,
|
||||||
|
crate::project_context::InstructionScope::Project => assembly.project_tokens,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Nothing left to trim: the guard is unsatisfiable. Continue under
|
||||||
|
// `policy = "trim"` with an explicit terminal notice — `refuse` is
|
||||||
|
// enforced at session start, before the engine exists.
|
||||||
|
if retained <= over {
|
||||||
|
self.session.pending_prefix_change_reason =
|
||||||
|
Some("instruction-budget:static-guard-unsatisfiable".to_string());
|
||||||
|
self.active_guard_trim =
|
||||||
|
Some(crate::project_context::InstructionGuardTrim { scope, tokens: 0 });
|
||||||
|
self.static_prefix_guard_checked = true;
|
||||||
|
return Some(
|
||||||
|
crate::project_context::InstructionNotice::static_guard(scope, over, true)
|
||||||
|
.to_string(),
|
||||||
);
|
);
|
||||||
Some(base)
|
}
|
||||||
|
|
||||||
|
// Trim by the overage from what the scope actually kept, so the
|
||||||
|
// recomposed prefix really loses that many tokens.
|
||||||
|
let trim = crate::project_context::InstructionGuardTrim {
|
||||||
|
scope,
|
||||||
|
tokens: retained - over,
|
||||||
|
};
|
||||||
|
let (prompt, trimmed_assembly) =
|
||||||
|
self.compose_stable_system_prompt_with_assembly(&context, Some(trim));
|
||||||
|
let prompt = prompt?;
|
||||||
|
let after = prompts::measure_static_prefix(Some(&prompt), tools);
|
||||||
|
self.session.system_prompt = Some(prompt);
|
||||||
|
self.session.last_system_prompt_hash =
|
||||||
|
Some(system_prompt_hash(self.session.system_prompt.as_ref()));
|
||||||
|
self.session.pending_prefix_change_reason =
|
||||||
|
Some("instruction-budget:static-guard".to_string());
|
||||||
|
self.active_guard_trim = Some(trim);
|
||||||
|
self.static_prefix_guard_checked = true;
|
||||||
|
|
||||||
|
let retained_after = trimmed_assembly
|
||||||
|
.as_ref()
|
||||||
|
.map_or(0, |assembly| assembly.tokens_kept());
|
||||||
|
let unsatisfiable = matches!(
|
||||||
|
crate::project_context::static_guard_outcome(
|
||||||
|
after,
|
||||||
|
&budgets,
|
||||||
|
Some(scope),
|
||||||
|
retained_after
|
||||||
|
),
|
||||||
|
crate::project_context::StaticGuardOutcome::Unsatisfiable { .. }
|
||||||
|
);
|
||||||
|
let mut body =
|
||||||
|
crate::project_context::InstructionNotice::static_guard(scope, over, unsatisfiable)
|
||||||
|
.to_string();
|
||||||
|
// The guard's own rebuild produces per-source losses; publish them with
|
||||||
|
// the scope-level sentence so the model sees which files went.
|
||||||
|
if let Some(losses) = trimmed_assembly
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|assembly| crate::project_context::render_notice_body(&assembly.notices))
|
||||||
|
{
|
||||||
|
body.push('\n');
|
||||||
|
body.push_str(&losses);
|
||||||
|
}
|
||||||
|
Some(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn installed_next_turn_prompt_context(&self) -> NextTurnPromptContext {
|
fn installed_next_turn_prompt_context(&self) -> NextTurnPromptContext {
|
||||||
|
|||||||
@@ -188,6 +188,12 @@ impl Engine {
|
|||||||
&progress.active_tool_names,
|
&progress.active_tool_names,
|
||||||
strict_tool_mode,
|
strict_tool_mode,
|
||||||
);
|
);
|
||||||
|
// #6526: the instruction-budget notice reaches the model once, as a
|
||||||
|
// logged synthetic message, and the static-prefix guard runs once with
|
||||||
|
// the real tool catalog — the only place where the prefix and the tool
|
||||||
|
// schemas are known together.
|
||||||
|
self.apply_instruction_budget_notices(active_tools.as_deref().unwrap_or_default())
|
||||||
|
.await;
|
||||||
match self
|
match self
|
||||||
.run_auto_compaction(
|
.run_auto_compaction(
|
||||||
client.as_ref(),
|
client.as_ref(),
|
||||||
|
|||||||
@@ -438,6 +438,13 @@ pub(crate) async fn run_exec_agent(
|
|||||||
);
|
);
|
||||||
let active_route_limits =
|
let active_route_limits =
|
||||||
crate::route_budget::known_route_limits(validated_route.candidate.limits());
|
crate::route_budget::known_route_limits(validated_route.candidate.limits());
|
||||||
|
// The startup instruction notice budgets against the window this route will
|
||||||
|
// actually use; the 128k default exists for callers that have no route.
|
||||||
|
let instruction_budget_window = crate::route_budget::route_context_window_tokens(
|
||||||
|
effective_provider,
|
||||||
|
&effective_model,
|
||||||
|
active_route_limits,
|
||||||
|
) as usize;
|
||||||
let max_subagents = if max_subagents
|
let max_subagents = if max_subagents
|
||||||
== config.max_subagents_for_provider(
|
== config.max_subagents_for_provider(
|
||||||
&config
|
&config
|
||||||
@@ -740,6 +747,44 @@ pub(crate) async fn run_exec_agent(
|
|||||||
eprintln!("warning: {sandbox_posture} — shell commands will run unrestricted");
|
eprintln!("warning: {sandbox_posture} — shell commands will run unrestricted");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #6526: headless runs report standing-instruction losses on stderr, the
|
||||||
|
// same content the interactive start puts in a system cell. The check
|
||||||
|
// covers every source the engine budgets, including the
|
||||||
|
// `--append-system-prompt` entry, and runs on a blocking worker: it walks
|
||||||
|
// directories and reads files.
|
||||||
|
let mut configured_sources: Vec<crate::prompts::InstructionSource> = execution_config
|
||||||
|
.instructions_paths()
|
||||||
|
.into_iter()
|
||||||
|
.map(Into::into)
|
||||||
|
.collect();
|
||||||
|
if let Some(extra) = append_system_prompt.as_ref() {
|
||||||
|
configured_sources.push(crate::prompts::InstructionSource::Inline {
|
||||||
|
name: "cli:append-system-prompt".to_string(),
|
||||||
|
content: extra.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let notice_workspace = workspace.clone();
|
||||||
|
let notice_model = effective_model.clone();
|
||||||
|
let notice = tokio::task::spawn_blocking(move || {
|
||||||
|
crate::project_context::startup_instruction_notice(
|
||||||
|
¬ice_workspace,
|
||||||
|
&configured_sources,
|
||||||
|
Some(instruction_budget_window),
|
||||||
|
Some(¬ice_model),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
match notice {
|
||||||
|
Ok(Ok(Some(notice))) => {
|
||||||
|
if output_format == ExecOutputFormat::Text && !json_output {
|
||||||
|
eprintln!("{notice}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Ok(None)) => {}
|
||||||
|
Ok(Err(error)) => anyhow::bail!(error),
|
||||||
|
Err(error) => anyhow::bail!("instruction check did not complete: {error}"),
|
||||||
|
}
|
||||||
|
|
||||||
let resuming_session = resume_session.is_some();
|
let resuming_session = resume_session.is_some();
|
||||||
let mut loaded_session_id = None;
|
let mut loaded_session_id = None;
|
||||||
if let Some(saved) = resume_session {
|
if let Some(saved) = resume_session {
|
||||||
|
|||||||
@@ -9651,6 +9651,7 @@ fn load_config_with_cli_preferences(
|
|||||||
// exec, ACP, and the app-server passthrough all resolve config here — so
|
// exec, ACP, and the app-server passthrough all resolve config here — so
|
||||||
// the loader never has to be handed the setting at each of its call sites.
|
// the loader never has to be handed the setting at each of its call sites.
|
||||||
install_foreign_instruction_imports(&config);
|
install_foreign_instruction_imports(&config);
|
||||||
|
install_instruction_budget(&config)?;
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9751,6 +9752,21 @@ fn apply_selected_fleet_operator_for_launch(
|
|||||||
/// Unrecognized names are reported rather than dropped: a typo in this key
|
/// Unrecognized names are reported rather than dropped: a typo in this key
|
||||||
/// silently means "import nothing", which is exactly the failure mode a user
|
/// silently means "import nothing", which is exactly the failure mode a user
|
||||||
/// would not notice.
|
/// would not notice.
|
||||||
|
/// Validate and install `[instruction_budget]` before anything can load
|
||||||
|
/// project context.
|
||||||
|
///
|
||||||
|
/// A broken table fails the launch: a nonsensical budget applied silently is
|
||||||
|
/// worse than a clear error, and the table is self-contained enough to be
|
||||||
|
/// checked at load time.
|
||||||
|
fn install_instruction_budget(config: &Config) -> Result<()> {
|
||||||
|
let table = config.instruction_budget.clone().unwrap_or_default();
|
||||||
|
table
|
||||||
|
.validate()
|
||||||
|
.map_err(|error| anyhow::anyhow!("invalid [instruction_budget]: {error}"))?;
|
||||||
|
crate::project_context::set_instruction_budget_config(table);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn install_foreign_instruction_imports(config: &Config) {
|
fn install_foreign_instruction_imports(config: &Config) {
|
||||||
let (imports, unknown) = crate::project_context::ForeignInstructionImports::from_config(
|
let (imports, unknown) = crate::project_context::ForeignInstructionImports::from_config(
|
||||||
&config.project_instruction_imports,
|
&config.project_instruction_imports,
|
||||||
@@ -13315,6 +13331,16 @@ fn merge_project_config_with_approval_baseline(
|
|||||||
(See #417.)"
|
(See #417.)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// #6526: the per-scope instruction budget is user authority for the same
|
||||||
|
// reason `instructions` is — a cloned repository must not be able to
|
||||||
|
// shrink the user's global budget or force `policy = "refuse"`.
|
||||||
|
if table.contains_key("instruction_budget") {
|
||||||
|
eprintln!(
|
||||||
|
"warning: project-scope `instruction_budget` is ignored — \
|
||||||
|
set instruction budgets in user config instead. \
|
||||||
|
(See #417.)"
|
||||||
|
);
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19213,6 +19239,41 @@ instructions = ["./AGENTS.md", "./extra.md"]
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn project_overlay_cannot_set_global_budget() {
|
||||||
|
let tmp = workspace_with_project_config(
|
||||||
|
r#"
|
||||||
|
[instruction_budget]
|
||||||
|
global_budget_tokens = 1
|
||||||
|
policy = "refuse"
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
let mut config = Config {
|
||||||
|
instruction_budget: Some(crate::project_context::InstructionBudgetConfig {
|
||||||
|
global_budget_tokens: Some(100_000),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Config::default()
|
||||||
|
};
|
||||||
|
merge_project_config(&mut config, tmp.path());
|
||||||
|
assert_eq!(
|
||||||
|
config
|
||||||
|
.instruction_budget
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|budget| budget.global_budget_tokens),
|
||||||
|
Some(100_000),
|
||||||
|
"a cloned repository must not shrink the user's global budget"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
config
|
||||||
|
.instruction_budget
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|budget| budget.policy),
|
||||||
|
None,
|
||||||
|
"a cloned repository must not force policy = refuse"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn project_overlay_empty_instructions_array_preserves_user_list() {
|
fn project_overlay_empty_instructions_array_preserves_user_list() {
|
||||||
let tmp = workspace_with_project_config(
|
let tmp = workspace_with_project_config(
|
||||||
|
|||||||
+1000
-494
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,647 @@
|
|||||||
|
//! Per-scope instruction budget, selection, and notice behaviour (#6526).
|
||||||
|
//!
|
||||||
|
//! One test per branch the design specifies: the `AGENTS.md`/`CLAUDE.md`
|
||||||
|
//! truth table, skip-as-absent, symlink dedupe, per-scope trims, rules
|
||||||
|
//! charging, configured entries, and the `refuse` policy.
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
fn write(path: &Path, body: &str) {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent).expect("mkdir");
|
||||||
|
}
|
||||||
|
fs::write(path, body).expect("write");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn budgets(global: usize, project: usize) -> InstructionBudgets {
|
||||||
|
InstructionBudgets {
|
||||||
|
global_tokens: global,
|
||||||
|
project_tokens: project,
|
||||||
|
static_guard_tokens: 900_000,
|
||||||
|
file_max_bytes: 8 * 1024 * 1024,
|
||||||
|
policy: InstructionBudgetPolicy::Trim,
|
||||||
|
claude_fallback: true,
|
||||||
|
usable_window_tokens: 1_000_000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_isolated(workspace: &Path, home: &Path) -> ProjectContext {
|
||||||
|
load_project_context_with_parents_and_home(workspace, Some(home))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assemble(
|
||||||
|
workspace: &Path,
|
||||||
|
home: &Path,
|
||||||
|
configured: &[crate::prompts::InstructionSource],
|
||||||
|
budgets: &InstructionBudgets,
|
||||||
|
) -> (ProjectContext, InstructionAssembly) {
|
||||||
|
let mut ctx = load_isolated(workspace, home);
|
||||||
|
let assembly = assemble_instruction_block(&mut ctx, configured, budgets);
|
||||||
|
(ctx, assembly)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn claude_imports() -> ForeignInstructionImports {
|
||||||
|
ForeignInstructionImports::from_config(&["claude".to_string()]).0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Selection ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn agents_md_wins_over_claude_md() {
|
||||||
|
let ws = tempdir().unwrap();
|
||||||
|
let home = tempdir().unwrap();
|
||||||
|
write(&ws.path().join("AGENTS.md"), "AGENTS_WINS");
|
||||||
|
write(&ws.path().join("CLAUDE.md"), "CLAUDE_LOSES");
|
||||||
|
|
||||||
|
let ctx = load_isolated(ws.path(), home.path());
|
||||||
|
let text = ctx.instructions.unwrap_or_default();
|
||||||
|
assert!(text.contains("AGENTS_WINS"), "{text}");
|
||||||
|
assert!(
|
||||||
|
!text.contains("CLAUDE_LOSES"),
|
||||||
|
"AGENTS.md wins; CLAUDE.md is not co-read without an import:\n{text}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn claude_md_is_default_fallback() {
|
||||||
|
let ws = tempdir().unwrap();
|
||||||
|
let home = tempdir().unwrap();
|
||||||
|
write(&ws.path().join("CLAUDE.md"), "CLAUDE_FALLBACK_TEXT");
|
||||||
|
|
||||||
|
let ctx = load_isolated(ws.path(), home.path());
|
||||||
|
assert!(
|
||||||
|
ctx.instructions
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("CLAUDE_FALLBACK_TEXT")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn claude_fallback_can_be_disabled() {
|
||||||
|
let config = InstructionBudgetConfig {
|
||||||
|
claude_fallback: Some(false),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
with_test_instruction_budget_config(config, || {
|
||||||
|
let ws = tempdir().unwrap();
|
||||||
|
let home = tempdir().unwrap();
|
||||||
|
write(&ws.path().join("CLAUDE.md"), "CLAUDE_OPTED_OUT");
|
||||||
|
|
||||||
|
let ctx = load_isolated(ws.path(), home.path());
|
||||||
|
assert!(
|
||||||
|
!ctx.instructions
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("CLAUDE_OPTED_OUT"),
|
||||||
|
"claude_fallback = false is an explicit opt-out"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn claude_import_reads_when_agents_absent_and_fallback_disabled() {
|
||||||
|
let config = InstructionBudgetConfig {
|
||||||
|
claude_fallback: Some(false),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
with_test_instruction_budget_config(config, || {
|
||||||
|
let ws = tempdir().unwrap();
|
||||||
|
let home = tempdir().unwrap();
|
||||||
|
write(&ws.path().join("CLAUDE.md"), "CLAUDE_EXPLICIT_IMPORT");
|
||||||
|
|
||||||
|
// The import set is explicit here: the process-wide default is
|
||||||
|
// "import nothing", and the fallback flag is off, so only the explicit
|
||||||
|
// `claude` import can read the file.
|
||||||
|
let mut ctx = load_project_context_with_parents_and_home_imports(
|
||||||
|
ws.path(),
|
||||||
|
Some(home.path()),
|
||||||
|
&claude_imports(),
|
||||||
|
);
|
||||||
|
let assembly = assemble_instruction_block(&mut ctx, &[], &budgets(1_000, 1_000));
|
||||||
|
assert!(
|
||||||
|
assembly
|
||||||
|
.block
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("CLAUDE_EXPLICIT_IMPORT")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The explicit import outranks the fallback flag: load through the import
|
||||||
|
// set the loader reads.
|
||||||
|
let ws = tempdir().unwrap();
|
||||||
|
write(&ws.path().join("CLAUDE.md"), "CLAUDE_IMPORTED");
|
||||||
|
let ctx = load_project_context_with_imports(ws.path(), &claude_imports());
|
||||||
|
assert!(
|
||||||
|
ctx.instructions
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("CLAUDE_IMPORTED")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn explicit_claude_import_reads_both() {
|
||||||
|
let ws = tempdir().unwrap();
|
||||||
|
write(&ws.path().join("AGENTS.md"), "AGENTS_PRIMARY");
|
||||||
|
write(&ws.path().join("CLAUDE.md"), "CLAUDE_CO_READ");
|
||||||
|
|
||||||
|
let ctx = load_project_context_with_imports(ws.path(), &claude_imports());
|
||||||
|
let text = ctx.instructions.unwrap_or_default();
|
||||||
|
assert!(text.contains("AGENTS_PRIMARY"), "{text}");
|
||||||
|
assert!(
|
||||||
|
text.contains("CLAUDE_CO_READ"),
|
||||||
|
"an explicit claude import co-reads CLAUDE.md next to AGENTS.md:\n{text}"
|
||||||
|
);
|
||||||
|
let agents_at = text.find("AGENTS_PRIMARY").unwrap();
|
||||||
|
let claude_at = text.find("CLAUDE_CO_READ").unwrap();
|
||||||
|
assert!(agents_at < claude_at, "AGENTS.md reads first");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn agents_md_skipped_falls_back_to_claude() {
|
||||||
|
let config = InstructionBudgetConfig {
|
||||||
|
file_max_bytes: Some(16),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
with_test_instruction_budget_config(config, || {
|
||||||
|
let ws = tempdir().unwrap();
|
||||||
|
let home = tempdir().unwrap();
|
||||||
|
write(&ws.path().join("AGENTS.md"), "A".repeat(200).as_str());
|
||||||
|
// Comfortably under the 16-byte ceiling above, or the fallback would
|
||||||
|
// be skipped for the same reason as `AGENTS.md`.
|
||||||
|
write(&ws.path().join("CLAUDE.md"), "CLAUDE_OK");
|
||||||
|
|
||||||
|
let ctx = load_isolated(ws.path(), home.path());
|
||||||
|
let text = ctx.instructions.unwrap_or_default();
|
||||||
|
assert!(
|
||||||
|
text.contains("CLAUDE_OK"),
|
||||||
|
"a skipped AGENTS.md counts as absent:\n{text}"
|
||||||
|
);
|
||||||
|
let skipped = ctx
|
||||||
|
.notices
|
||||||
|
.iter()
|
||||||
|
.find(|notice| notice.limit == InstructionLimit::FileCeiling)
|
||||||
|
.expect("the skip is reported");
|
||||||
|
assert_eq!(skipped.action, InstructionAction::Skipped);
|
||||||
|
assert!(
|
||||||
|
skipped
|
||||||
|
.fallback_selected
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|label| label.contains("CLAUDE.md")),
|
||||||
|
"the notice names the file that replaced it: {skipped:?}"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn global_claude_is_last_resort_after_instructions_md() {
|
||||||
|
let ws = tempdir().unwrap();
|
||||||
|
let home = tempdir().unwrap();
|
||||||
|
write(
|
||||||
|
&home.path().join(".codewhale/instructions.md"),
|
||||||
|
"GLOBAL_INSTRUCTIONS_WIN",
|
||||||
|
);
|
||||||
|
write(&home.path().join(".claude/CLAUDE.md"), "GLOBAL_CLAUDE_LAST");
|
||||||
|
|
||||||
|
let ctx = load_isolated(ws.path(), home.path());
|
||||||
|
let text = ctx.instructions.unwrap_or_default();
|
||||||
|
assert!(text.contains("GLOBAL_INSTRUCTIONS_WIN"), "{text}");
|
||||||
|
assert!(
|
||||||
|
!text.contains("GLOBAL_CLAUDE_LAST"),
|
||||||
|
"a foreign file never outranks codewhale's own:\n{text}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn global_claude_is_read_when_nothing_else_exists() {
|
||||||
|
let config = InstructionBudgetConfig {
|
||||||
|
claude_fallback: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
with_test_instruction_budget_config(config, || {
|
||||||
|
let ws = tempdir().unwrap();
|
||||||
|
let home = tempdir().unwrap();
|
||||||
|
write(&home.path().join(".claude/CLAUDE.md"), "GLOBAL_CLAUDE_ONLY");
|
||||||
|
|
||||||
|
let ctx = load_isolated(ws.path(), home.path());
|
||||||
|
assert!(
|
||||||
|
ctx.instructions
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("GLOBAL_CLAUDE_ONLY")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn symlinked_duplicate_is_loaded_once() {
|
||||||
|
let ws = tempdir().unwrap();
|
||||||
|
let home = tempdir().unwrap();
|
||||||
|
let global = home.path().join(".codewhale/AGENTS.md");
|
||||||
|
write(&global, "SHARED_SYMLINK_BODY");
|
||||||
|
std::os::unix::fs::symlink(&global, ws.path().join("AGENTS.md")).expect("symlink");
|
||||||
|
|
||||||
|
let ctx = load_isolated(ws.path(), home.path());
|
||||||
|
let text = ctx.instructions.unwrap_or_default();
|
||||||
|
assert_eq!(
|
||||||
|
text.matches("SHARED_SYMLINK_BODY").count(),
|
||||||
|
1,
|
||||||
|
"one physical file loads once:\n{text}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
ctx.notices.iter().any(|notice| {
|
||||||
|
notice.path.as_deref() == Some(ws.path().join("AGENTS.md").as_path())
|
||||||
|
&& notice.action == InstructionAction::Skipped
|
||||||
|
}),
|
||||||
|
"the duplicate is reported, not silently dropped: {:?}",
|
||||||
|
ctx.notices
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Per-scope budgets ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_trim_within_scope_budget() {
|
||||||
|
let ws = tempdir().unwrap();
|
||||||
|
let home = tempdir().unwrap();
|
||||||
|
write(&ws.path().join("AGENTS.md"), "SMALL PROJECT FILE");
|
||||||
|
|
||||||
|
let (ctx, assembly) = assemble(ws.path(), home.path(), &[], &budgets(10_000, 10_000));
|
||||||
|
assert!(
|
||||||
|
!assembly.notices.iter().any(|notice| notice.is_loss()),
|
||||||
|
"nothing is lost while a source is within its budget: {:?}",
|
||||||
|
assembly.notices
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ctx.instructions.as_deref(),
|
||||||
|
Some("SMALL PROJECT FILE"),
|
||||||
|
"content is unmodified"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn per_scope_budgets_are_independent() {
|
||||||
|
let ws = tempdir().unwrap();
|
||||||
|
let home = tempdir().unwrap();
|
||||||
|
write(
|
||||||
|
&home.path().join(".codewhale/AGENTS.md"),
|
||||||
|
format!("GLOBAL {}", "g".repeat(4_000)).as_str(),
|
||||||
|
);
|
||||||
|
write(&ws.path().join("AGENTS.md"), "PROJECT INTACT");
|
||||||
|
|
||||||
|
let (_ctx, assembly) = assemble(ws.path(), home.path(), &[], &budgets(50, 10_000));
|
||||||
|
let global_loss = assembly
|
||||||
|
.notices
|
||||||
|
.iter()
|
||||||
|
.find(|notice| notice.is_loss())
|
||||||
|
.expect("the global scope is over its own budget");
|
||||||
|
assert_eq!(global_loss.scope, InstructionScope::Global);
|
||||||
|
assert!(
|
||||||
|
!assembly
|
||||||
|
.notices
|
||||||
|
.iter()
|
||||||
|
.any(|notice| notice.is_loss() && notice.scope == InstructionScope::Project),
|
||||||
|
"a huge global file does not shrink the project scope: {:?}",
|
||||||
|
assembly.notices
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
assembly
|
||||||
|
.block
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("PROJECT INTACT")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trim_keeps_the_nearest_scope() {
|
||||||
|
let ws = tempdir().unwrap();
|
||||||
|
let home = tempdir().unwrap();
|
||||||
|
fs::create_dir_all(ws.path().join(".git")).unwrap();
|
||||||
|
write(&ws.path().join(".git/HEAD"), "ref: refs/heads/main\n");
|
||||||
|
write(
|
||||||
|
&ws.path().join("AGENTS.md"),
|
||||||
|
format!("ROOT-MARKER {}", "R".repeat(20_000)).as_str(),
|
||||||
|
);
|
||||||
|
let nested = ws.path().join("crates").join("tui");
|
||||||
|
write(&nested.join("AGENTS.md"), "NEAREST-MARKER stays");
|
||||||
|
|
||||||
|
let (_ctx, assembly) = assemble(&nested, home.path(), &[], &budgets(10_000, 200));
|
||||||
|
let block = assembly.block.unwrap_or_default();
|
||||||
|
assert!(block.contains("NEAREST-MARKER stays"), "{block}");
|
||||||
|
assert!(
|
||||||
|
!block.contains("ROOT-MARKER"),
|
||||||
|
"the broadest segment is dropped first:\n{block}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_oversized_file_keeps_the_head() {
|
||||||
|
let ws = tempdir().unwrap();
|
||||||
|
let home = tempdir().unwrap();
|
||||||
|
let body = format!("HEAD-MARKER {}", "z".repeat(20_000));
|
||||||
|
write(&ws.path().join("AGENTS.md"), &body);
|
||||||
|
|
||||||
|
let (_ctx, assembly) = assemble(ws.path(), home.path(), &[], &budgets(10_000, 300));
|
||||||
|
let block = assembly.block.unwrap_or_default();
|
||||||
|
assert!(block.contains("HEAD-MARKER"), "{block}");
|
||||||
|
let head_at = block.find("HEAD-MARKER").expect("head marker");
|
||||||
|
assert!(
|
||||||
|
head_at < block.find("instructions trimmed").unwrap_or(usize::MAX),
|
||||||
|
"the head survives and the marker records the cut:\n{block}"
|
||||||
|
);
|
||||||
|
assert!(block.contains("instructions trimmed"), "{block}");
|
||||||
|
assert!(
|
||||||
|
block.contains("estimated tokens"),
|
||||||
|
"the marker carries numbers"
|
||||||
|
);
|
||||||
|
assert!(assembly.global_tokens <= 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_bytes_left_is_reported_as_skipped() {
|
||||||
|
let ws = tempdir().unwrap();
|
||||||
|
let home = tempdir().unwrap();
|
||||||
|
write(&ws.path().join("AGENTS.md"), &"q".repeat(4_000));
|
||||||
|
|
||||||
|
let (_ctx, assembly) = assemble(ws.path(), home.path(), &[], &budgets(10_000, 1));
|
||||||
|
let notice = assembly
|
||||||
|
.notices
|
||||||
|
.iter()
|
||||||
|
.find(|notice| notice.scope == InstructionScope::Project)
|
||||||
|
.expect("a notice");
|
||||||
|
assert_eq!(
|
||||||
|
notice.action,
|
||||||
|
InstructionAction::Skipped,
|
||||||
|
"no source bytes left means skipped, not trimmed: {notice:?}"
|
||||||
|
);
|
||||||
|
assert_eq!(notice.limit, InstructionLimit::ScopeBudget);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rules_share_the_project_scope_budget() {
|
||||||
|
let ws = tempdir().unwrap();
|
||||||
|
let home = tempdir().unwrap();
|
||||||
|
write(&ws.path().join("AGENTS.md"), &"I".repeat(1_000));
|
||||||
|
write(&ws.path().join(".codewhale/rules/a.md"), &"R".repeat(4_000));
|
||||||
|
|
||||||
|
let tokens_for_instructions = estimate_instruction_tokens(&"I".repeat(1_000));
|
||||||
|
let (_ctx, assembly) = assemble(
|
||||||
|
ws.path(),
|
||||||
|
home.path(),
|
||||||
|
&[],
|
||||||
|
&budgets(10_000, tokens_for_instructions + 10),
|
||||||
|
);
|
||||||
|
let rules = assembly
|
||||||
|
.notices
|
||||||
|
.iter()
|
||||||
|
.find(|notice| notice.origin == InstructionOrigin::Rules)
|
||||||
|
.expect("rules are charged after the chain");
|
||||||
|
assert_eq!(rules.limit, InstructionLimit::RulesBudget);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rules_block_is_head_trimmed_with_numbers() {
|
||||||
|
let ws = tempdir().unwrap();
|
||||||
|
let home = tempdir().unwrap();
|
||||||
|
write(&ws.path().join("AGENTS.md"), "SHORT");
|
||||||
|
write(&ws.path().join(".codewhale/rules/a.md"), &"R".repeat(8_000));
|
||||||
|
|
||||||
|
let (_ctx, assembly) = assemble(ws.path(), home.path(), &[], &budgets(10_000, 400));
|
||||||
|
let block = assembly.block.unwrap_or_default();
|
||||||
|
assert!(block.contains("rules block trimmed"), "{block}");
|
||||||
|
assert!(
|
||||||
|
block.contains("estimated tokens"),
|
||||||
|
"numbers, not just a marker"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Per-file ceiling, empties, cap, duplicates ─────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn file_over_ceiling_is_skipped_not_trimmed() {
|
||||||
|
let config = InstructionBudgetConfig {
|
||||||
|
file_max_bytes: Some(8),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
with_test_instruction_budget_config(config, || {
|
||||||
|
let ws = tempdir().unwrap();
|
||||||
|
let home = tempdir().unwrap();
|
||||||
|
write(
|
||||||
|
&ws.path().join("AGENTS.md"),
|
||||||
|
"THIS IS LONGER THAN EIGHT BYTES",
|
||||||
|
);
|
||||||
|
|
||||||
|
let ctx = load_isolated(ws.path(), home.path());
|
||||||
|
assert!(
|
||||||
|
!ctx.instructions
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("LONGER THAN EIGHT")
|
||||||
|
);
|
||||||
|
let notice = ctx
|
||||||
|
.notices
|
||||||
|
.iter()
|
||||||
|
.find(|notice| notice.limit == InstructionLimit::FileCeiling)
|
||||||
|
.expect("the ceiling is reported");
|
||||||
|
assert_eq!(notice.action, InstructionAction::Skipped);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_file_is_skipped() {
|
||||||
|
let ws = tempdir().unwrap();
|
||||||
|
let home = tempdir().unwrap();
|
||||||
|
write(&ws.path().join("AGENTS.md"), " \n\t\n");
|
||||||
|
write(&ws.path().join("CLAUDE.md"), "CLAUDE_AFTER_EMPTY");
|
||||||
|
|
||||||
|
let ctx = load_isolated(ws.path(), home.path());
|
||||||
|
assert!(
|
||||||
|
ctx.notices
|
||||||
|
.iter()
|
||||||
|
.any(|notice| notice.limit == InstructionLimit::EmptyFile)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
ctx.instructions
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("CLAUDE_AFTER_EMPTY")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rules_cap_is_reported() {
|
||||||
|
let ws = tempdir().unwrap();
|
||||||
|
let home = tempdir().unwrap();
|
||||||
|
for index in 0..MAX_RULES_FILES + 5 {
|
||||||
|
write(
|
||||||
|
&ws.path()
|
||||||
|
.join(format!(".codewhale/rules/rule_{index:03}.md")),
|
||||||
|
"rule body",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let ctx = load_isolated(ws.path(), home.path());
|
||||||
|
let capped = ctx
|
||||||
|
.notices
|
||||||
|
.iter()
|
||||||
|
.filter(|notice| notice.limit == InstructionLimit::RulesCap)
|
||||||
|
.count();
|
||||||
|
assert_eq!(capped, 5, "every file beyond the cap is reported");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Configured entries and the cache ───────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn configured_instructions_are_read_fresh() {
|
||||||
|
let ws = tempdir().unwrap();
|
||||||
|
let home = tempdir().unwrap();
|
||||||
|
let extra = ws.path().join("extra.md");
|
||||||
|
write(&extra, "VERSION_ONE");
|
||||||
|
let configured = vec![crate::prompts::InstructionSource::File(extra.clone())];
|
||||||
|
|
||||||
|
let (_ctx, first) = assemble(
|
||||||
|
ws.path(),
|
||||||
|
home.path(),
|
||||||
|
&configured,
|
||||||
|
&budgets(10_000, 10_000),
|
||||||
|
);
|
||||||
|
assert!(first.block.unwrap_or_default().contains("VERSION_ONE"));
|
||||||
|
|
||||||
|
write(&extra, "VERSION_TWO");
|
||||||
|
let (_ctx, second) = assemble(
|
||||||
|
ws.path(),
|
||||||
|
home.path(),
|
||||||
|
&configured,
|
||||||
|
&budgets(10_000, 10_000),
|
||||||
|
);
|
||||||
|
let block = second.block.unwrap_or_default();
|
||||||
|
assert!(
|
||||||
|
block.contains("VERSION_TWO") && !block.contains("VERSION_ONE"),
|
||||||
|
"configured entries are read after the cache lookup:\n{block}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inline_instructions_stay_outside_cache() {
|
||||||
|
let ws = tempdir().unwrap();
|
||||||
|
let home = tempdir().unwrap();
|
||||||
|
write(&ws.path().join("AGENTS.md"), "PROJECT");
|
||||||
|
let configured = vec![crate::prompts::InstructionSource::Inline {
|
||||||
|
name: "embedded:test".to_string(),
|
||||||
|
content: "INLINE_ONLY".to_string(),
|
||||||
|
}];
|
||||||
|
|
||||||
|
let (_ctx, assembly) = assemble(
|
||||||
|
ws.path(),
|
||||||
|
home.path(),
|
||||||
|
&configured,
|
||||||
|
&budgets(10_000, 10_000),
|
||||||
|
);
|
||||||
|
assert!(assembly.block.unwrap_or_default().contains("INLINE_ONLY"));
|
||||||
|
|
||||||
|
let cached = load_isolated(ws.path(), home.path());
|
||||||
|
assert!(
|
||||||
|
!cached
|
||||||
|
.instructions
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("INLINE_ONLY"),
|
||||||
|
"inline content never enters the file-keyed cache"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn instructions_array_counts_toward_global() {
|
||||||
|
let ws = tempdir().unwrap();
|
||||||
|
let home = tempdir().unwrap();
|
||||||
|
write(&ws.path().join("AGENTS.md"), "PROJECT_KEPT");
|
||||||
|
let global_file = ws.path().join("global-extra.md");
|
||||||
|
write(&global_file, &"G".repeat(4_000));
|
||||||
|
let configured = vec![crate::prompts::InstructionSource::File(global_file)];
|
||||||
|
|
||||||
|
let (_ctx, assembly) = assemble(ws.path(), home.path(), &configured, &budgets(60, 10_000));
|
||||||
|
assert!(
|
||||||
|
assembly
|
||||||
|
.notices
|
||||||
|
.iter()
|
||||||
|
.any(|notice| notice.is_loss() && notice.scope == InstructionScope::Global),
|
||||||
|
"configured entries are global authority: {:?}",
|
||||||
|
assembly.notices
|
||||||
|
);
|
||||||
|
assert!(assembly.block.unwrap_or_default().contains("PROJECT_KEPT"));
|
||||||
|
assert!(assembly.project_tokens > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cache_returns_untrimmed_segments_across_budgets() {
|
||||||
|
let ws = tempdir().unwrap();
|
||||||
|
let home = tempdir().unwrap();
|
||||||
|
let body = format!("FULL_BODY {}", "c".repeat(10_000));
|
||||||
|
write(&ws.path().join("AGENTS.md"), &body);
|
||||||
|
|
||||||
|
let (_small, _assembly) = assemble(ws.path(), home.path(), &[], &budgets(10_000, 50));
|
||||||
|
let cached = load_isolated(ws.path(), home.path());
|
||||||
|
assert_eq!(
|
||||||
|
cached.instructions.as_deref().map(str::len),
|
||||||
|
Some(body.len()),
|
||||||
|
"a trim must not be written back into the cache"
|
||||||
|
);
|
||||||
|
assert_eq!(cached.instruction_segments.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Policy ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn refuse_policy_stops_on_trim_but_not_on_skip() {
|
||||||
|
let trim_config = InstructionBudgetConfig {
|
||||||
|
project_budget_tokens: Some(10),
|
||||||
|
policy: Some(InstructionBudgetPolicy::Refuse),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
with_test_instruction_budget_config(trim_config, || {
|
||||||
|
let ws = tempdir().unwrap();
|
||||||
|
write(&ws.path().join("AGENTS.md"), &"T".repeat(4_000));
|
||||||
|
let outcome = startup_instruction_notice(ws.path(), &[], Some(128_000), None);
|
||||||
|
let error = outcome.expect_err("refuse stops on a trim");
|
||||||
|
assert!(error.contains("refuse"), "{error}");
|
||||||
|
});
|
||||||
|
|
||||||
|
let skip_config = InstructionBudgetConfig {
|
||||||
|
file_max_bytes: Some(8),
|
||||||
|
policy: Some(InstructionBudgetPolicy::Refuse),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
with_test_instruction_budget_config(skip_config, || {
|
||||||
|
let ws = tempdir().unwrap();
|
||||||
|
write(&ws.path().join("AGENTS.md"), &"S".repeat(1_000));
|
||||||
|
let notice = startup_instruction_notice(ws.path(), &[], Some(128_000), None)
|
||||||
|
.expect("a skip is not a refusal")
|
||||||
|
.expect("a skip is reported");
|
||||||
|
assert!(notice.contains("skipped"), "{notice}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn startup_notice_names_the_loss_and_numbers() {
|
||||||
|
with_test_instruction_budget_config(
|
||||||
|
InstructionBudgetConfig {
|
||||||
|
project_budget_tokens: Some(50),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
|| {
|
||||||
|
let ws = tempdir().unwrap();
|
||||||
|
write(&ws.path().join("AGENTS.md"), &"N".repeat(4_000));
|
||||||
|
let notice = startup_instruction_notice(ws.path(), &[], Some(128_000), None)
|
||||||
|
.expect("valid config")
|
||||||
|
.expect("a trim is reported");
|
||||||
|
assert!(notice.contains("trimmed"), "{notice}");
|
||||||
|
assert!(notice.contains("estimated tokens"), "{notice}");
|
||||||
|
assert!(notice.contains("bytes"), "{notice}");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn file_max_bytes_zero_is_rejected() {
|
||||||
|
let config = InstructionBudgetConfig {
|
||||||
|
file_max_bytes: Some(0),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
assert!(config.validate().is_err());
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ use std::path::{Path, PathBuf};
|
|||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use super::budget::{InstructionNotice, InstructionSegment};
|
||||||
|
|
||||||
// === Errors ===
|
// === Errors ===
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
@@ -30,11 +32,38 @@ pub(crate) enum ProjectContextError {
|
|||||||
Empty { path: PathBuf },
|
Empty { path: PathBuf },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ProjectContextError {
|
||||||
|
/// Machine-readable reason, so a skip can name the limit that produced it.
|
||||||
|
pub(crate) fn kind(&self) -> super::budget::InstructionLimit {
|
||||||
|
match self {
|
||||||
|
Self::TooLarge { .. } => super::budget::InstructionLimit::FileCeiling,
|
||||||
|
Self::Empty { .. } => super::budget::InstructionLimit::EmptyFile,
|
||||||
|
Self::Metadata { .. } | Self::Read { .. } => super::budget::InstructionLimit::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Result of loading project context
|
/// Result of loading project context
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ProjectContext {
|
pub struct ProjectContext {
|
||||||
/// The loaded instructions content
|
/// The loaded instructions content.
|
||||||
|
///
|
||||||
|
/// This is the rendered join of [`Self::instruction_segments`] — global
|
||||||
|
/// scope first, then the project chain — and is rebuilt after every
|
||||||
|
/// budget trim, so a consumer that only reads the joined string always
|
||||||
|
/// sees post-budget content.
|
||||||
pub instructions: Option<String>,
|
pub instructions: Option<String>,
|
||||||
|
/// Raw instruction content kept per source, untrimmed.
|
||||||
|
///
|
||||||
|
/// Segment-level trimming needs to know where one file ends and the next
|
||||||
|
/// begins; a flattened string cannot answer that. The cached project
|
||||||
|
/// context is therefore stored raw and trimmed per request, so a context
|
||||||
|
/// window change never invalidates the file-keyed cache and a content
|
||||||
|
/// change never reuses trimmed text.
|
||||||
|
pub instruction_segments: Vec<InstructionSegment>,
|
||||||
|
/// Every instruction source that was trimmed, skipped, or refused while
|
||||||
|
/// loading or budgeting, for the startup notice and `/context report`.
|
||||||
|
pub notices: Vec<InstructionNotice>,
|
||||||
/// Auto-discovered rules from `.codewhale/rules/` / `.claude/rules/`.
|
/// Auto-discovered rules from `.codewhale/rules/` / `.claude/rules/`.
|
||||||
/// Kept separate from `instructions` so rules alone don't block
|
/// Kept separate from `instructions` so rules alone don't block
|
||||||
/// parent-directory AGENTS.md discovery via `has_instructions()`.
|
/// parent-directory AGENTS.md discovery via `has_instructions()`.
|
||||||
@@ -61,6 +90,8 @@ impl ProjectContext {
|
|||||||
pub fn empty(project_root: PathBuf) -> Self {
|
pub fn empty(project_root: PathBuf) -> Self {
|
||||||
Self {
|
Self {
|
||||||
instructions: None,
|
instructions: None,
|
||||||
|
instruction_segments: Vec::new(),
|
||||||
|
notices: Vec::new(),
|
||||||
rules_block: None,
|
rules_block: None,
|
||||||
source_path: None,
|
source_path: None,
|
||||||
warnings: Vec::new(),
|
warnings: Vec::new(),
|
||||||
@@ -76,6 +107,15 @@ impl ProjectContext {
|
|||||||
self.instructions.is_some()
|
self.instructions.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Rebuild [`Self::instructions`] from the current segments.
|
||||||
|
///
|
||||||
|
/// Called after assembly and after every trim. Global scope renders
|
||||||
|
/// first so the workspace keeps the last word, exactly as the single
|
||||||
|
/// merged block did before segments existed.
|
||||||
|
pub(crate) fn rebuild_joined_instructions(&mut self) {
|
||||||
|
self.instructions = super::render_joined_instructions(&self.instruction_segments);
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the instructions as a formatted block for system prompt.
|
/// Get the instructions as a formatted block for system prompt.
|
||||||
///
|
///
|
||||||
/// The Codewhale repo constitution (`.codewhale/constitution.json`), when
|
/// The Codewhale repo constitution (`.codewhale/constitution.json`), when
|
||||||
|
|||||||
+198
-178
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
use crate::project_context::load_project_context_with_parents;
|
use crate::project_context::load_project_context_with_parents;
|
||||||
use codewhale_config::AppMode;
|
use codewhale_config::AppMode;
|
||||||
use codewhale_models::{SystemBlock, SystemPrompt};
|
use codewhale_models::{SystemBlock, SystemPrompt, Tool};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::{LazyLock, Mutex};
|
use std::sync::{LazyLock, Mutex};
|
||||||
|
|
||||||
@@ -105,12 +105,13 @@ pub(crate) fn resolve_handoff_path(workspace: &Path) -> (PathBuf, &'static str)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per-file size cap for `instructions = [...]` entries (#454). Mirrors
|
/// Per-file size cap for the workspace relay artifact (`/relay`, compaction).
|
||||||
/// the existing project-context cap in `project_context::load_context_file`
|
///
|
||||||
/// so a malicious / oversized include can't blow the prompt budget on
|
/// Deliberately separate from the instruction-file ceiling: the relay is a
|
||||||
/// its own. Files larger than this are truncated with an explicit `[…truncated: N bytes omitted]`
|
/// workspace-writable checkpoint with its own trim marker, and
|
||||||
/// marker rather than skipped entirely so the model still sees the head.
|
/// `[instruction_budget] policy = "refuse"` must not change how the relay is
|
||||||
const INSTRUCTIONS_FILE_MAX_BYTES: usize = 100 * 1024;
|
/// read.
|
||||||
|
const RELAY_ARTIFACT_MAX_BYTES: usize = 100 * 1024;
|
||||||
|
|
||||||
fn read_prompt_file_bounded(path: &Path, cap: usize) -> std::io::Result<(String, usize)> {
|
fn read_prompt_file_bounded(path: &Path, cap: usize) -> std::io::Result<(String, usize)> {
|
||||||
read_prompt_text_bounded(std::fs::File::open(path)?, cap)
|
read_prompt_text_bounded(std::fs::File::open(path)?, cap)
|
||||||
@@ -439,10 +440,9 @@ impl Drop for RecordedEnvironmentGuard {
|
|||||||
/// keep working with a `.into()` upgrade at the call site.
|
/// keep working with a `.into()` upgrade at the call site.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum InstructionSource {
|
pub enum InstructionSource {
|
||||||
/// Load this file from disk at prompt-render time. Original behavior:
|
/// Read this file from disk when the instruction assembly runs.
|
||||||
/// missing files are skipped with a warning, oversized files are
|
/// Missing, empty, and over-`file_max_bytes` files are skipped with a
|
||||||
/// truncated to `INSTRUCTIONS_FILE_MAX_BYTES` with an `[…elided]`
|
/// notice; a file that fits is charged to the global scope budget.
|
||||||
/// marker.
|
|
||||||
File(PathBuf),
|
File(PathBuf),
|
||||||
/// Use the provided string directly. `name` becomes the
|
/// Use the provided string directly. `name` becomes the
|
||||||
/// `<instructions source="…">` attribute (typically a synthetic
|
/// `<instructions source="…">` attribute (typically a synthetic
|
||||||
@@ -462,63 +462,6 @@ impl From<&PathBuf> for InstructionSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render the `instructions = [...]` config array as a single
|
|
||||||
/// system-prompt block (#454). Each source is processed in declared order;
|
|
||||||
/// missing `File` sources are skipped with a tracing warning so a stale entry
|
|
||||||
/// doesn't fail the launch. Empty input (or all sources missing/empty)
|
|
||||||
/// returns `None` so callers append nothing.
|
|
||||||
fn render_instructions_block(sources: &[InstructionSource]) -> Option<String> {
|
|
||||||
let mut sections: Vec<String> = Vec::new();
|
|
||||||
for source in sources {
|
|
||||||
let mut total_len: Option<usize> = None;
|
|
||||||
let (raw_source_name, raw_content): (String, String) = match source {
|
|
||||||
InstructionSource::File(path) => {
|
|
||||||
match read_prompt_file_bounded(path, INSTRUCTIONS_FILE_MAX_BYTES) {
|
|
||||||
Ok((raw, full_len)) => {
|
|
||||||
// `full_len` covers bytes the bounded read left unread.
|
|
||||||
total_len = Some(full_len);
|
|
||||||
(path.display().to_string(), raw)
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
tracing::warn!(
|
|
||||||
target: "instructions",
|
|
||||||
?err,
|
|
||||||
?path,
|
|
||||||
"skipping unreadable instructions file"
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
InstructionSource::Inline { name, content } => (name.clone(), content.clone()),
|
|
||||||
};
|
|
||||||
let trimmed = raw_content.trim();
|
|
||||||
if trimmed.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// A bounded read that stopped short makes the file's own length the
|
|
||||||
// total; otherwise the trimmed text is everything there was.
|
|
||||||
let total = match total_len {
|
|
||||||
Some(full_len) if raw_content.len() < full_len => full_len,
|
|
||||||
_ => trimmed.len(),
|
|
||||||
};
|
|
||||||
let body = cap_prompt_text(
|
|
||||||
trimmed,
|
|
||||||
total,
|
|
||||||
INSTRUCTIONS_FILE_MAX_BYTES,
|
|
||||||
"consider splitting this instructions file",
|
|
||||||
);
|
|
||||||
sections.push(format!(
|
|
||||||
"<instructions source=\"{raw_source_name}\">\n{body}\n</instructions>"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if sections.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(sections.join("\n\n"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read the workspace-local relay artifact, if present, and format it as a
|
/// Read the workspace-local relay artifact, if present, and format it as a
|
||||||
/// system-prompt block. Returns `None` when the file is absent or empty so
|
/// system-prompt block. Returns `None` when the file is absent or empty so
|
||||||
/// callers can keep the default-uncluttered prompt for fresh workspaces.
|
/// callers can keep the default-uncluttered prompt for fresh workspaces.
|
||||||
@@ -526,7 +469,7 @@ fn load_handoff_block(workspace: &Path) -> Option<String> {
|
|||||||
let (path, shown) = resolve_handoff_path(workspace);
|
let (path, shown) = resolve_handoff_path(workspace);
|
||||||
// The relay is workspace-writable, so it gets the same per-file cap as
|
// The relay is workspace-writable, so it gets the same per-file cap as
|
||||||
// an instructions file rather than an unbounded read into the prompt.
|
// an instructions file rather than an unbounded read into the prompt.
|
||||||
let (raw, full_len) = read_prompt_file_bounded(&path, INSTRUCTIONS_FILE_MAX_BYTES).ok()?;
|
let (raw, full_len) = read_prompt_file_bounded(&path, RELAY_ARTIFACT_MAX_BYTES).ok()?;
|
||||||
let trimmed = raw.trim();
|
let trimmed = raw.trim();
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
@@ -539,7 +482,7 @@ fn load_handoff_block(workspace: &Path) -> Option<String> {
|
|||||||
let relay = cap_prompt_text(
|
let relay = cap_prompt_text(
|
||||||
trimmed,
|
trimmed,
|
||||||
total,
|
total,
|
||||||
INSTRUCTIONS_FILE_MAX_BYTES,
|
RELAY_ARTIFACT_MAX_BYTES,
|
||||||
"shorten the relay artifact",
|
"shorten the relay artifact",
|
||||||
);
|
);
|
||||||
Some(format!(
|
Some(format!(
|
||||||
@@ -1284,6 +1227,33 @@ pub(crate) fn system_prompt_for_mode_with_context_skills_session_and_approval_fo
|
|||||||
session_context: PromptSessionContext<'_>,
|
session_context: PromptSessionContext<'_>,
|
||||||
prompt_host: PromptHost,
|
prompt_host: PromptHost,
|
||||||
) -> SystemPrompt {
|
) -> SystemPrompt {
|
||||||
|
system_prompt_with_instruction_assembly_for_host(
|
||||||
|
workspace,
|
||||||
|
_working_set_summary,
|
||||||
|
skills_dir,
|
||||||
|
instructions,
|
||||||
|
session_context,
|
||||||
|
prompt_host,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compose the system prompt **and** the instruction assembly that produced
|
||||||
|
/// its instruction block.
|
||||||
|
///
|
||||||
|
/// The assembly carries the per-scope budgets and every trim/skip notice, so
|
||||||
|
/// the engine can enforce the static-prefix guard and surface the losses to
|
||||||
|
/// the user, the model, and `/context` from one measurement.
|
||||||
|
pub(crate) fn system_prompt_with_instruction_assembly_for_host(
|
||||||
|
workspace: &Path,
|
||||||
|
_working_set_summary: Option<&str>,
|
||||||
|
skills_dir: Option<&Path>,
|
||||||
|
instructions: Option<&[InstructionSource]>,
|
||||||
|
session_context: PromptSessionContext<'_>,
|
||||||
|
prompt_host: PromptHost,
|
||||||
|
guard_trim: Option<crate::project_context::InstructionGuardTrim>,
|
||||||
|
) -> (SystemPrompt, crate::project_context::InstructionAssembly) {
|
||||||
// One base prompt for every host (AGENTS.md: `BASE_PROMPT` is the sole
|
// One base prompt for every host (AGENTS.md: `BASE_PROMPT` is the sole
|
||||||
// base prompt). Headless still skips interactive ceremony layers below —
|
// base prompt). Headless still skips interactive ceremony layers below —
|
||||||
// the execution profile and authority recap — which are host chrome, not
|
// the execution profile and authority recap — which are host chrome, not
|
||||||
@@ -1299,8 +1269,34 @@ pub(crate) fn system_prompt_for_mode_with_context_skills_session_and_approval_fo
|
|||||||
&default_layers,
|
&default_layers,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Load project context from workspace
|
// Load project context from workspace, then charge it against the
|
||||||
let project_context = load_project_context_with_parents(workspace);
|
// per-scope budgets. The cache keeps raw segments; trimming happens here,
|
||||||
|
// per request, so a window change never invalidates the file cache and a
|
||||||
|
// content change never reuses trimmed text.
|
||||||
|
let mut project_context = load_project_context_with_parents(workspace);
|
||||||
|
let window_tokens = session_context
|
||||||
|
.context_window_override
|
||||||
|
.map(|window| usize::try_from(window).unwrap_or(usize::MAX));
|
||||||
|
let mut budgets = match crate::project_context::active_instruction_budgets(
|
||||||
|
window_tokens,
|
||||||
|
Some(session_context.model_id),
|
||||||
|
) {
|
||||||
|
Ok(budgets) => budgets,
|
||||||
|
Err(error) => {
|
||||||
|
// Validation runs when the table is installed, so reaching this
|
||||||
|
// branch means a budget was resolved before config was applied.
|
||||||
|
tracing::error!(target: "project_context", %error, "invalid instruction budget");
|
||||||
|
crate::project_context::InstructionBudgets::defaults_for(window_tokens)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(trim) = guard_trim {
|
||||||
|
budgets = budgets.with_scope_ceiling(trim.scope, trim.tokens);
|
||||||
|
}
|
||||||
|
let assembly = crate::project_context::assemble_instruction_block(
|
||||||
|
&mut project_context,
|
||||||
|
instructions.unwrap_or(&[]),
|
||||||
|
&budgets,
|
||||||
|
);
|
||||||
|
|
||||||
// 0. Locale-native reinforcement preamble (#1118 follow-up). When the
|
// 0. Locale-native reinforcement preamble (#1118 follow-up). When the
|
||||||
// user's UI locale is non-English we prepend a short native-script
|
// user's UI locale is non-English we prepend a short native-script
|
||||||
@@ -1317,7 +1313,7 @@ pub(crate) fn system_prompt_for_mode_with_context_skills_session_and_approval_fo
|
|||||||
// `load_project_context_with_parents` generates an in-memory bounded
|
// `load_project_context_with_parents` generates an in-memory bounded
|
||||||
// overview when no context file exists, so the fallback should usually be
|
// overview when no context file exists, so the fallback should usually be
|
||||||
// available without writing project-local files.
|
// available without writing project-local files.
|
||||||
let mut full_prompt = if let Some(project_block) = project_context.as_system_block() {
|
let mut full_prompt = if let Some(project_block) = assembly.block.as_deref() {
|
||||||
format!("{}\n\n{project_block}", composed.trim())
|
format!("{}\n\n{project_block}", composed.trim())
|
||||||
} else {
|
} else {
|
||||||
// Extremely unlikely: context generation failed (e.g. filesystem error).
|
// Extremely unlikely: context generation failed (e.g. filesystem error).
|
||||||
@@ -1450,8 +1446,11 @@ pub(crate) fn system_prompt_for_mode_with_context_skills_session_and_approval_fo
|
|||||||
}
|
}
|
||||||
let workspace_body = workspace_parts.join("\n\n");
|
let workspace_body = workspace_parts.join("\n\n");
|
||||||
|
|
||||||
// Permissions fragment: configured `instructions = [...]` files (#454).
|
// Configured `instructions = [...]` files (#454) are global-scope
|
||||||
let permissions_body = instructions.and_then(render_instructions_block);
|
// instruction authority now: the assembly reads and charges them, and they
|
||||||
|
// render inside the project-instructions block instead of as a separate
|
||||||
|
// world-state fragment. Exactly one owner, one charge.
|
||||||
|
let permissions_body: Option<String> = None;
|
||||||
|
|
||||||
// Route fragment: verbosity / translation posture (the model id was
|
// Route fragment: verbosity / translation posture (the model id was
|
||||||
// removed by the turn-meta diet — it is telemetry the model cannot act on).
|
// removed by the turn-meta diet — it is telemetry the model cannot act on).
|
||||||
@@ -1505,7 +1504,37 @@ pub(crate) fn system_prompt_for_mode_with_context_skills_session_and_approval_fo
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
SystemPrompt::Blocks(blocks)
|
(SystemPrompt::Blocks(blocks), assembly)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Estimated tokens of the **stable** prefix: the constitution block — base
|
||||||
|
/// prompt, instructions, skills index, execution profile — plus the tool
|
||||||
|
/// schemas the request carries.
|
||||||
|
///
|
||||||
|
/// Volatile world-state blocks (environment, memory, goals, relay) are
|
||||||
|
/// excluded on purpose: they drift mid-session, are not part of the pinned
|
||||||
|
/// KV-cache prefix, and carry their own budgets. Only the first block plus the
|
||||||
|
/// tool catalog is what the static-prefix guard protects.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn measure_static_prefix(system: Option<&SystemPrompt>, tools: &[Tool]) -> usize {
|
||||||
|
let text_tokens = match system {
|
||||||
|
Some(SystemPrompt::Text(text)) => {
|
||||||
|
crate::compaction::estimate_text_tokens_conservative(text)
|
||||||
|
}
|
||||||
|
Some(SystemPrompt::Blocks(blocks)) => blocks.first().map_or(0, |block| {
|
||||||
|
crate::compaction::estimate_text_tokens_conservative(&block.text)
|
||||||
|
}),
|
||||||
|
None => 0,
|
||||||
|
};
|
||||||
|
let tool_tokens: usize = tools
|
||||||
|
.iter()
|
||||||
|
.map(|tool| {
|
||||||
|
serde_json::to_string(tool).map_or(0, |json| {
|
||||||
|
crate::compaction::estimate_text_tokens_conservative(&json)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.sum();
|
||||||
|
text_tokens.saturating_add(tool_tokens)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Flatten a system prompt to joined text (tests + debug inspectors).
|
/// Flatten a system prompt to joined text (tests + debug inspectors).
|
||||||
@@ -3773,12 +3802,6 @@ mod tests {
|
|||||||
assert!(!prompt.contains("# Session relay"));
|
assert!(!prompt.contains("# Session relay"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn render_instructions_block_returns_none_for_empty_input() {
|
|
||||||
let empty: &[super::InstructionSource] = &[];
|
|
||||||
assert!(super::render_instructions_block(empty).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// #4632 — The system prompt prefix (the byte-stable part cached by
|
/// #4632 — The system prompt prefix (the byte-stable part cached by
|
||||||
/// inference servers) must never contain private content: absolute
|
/// inference servers) must never contain private content: absolute
|
||||||
/// filesystem paths, API keys, or home-directory references.
|
/// filesystem paths, API keys, or home-directory references.
|
||||||
@@ -3832,112 +3855,109 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
fn test_tool(name: &str, description: &str) -> Tool {
|
||||||
fn render_instructions_block_skips_missing_files_with_warning() {
|
Tool {
|
||||||
let tmp = tempdir().expect("tempdir");
|
tool_type: Some("function".to_string()),
|
||||||
let real = tmp.path().join("real.md");
|
name: name.to_string(),
|
||||||
std::fs::write(&real, "real content here").unwrap();
|
description: description.to_string(),
|
||||||
let bogus = tmp.path().join("does-not-exist.md");
|
input_schema: serde_json::json!({
|
||||||
|
"type": "object",
|
||||||
let block = super::render_instructions_block(&[bogus.clone().into(), real.clone().into()])
|
"properties": {"path": {"type": "string"}}
|
||||||
.expect("present file should produce a block");
|
}),
|
||||||
assert!(block.contains("real content here"));
|
allowed_callers: None,
|
||||||
assert!(block.contains(&real.display().to_string()));
|
defer_loading: None,
|
||||||
// Bogus path is skipped, not rendered.
|
input_examples: None,
|
||||||
assert!(!block.contains(&bogus.display().to_string()));
|
strict: None,
|
||||||
|
cache_control: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_instructions_block_concatenates_in_declared_order() {
|
fn static_prefix_guard_measures_tool_schemas() {
|
||||||
let tmp = tempdir().expect("tempdir");
|
let prompt = SystemPrompt::Blocks(vec![SystemBlock {
|
||||||
let a = tmp.path().join("a.md");
|
block_type: "text".to_string(),
|
||||||
let b = tmp.path().join("b.md");
|
text: "constitution".to_string(),
|
||||||
std::fs::write(&a, "ALPHA_MARKER").unwrap();
|
cache_control: None,
|
||||||
std::fs::write(&b, "BRAVO_MARKER").unwrap();
|
}]);
|
||||||
|
let without_tools = super::measure_static_prefix(Some(&prompt), &[]);
|
||||||
let block = super::render_instructions_block(&[a.into(), b.into()]).expect("non-empty");
|
let with_tools = super::measure_static_prefix(
|
||||||
let alpha_pos = block.find("ALPHA_MARKER").expect("alpha rendered");
|
Some(&prompt),
|
||||||
let bravo_pos = block.find("BRAVO_MARKER").expect("bravo rendered");
|
&[test_tool("read_file", "Read a file from the workspace")],
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
alpha_pos < bravo_pos,
|
with_tools > without_tools,
|
||||||
"instructions must concatenate in declared order"
|
"tool schemas are part of the stable prefix ({with_tools} vs {without_tools})"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_instructions_block_skips_empty_files() {
|
fn stable_prefix_guard_ignores_memory_growth() {
|
||||||
let tmp = tempdir().expect("tempdir");
|
let stable = SystemBlock {
|
||||||
let empty = tmp.path().join("empty.md");
|
block_type: "text".to_string(),
|
||||||
let real = tmp.path().join("real.md");
|
text: "stable prefix".to_string(),
|
||||||
std::fs::write(&empty, " \n \n").unwrap();
|
cache_control: None,
|
||||||
std::fs::write(&real, "real content").unwrap();
|
|
||||||
|
|
||||||
let block =
|
|
||||||
super::render_instructions_block(&[empty.into(), real.into()]).expect("non-empty");
|
|
||||||
// Empty file produces no `<instructions>` section, only the real one.
|
|
||||||
let count = block.matches("<instructions").count();
|
|
||||||
assert_eq!(count, 1, "only the non-empty file should produce a section");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn render_instructions_block_truncates_oversize_files() {
|
|
||||||
let tmp = tempdir().expect("tempdir");
|
|
||||||
let big = tmp.path().join("big.md");
|
|
||||||
// 200 KiB of content — well above the 100 KiB cap.
|
|
||||||
std::fs::write(&big, "X".repeat(200 * 1024)).unwrap();
|
|
||||||
|
|
||||||
let block = super::render_instructions_block(&[big.into()]).expect("non-empty");
|
|
||||||
assert!(block.contains("[…truncated:"), "truncation marker missing");
|
|
||||||
// Block should be much smaller than the original file.
|
|
||||||
assert!(
|
|
||||||
block.len() < 110 * 1024,
|
|
||||||
"block should be capped near 100 KiB"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `InstructionSource::Inline` bypasses disk reads — the content is used
|
|
||||||
/// directly and `name` becomes the `<instructions source="…">` attribute.
|
|
||||||
/// Empty / oversize handling mirrors `File` variant.
|
|
||||||
#[test]
|
|
||||||
fn render_instructions_block_handles_inline_source() {
|
|
||||||
let block = super::render_instructions_block(&[super::InstructionSource::Inline {
|
|
||||||
name: "embedded:test/template".to_string(),
|
|
||||||
content: "INLINE_MARKER_CONTENT".to_string(),
|
|
||||||
}])
|
|
||||||
.expect("non-empty");
|
|
||||||
assert!(block.contains("INLINE_MARKER_CONTENT"));
|
|
||||||
assert!(block.contains("source=\"embedded:test/template\""));
|
|
||||||
|
|
||||||
// Empty inline → skipped just like empty file.
|
|
||||||
let empty_inline = super::InstructionSource::Inline {
|
|
||||||
name: "empty".to_string(),
|
|
||||||
content: " ".to_string(),
|
|
||||||
};
|
};
|
||||||
assert!(super::render_instructions_block(&[empty_inline]).is_none());
|
let one_block = SystemPrompt::Blocks(vec![stable.clone()]);
|
||||||
|
let with_volatile = SystemPrompt::Blocks(vec![
|
||||||
// Oversize inline → truncated with elided marker.
|
stable,
|
||||||
let big_inline = super::InstructionSource::Inline {
|
SystemBlock {
|
||||||
name: "huge".to_string(),
|
block_type: "text".to_string(),
|
||||||
content: "Y".repeat(200 * 1024),
|
text: "MEMORY_BLOCK ".repeat(2_000),
|
||||||
};
|
cache_control: None,
|
||||||
let trimmed = super::render_instructions_block(&[big_inline]).expect("non-empty");
|
|
||||||
assert!(trimmed.contains("[…truncated:"));
|
|
||||||
|
|
||||||
// File + Inline 混用,顺序保持。
|
|
||||||
let tmp = tempdir().expect("tempdir");
|
|
||||||
let file_path = tmp.path().join("file-first.md");
|
|
||||||
std::fs::write(&file_path, "FILE_MARKER").unwrap();
|
|
||||||
let mixed = super::render_instructions_block(&[
|
|
||||||
file_path.into(),
|
|
||||||
super::InstructionSource::Inline {
|
|
||||||
name: "inline-second".to_string(),
|
|
||||||
content: "INLINE_MARKER".to_string(),
|
|
||||||
},
|
},
|
||||||
])
|
]);
|
||||||
.expect("non-empty");
|
assert_eq!(
|
||||||
let file_pos = mixed.find("FILE_MARKER").expect("file rendered");
|
super::measure_static_prefix(Some(&one_block), &[]),
|
||||||
let inline_pos = mixed.find("INLINE_MARKER").expect("inline rendered");
|
super::measure_static_prefix(Some(&with_volatile), &[]),
|
||||||
assert!(file_pos < inline_pos, "声明顺序必须保留(File then Inline)");
|
"volatile world-state blocks are not part of the guarded prefix"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relay_artifact_cap_unchanged() {
|
||||||
|
// The relay keeps its own 100 KiB cap; the instruction ceiling comes
|
||||||
|
// from `[instruction_budget] file_max_bytes` and must not change how a
|
||||||
|
// relay artifact is read.
|
||||||
|
assert_eq!(super::RELAY_ARTIFACT_MAX_BYTES, 100 * 1024);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn configured_instructions_render_inside_the_instruction_block() {
|
||||||
|
let tmp = tempdir().expect("tempdir");
|
||||||
|
let workspace = tmp.path();
|
||||||
|
let present = workspace.join("present.md");
|
||||||
|
std::fs::write(&present, "CONFIGURED_PRESENT").unwrap();
|
||||||
|
let empty = workspace.join("empty.md");
|
||||||
|
std::fs::write(&empty, " \n\n").unwrap();
|
||||||
|
let missing = workspace.join("missing.md");
|
||||||
|
let configured = vec![
|
||||||
|
super::InstructionSource::File(missing),
|
||||||
|
super::InstructionSource::File(empty),
|
||||||
|
super::InstructionSource::File(present.clone()),
|
||||||
|
super::InstructionSource::Inline {
|
||||||
|
name: "embedded:test".to_string(),
|
||||||
|
content: "INLINE_PRESENT".to_string(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let mut ctx = crate::project_context::load_project_context_with_parents(workspace);
|
||||||
|
let budgets = crate::project_context::InstructionBudgets::defaults_for(Some(128_000));
|
||||||
|
let assembly =
|
||||||
|
crate::project_context::assemble_instruction_block(&mut ctx, &configured, &budgets);
|
||||||
|
let block = assembly.block.expect("a block");
|
||||||
|
assert!(block.contains("CONFIGURED_PRESENT"), "{block}");
|
||||||
|
assert!(block.contains("INLINE_PRESENT"), "{block}");
|
||||||
|
assert!(block.contains("source=\"embedded:test\""), "{block}");
|
||||||
|
let present_at = block.find("CONFIGURED_PRESENT").unwrap();
|
||||||
|
let inline_at = block.find("INLINE_PRESENT").unwrap();
|
||||||
|
assert!(present_at < inline_at, "declared order is preserved");
|
||||||
|
assert!(
|
||||||
|
assembly
|
||||||
|
.notices
|
||||||
|
.iter()
|
||||||
|
.any(|notice| notice.action == crate::project_context::InstructionAction::Skipped),
|
||||||
|
"missing and empty entries are reported: {:?}",
|
||||||
|
assembly.notices
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -93,7 +93,10 @@ async fn get_v1_workspace_instructions_lists_effective_sources() -> Result<()> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(body["generated_fallback"], false);
|
assert_eq!(body["generated_fallback"], false);
|
||||||
assert!(body["aggregate_budget_bytes"].as_u64().unwrap() > 0);
|
// #6526: the per-scope token ceilings replaced the single byte cap.
|
||||||
|
assert!(body["global_budget_tokens"].as_u64().unwrap() > 0);
|
||||||
|
assert!(body["project_budget_tokens"].as_u64().unwrap() > 0);
|
||||||
|
assert!(body["usable_window_tokens"].as_u64().unwrap() > 0);
|
||||||
|
|
||||||
handle.abort();
|
handle.abort();
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -890,9 +890,13 @@ pub(super) struct WorkspaceInstructionsResponse {
|
|||||||
/// True when no file-based instructions exist anywhere and the
|
/// True when no file-based instructions exist anywhere and the
|
||||||
/// prompt carries the ephemeral generated context instead.
|
/// prompt carries the ephemeral generated context instead.
|
||||||
generated_fallback: bool,
|
generated_fallback: bool,
|
||||||
/// Aggregate byte ceiling applied across instructions and rules at
|
/// Resolved per-scope ceilings in estimated tokens (#6526): the global
|
||||||
/// render (`project_context::MAX_PROJECT_INSTRUCTION_BYTES`).
|
/// scope, the project scope (instructions + rules), the static-prefix
|
||||||
aggregate_budget_bytes: usize,
|
/// guard, and the usable window they are percentages of.
|
||||||
|
global_budget_tokens: usize,
|
||||||
|
project_budget_tokens: usize,
|
||||||
|
static_guard_tokens: usize,
|
||||||
|
usable_window_tokens: usize,
|
||||||
/// Warnings from the real load pass — includes assembly-level notices
|
/// Warnings from the real load pass — includes assembly-level notices
|
||||||
/// (unimported foreign formats, ignored WHALE.md, constitution parse)
|
/// (unimported foreign formats, ignored WHALE.md, constitution parse)
|
||||||
/// alongside what per-source `warning` fields already carry.
|
/// alongside what per-source `warning` fields already carry.
|
||||||
@@ -937,6 +941,10 @@ pub(super) async fn workspace_instructions(
|
|||||||
let workspace = state.workspace.clone();
|
let workspace = state.workspace.clone();
|
||||||
let home = codewhale_paths::user_home();
|
let home = codewhale_paths::user_home();
|
||||||
let configured = state.config.read().instructions_paths();
|
let configured = state.config.read().instructions_paths();
|
||||||
|
// Budgets are resolved per route window; this route has no model context
|
||||||
|
// window of its own, so it reports what the loader would use when the
|
||||||
|
// window is unknown.
|
||||||
|
let budgets = crate::project_context::active_instruction_budgets(None, None).ok();
|
||||||
|
|
||||||
let (sources, generated_fallback, warnings, workspace_root) =
|
let (sources, generated_fallback, warnings, workspace_root) =
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
@@ -999,7 +1007,10 @@ pub(super) async fn workspace_instructions(
|
|||||||
.map(str::to_string)
|
.map(str::to_string)
|
||||||
.collect(),
|
.collect(),
|
||||||
generated_fallback,
|
generated_fallback,
|
||||||
aggregate_budget_bytes: crate::project_context::MAX_PROJECT_INSTRUCTION_BYTES,
|
global_budget_tokens: budgets.map_or(0, |budgets| budgets.global_tokens),
|
||||||
|
project_budget_tokens: budgets.map_or(0, |budgets| budgets.project_tokens),
|
||||||
|
static_guard_tokens: budgets.map_or(0, |budgets| budgets.static_guard_tokens),
|
||||||
|
usable_window_tokens: budgets.map_or(0, |budgets| budgets.usable_window_tokens),
|
||||||
warnings,
|
warnings,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1111,6 +1111,45 @@ pub async fn run_tui(
|
|||||||
app.status_message = Some(notice);
|
app.status_message = Some(notice);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #6526: standing-instruction losses are reported at session start as a
|
||||||
|
// system cell. The status line above belongs to auto-resume and would be
|
||||||
|
// overwritten by the next receipt.
|
||||||
|
let configured_sources: Vec<crate::prompts::InstructionSource> = config
|
||||||
|
.instructions_paths()
|
||||||
|
.into_iter()
|
||||||
|
.map(Into::into)
|
||||||
|
.collect();
|
||||||
|
let instruction_budget_window = crate::route_budget::route_context_window_tokens(
|
||||||
|
app.api_provider,
|
||||||
|
app.effective_model_for_budget(),
|
||||||
|
app.active_route_limits,
|
||||||
|
) as usize;
|
||||||
|
// The check walks directories and reads configured files; keep it off the
|
||||||
|
// UI's async task.
|
||||||
|
let notice_workspace = app.workspace.clone();
|
||||||
|
let notice_model = app.effective_model_for_budget().to_string();
|
||||||
|
let notice = tokio::task::spawn_blocking(move || {
|
||||||
|
crate::project_context::startup_instruction_notice(
|
||||||
|
¬ice_workspace,
|
||||||
|
&configured_sources,
|
||||||
|
Some(instruction_budget_window),
|
||||||
|
Some(¬ice_model),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
match notice {
|
||||||
|
Ok(Ok(Some(notice))) => {
|
||||||
|
app.add_message(HistoryCell::System { content: notice });
|
||||||
|
}
|
||||||
|
Ok(Ok(None)) => {}
|
||||||
|
Ok(Err(error)) => return Err(anyhow::anyhow!(error)),
|
||||||
|
Err(error) => {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"instruction check did not complete: {error}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let session_id = ensure_runtime_session_id(&mut app);
|
let session_id = ensure_runtime_session_id(&mut app);
|
||||||
let transition =
|
let transition =
|
||||||
prepare_offline_queue_transition(&app, &session_id).map_err(anyhow::Error::msg)?;
|
prepare_offline_queue_transition(&app, &session_id).map_err(anyhow::Error::msg)?;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{"created_at":"<masked>","event":"turn_started","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
|
{"created_at":"<masked>","event":"turn_started","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
|
||||||
{"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"Create denied.txt with touch.","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\n## Repo Working Set\nKey files: README.md\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"}
|
{"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"Create denied.txt with touch.","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\n## Repo Working Set\nKey files: README.md\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"}
|
||||||
{"changed":false,"context_updates":0,"description":"","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
|
{"changed":false,"context_updates":0,"description":"","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
|
||||||
{"changed":false,"context_updates":0,"description":"frozen: 485d06a920ab","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
|
{"changed":false,"context_updates":0,"description":"frozen: 5fc80eccdc0f","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
|
||||||
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"6b321388357ce33f457a866082c4106d7983cdd913658e83ba8cd426b7513ef0","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16189,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":0,"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound network is on only in Full Access when the session sandbox, the network policy and your network grant allow it. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background byLine truncated
|
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"6b321388357ce33f457a866082c4106d7983cdd913658e83ba8cd426b7513ef0","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16189,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":0,"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound network is on only in Full Access when the session sandbox, the network policy and your network grant allow it. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background byLine truncated
|
||||||
{"event":"route_dispatched","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing":{"billing_mode":"metered","dispatched_at":"<timestamp>"},"billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
|
{"event":"route_dispatched","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing":{"billing_mode":"metered","dispatched_at":"<timestamp>"},"billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
|
||||||
{"event":"provider_request_receipt","receipt":{"cache_attribution":"Local prefix changes and wire schema measurements are context for reported usage, not proof that eviction caused a miss or that unreported retries were free.","elapsed_ms":"<masked>","outcome":"completion_unknown","prefix_check":{"outcome":"stable"},"request_index":1,"reused_preparation":false,"transport_retries":0,"turn_id":"<uuid:1>","usage":{"input_tokens":150,"output_tokens":15},"usage_reported":true,"wire_observations":[],"wire_observations_dropped":0}}
|
{"event":"provider_request_receipt","receipt":{"cache_attribution":"Local prefix changes and wire schema measurements are context for reported usage, not proof that eviction caused a miss or that unreported retries were free.","elapsed_ms":"<masked>","outcome":"completion_unknown","prefix_check":{"outcome":"stable"},"request_index":1,"reused_preparation":false,"transport_retries":0,"turn_id":"<uuid:1>","usage":{"input_tokens":150,"output_tokens":15},"usage_reported":true,"wire_observations":[],"wire_observations_dropped":0}}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{"created_at":"<masked>","event":"turn_started","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
|
{"created_at":"<masked>","event":"turn_started","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
|
||||||
{"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"Tell me a long story.","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\n## Repo Working Set\nKey files: README.md\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"}
|
{"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"Tell me a long story.","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\n## Repo Working Set\nKey files: README.md\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"}
|
||||||
{"changed":false,"context_updates":0,"description":"","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
|
{"changed":false,"context_updates":0,"description":"","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
|
||||||
{"changed":false,"context_updates":0,"description":"frozen: 0be12fe15cac","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
|
{"changed":false,"context_updates":0,"description":"frozen: 54c42060c945","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
|
||||||
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"6b321388357ce33f457a866082c4106d7983cdd913658e83ba8cd426b7513ef0","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16189,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":0,"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound network is on only in Full Access when the session sandbox, the network policy and your network grant allow it. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background byLine truncated
|
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"6b321388357ce33f457a866082c4106d7983cdd913658e83ba8cd426b7513ef0","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16189,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":0,"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound network is on only in Full Access when the session sandbox, the network policy and your network grant allow it. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background byLine truncated
|
||||||
{"event":"route_dispatched","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing":{"billing_mode":"metered","dispatched_at":"<timestamp>"},"billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
|
{"event":"route_dispatched","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing":{"billing_mode":"metered","dispatched_at":"<timestamp>"},"billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
|
||||||
{"event":"message_started","index":0}
|
{"event":"message_started","index":0}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{"created_at":"<masked>","event":"turn_started","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
|
{"created_at":"<masked>","event":"turn_started","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
|
||||||
{"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"What does README.md say?","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\n## Repo Working Set\nKey files: README.md\nActive paths (prioritize these):\n- README.md (file)\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"}
|
{"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"What does README.md say?","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\n## Repo Working Set\nKey files: README.md\nActive paths (prioritize these):\n- README.md (file)\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"}
|
||||||
{"changed":false,"context_updates":0,"description":"","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
|
{"changed":false,"context_updates":0,"description":"","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
|
||||||
{"changed":false,"context_updates":0,"description":"frozen: 0be12fe15cac","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
|
{"changed":false,"context_updates":0,"description":"frozen: 54c42060c945","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
|
||||||
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"6b321388357ce33f457a866082c4106d7983cdd913658e83ba8cd426b7513ef0","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16189,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":0,"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound network is on only in Full Access when the session sandbox, the network policy and your network grant allow it. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background byLine truncated
|
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"6b321388357ce33f457a866082c4106d7983cdd913658e83ba8cd426b7513ef0","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16189,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":0,"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound network is on only in Full Access when the session sandbox, the network policy and your network grant allow it. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background byLine truncated
|
||||||
{"event":"route_dispatched","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing":{"billing_mode":"metered","dispatched_at":"<timestamp>"},"billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
|
{"event":"route_dispatched","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing":{"billing_mode":"metered","dispatched_at":"<timestamp>"},"billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
|
||||||
{"event":"provider_request_receipt","receipt":{"cache_attribution":"Local prefix changes and wire schema measurements are context for reported usage, not proof that eviction caused a miss or that unreported retries were free.","elapsed_ms":"<masked>","outcome":"completion_unknown","prefix_check":{"outcome":"stable"},"request_index":1,"reused_preparation":false,"transport_retries":0,"turn_id":"<uuid:1>","usage":{"input_tokens":150,"output_tokens":20},"usage_reported":true,"wire_observations":[],"wire_observations_dropped":0}}
|
{"event":"provider_request_receipt","receipt":{"cache_attribution":"Local prefix changes and wire schema measurements are context for reported usage, not proof that eviction caused a miss or that unreported retries were free.","elapsed_ms":"<masked>","outcome":"completion_unknown","prefix_check":{"outcome":"stable"},"request_index":1,"reused_preparation":false,"transport_retries":0,"turn_id":"<uuid:1>","usage":{"input_tokens":150,"output_tokens":20},"usage_reported":true,"wire_observations":[],"wire_observations_dropped":0}}
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
{"created_at":"<masked>","event":"turn_started","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
|
{"created_at":"<masked>","event":"turn_started","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
|
||||||
{"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"Read README.md and NOTES.md.","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\n## Repo Working Set\nKey files: README.md\nActive paths (prioritize these):\n- NOTES.md (file)\n- README.md (file)\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"}
|
{"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"Read README.md and NOTES.md.","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\n## Repo Working Set\nKey files: README.md\nActive paths (prioritize these):\n- NOTES.md (file)\n- README.md (file)\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"}
|
||||||
{"changed":false,"context_updates":0,"description":"","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
|
{"changed":false,"context_updates":0,"description":"","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
|
||||||
{"changed":false,"context_updates":0,"description":"frozen: 0be12fe15cac","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
|
{"changed":false,"context_updates":0,"description":"frozen: 54c42060c945","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
|
||||||
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"6b321388357ce33f457a866082c4106d7983cdd913658e83ba8cd426b7513ef0","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16189,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":0,"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound network is on only in Full Access when the session sandbox, the network policy and your network grant allow it. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background byLine truncated
|
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"6b321388357ce33f457a866082c4106d7983cdd913658e83ba8cd426b7513ef0","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16189,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":0,"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound network is on only in Full Access when the session sandbox, the network policy and your network grant allow it. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background byLine truncated
|
||||||
{"event":"route_dispatched","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing":{"billing_mode":"metered","dispatched_at":"<timestamp>"},"billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
|
{"event":"route_dispatched","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing":{"billing_mode":"metered","dispatched_at":"<timestamp>"},"billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
|
||||||
{"event":"provider_request_receipt","receipt":{"cache_attribution":"Local prefix changes and wire schema measurements are context for reported usage, not proof that eviction caused a miss or that unreported retries were free.","elapsed_ms":"<masked>","outcome":"completion_unknown","prefix_check":{"outcome":"stable"},"request_index":1,"reused_preparation":false,"transport_retries":0,"turn_id":"<uuid:1>","usage":{"input_tokens":160,"output_tokens":40},"usage_reported":true,"wire_observations":[],"wire_observations_dropped":0}}
|
{"event":"provider_request_receipt","receipt":{"cache_attribution":"Local prefix changes and wire schema measurements are context for reported usage, not proof that eviction caused a miss or that unreported retries were free.","elapsed_ms":"<masked>","outcome":"completion_unknown","prefix_check":{"outcome":"stable"},"request_index":1,"reused_preparation":false,"transport_retries":0,"turn_id":"<uuid:1>","usage":{"input_tokens":160,"output_tokens":40},"usage_reported":true,"wire_observations":[],"wire_observations_dropped":0}}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{"created_at":"<masked>","event":"turn_started","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
|
{"created_at":"<masked>","event":"turn_started","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
|
||||||
{"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"Say hello.","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\n## Repo Working Set\nKey files: README.md\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"}
|
{"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"Say hello.","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\n## Repo Working Set\nKey files: README.md\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"}
|
||||||
{"changed":false,"context_updates":0,"description":"","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
|
{"changed":false,"context_updates":0,"description":"","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
|
||||||
{"changed":false,"context_updates":0,"description":"frozen: 0be12fe15cac","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
|
{"changed":false,"context_updates":0,"description":"frozen: 54c42060c945","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
|
||||||
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"6b321388357ce33f457a866082c4106d7983cdd913658e83ba8cd426b7513ef0","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16189,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":0,"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound network is on only in Full Access when the session sandbox, the network policy and your network grant allow it. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background byLine truncated
|
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"6b321388357ce33f457a866082c4106d7983cdd913658e83ba8cd426b7513ef0","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16189,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":0,"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound network is on only in Full Access when the session sandbox, the network policy and your network grant allow it. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background byLine truncated
|
||||||
{"event":"route_dispatched","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing":{"billing_mode":"metered","dispatched_at":"<timestamp>"},"billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
|
{"event":"route_dispatched","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing":{"billing_mode":"metered","dispatched_at":"<timestamp>"},"billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
|
||||||
{"event":"message_started","index":0}
|
{"event":"message_started","index":0}
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
{"created_at":"<masked>","event":"turn_started","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
|
{"created_at":"<masked>","event":"turn_started","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
|
||||||
{"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"Write c0205.txt.","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Full Access\nCurrent sandbox posture: full access (sandbox disabled)\n## Repo Working Set\nKey files: README.md\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"}
|
{"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"Write c0205.txt.","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Full Access\nCurrent sandbox posture: full access (sandbox disabled)\n## Repo Working Set\nKey files: README.md\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"}
|
||||||
{"changed":false,"context_updates":0,"description":"","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
|
{"changed":false,"context_updates":0,"description":"","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
|
||||||
{"changed":false,"context_updates":0,"description":"frozen: 0be12fe15cac","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
|
{"changed":false,"context_updates":0,"description":"frozen: 54c42060c945","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
|
||||||
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"6b321388357ce33f457a866082c4106d7983cdd913658e83ba8cd426b7513ef0","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16189,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":0,"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound network is on only in Full Access when the session sandbox, the network policy and your network grant allow it. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background byLine truncated
|
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"6b321388357ce33f457a866082c4106d7983cdd913658e83ba8cd426b7513ef0","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16189,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":0,"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound network is on only in Full Access when the session sandbox, the network policy and your network grant allow it. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background byLine truncated
|
||||||
{"event":"route_dispatched","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing":{"billing_mode":"metered","dispatched_at":"<timestamp>"},"billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
|
{"event":"route_dispatched","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing":{"billing_mode":"metered","dispatched_at":"<timestamp>"},"billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
|
||||||
{"category":"invalid_input","code":"invalid_input","event":"error","message":"Model not exist.","recoverable":false,"severity":"error"}
|
{"category":"invalid_input","code":"invalid_input","event":"error","message":"Model not exist.","recoverable":false,"severity":"error"}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"system_sha256": "8ff744b8ed90564a8c4754ac79cdbaf05df69ed024cc2ec154abfe581ed72d7f",
|
"system_sha256": "bc9eb8c1158d3aadb8de07b759b21bd6795d6dcb4e358ce118ab7ac32fceeea5",
|
||||||
"system_bytes": 9183,
|
"system_bytes": 9234,
|
||||||
"system_blocks": 5,
|
"system_blocks": 4,
|
||||||
"tools_sha256": "be2b1339653cbfba99a14a32777c896e766001ef0e62edca75386e749fb4940d",
|
"tools_sha256": "be2b1339653cbfba99a14a32777c896e766001ef0e62edca75386e749fb4940d",
|
||||||
"tools_bytes": 16189,
|
"tools_bytes": 16189,
|
||||||
"tool_count": 13,
|
"tool_count": 13,
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
"execute_tools",
|
"execute_tools",
|
||||||
"tool_search"
|
"tool_search"
|
||||||
],
|
],
|
||||||
"prefix_sha256": "526cfe8c2589afe41fffc96e4b33c36375c9abfc1a1608fd68c12d83caaadaa7",
|
"prefix_sha256": "2f9915b84316edcf1d12fef830586214807662f7b201bfd7a22ea37af0d4a619",
|
||||||
"masks": [
|
"masks": [
|
||||||
"<WORKSPACE>",
|
"<WORKSPACE>",
|
||||||
"<HOME>",
|
"<HOME>",
|
||||||
|
|||||||
+14
-13
@@ -97,6 +97,20 @@ If you genuinely need column-aligned data because the user asked for a table or
|
|||||||
Progress updates narrate the user's task — what you found, what you are doing next, what you decided — not the harness. Do not narrate tool plumbing: sandboxing, network routing, schema loading, tool search, retries, batching, or which tool you will call. When a gate actually blocks the work and needs the user, say what is blocked and what they can do, in their terms; otherwise just proceed.
|
Progress updates narrate the user's task — what you found, what you are doing next, what you decided — not the harness. Do not narrate tool plumbing: sandboxing, network routing, schema loading, tool search, retries, batching, or which tool you will call. When a gate actually blocks the work and needs the user, say what is blocked and what they can do, in their terms; otherwise just proceed.
|
||||||
|
|
||||||
<project_instructions source="project">
|
<project_instructions source="project">
|
||||||
|
<!-- global: runtime:mcp-registry-first -->
|
||||||
|
<instructions source="runtime:mcp-registry-first">
|
||||||
|
## MCP Registry
|
||||||
|
|
||||||
|
The Registry installs and connects a local MCP server when this session lacks a capability. It is a fallback for a capability you do not have, not a step before ordinary work.
|
||||||
|
|
||||||
|
Prefer what is already available, in order: tools already in this catalog, the project's own scripts, tests, and dev tooling, and platform capabilities. Creating a file, reading a fixture, running a repo command, and checking your own output are ordinary work — do them directly.
|
||||||
|
|
||||||
|
Reach for the Registry once you have identified a specific capability that no available tool covers and that you would otherwise install or reimplement, such as a document or media converter, access to an external database or service, or a protocol client. Then call `registry_sync` with a `query` naming that capability; it scores the local Registry snapshot host-side and returns at most eight matches, so the full index never enters the conversation. When a returned server plausibly covers that capability, call `start_registry_mcp_server` with its exact name rather than installing or running its package command through the shell. If nothing matches, refine the query once, then continue with local tools.
|
||||||
|
|
||||||
|
Both Registry tools are deferred: load one with `tool_search` before its first call, and use the returned schema. If a call instead reports that it only loaded the schema, retry once with that schema. Do not go searching for them for work you can already do.
|
||||||
|
</instructions>
|
||||||
|
|
||||||
|
<!-- project (overrides global where they conflict) -->
|
||||||
# Project Context (Auto-generated, ephemeral)
|
# Project Context (Auto-generated, ephemeral)
|
||||||
|
|
||||||
> This context was generated in memory by Codewhale.
|
> This context was generated in memory by Codewhale.
|
||||||
@@ -150,19 +164,6 @@ lists.
|
|||||||
- platform: <PLATFORM>
|
- platform: <PLATFORM>
|
||||||
- shell: <SHELL>
|
- shell: <SHELL>
|
||||||
|
|
||||||
<!-- cw:ctx:permissions -->
|
|
||||||
<instructions source="runtime:mcp-registry-first">
|
|
||||||
## MCP Registry
|
|
||||||
|
|
||||||
The Registry installs and connects a local MCP server when this session lacks a capability. It is a fallback for a capability you do not have, not a step before ordinary work.
|
|
||||||
|
|
||||||
Prefer what is already available, in order: tools already in this catalog, the project's own scripts, tests, and dev tooling, and platform capabilities. Creating a file, reading a fixture, running a repo command, and checking your own output are ordinary work — do them directly.
|
|
||||||
|
|
||||||
Reach for the Registry once you have identified a specific capability that no available tool covers and that you would otherwise install or reimplement, such as a document or media converter, access to an external database or service, or a protocol client. Then call `registry_sync` with a `query` naming that capability; it scores the local Registry snapshot host-side and returns at most eight matches, so the full index never enters the conversation. When a returned server plausibly covers that capability, call `start_registry_mcp_server` with its exact name rather than installing or running its package command through the shell. If nothing matches, refine the query once, then continue with local tools.
|
|
||||||
|
|
||||||
Both Registry tools are deferred: load one with `tool_search` before its first call, and use the returned schema. If a call instead reports that it only loaded the schema, retry once with that schema. Do not go searching for them for work you can already do.
|
|
||||||
</instructions>
|
|
||||||
|
|
||||||
<!-- cw:ctx:route -->
|
<!-- cw:ctx:route -->
|
||||||
verbosity: default
|
verbosity: default
|
||||||
translation: off
|
translation: off
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"system_sha256": "8ff744b8ed90564a8c4754ac79cdbaf05df69ed024cc2ec154abfe581ed72d7f",
|
"system_sha256": "bc9eb8c1158d3aadb8de07b759b21bd6795d6dcb4e358ce118ab7ac32fceeea5",
|
||||||
"system_bytes": 9183,
|
"system_bytes": 9234,
|
||||||
"system_blocks": 5,
|
"system_blocks": 4,
|
||||||
"tools_sha256": "c1bb513318e0f4f33100133c7cfdd04c8acbda1280efb353a688b061d5ead264",
|
"tools_sha256": "c1bb513318e0f4f33100133c7cfdd04c8acbda1280efb353a688b061d5ead264",
|
||||||
"tools_bytes": 13689,
|
"tools_bytes": 13689,
|
||||||
"tool_count": 11,
|
"tool_count": 11,
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"write",
|
"write",
|
||||||
"tool_search"
|
"tool_search"
|
||||||
],
|
],
|
||||||
"prefix_sha256": "d105d912830aa12fb1519e68df9f23c8a13dd8b8efd39daf3ab35545ab71cfcb",
|
"prefix_sha256": "1355b7e1b5a0e760261999ea277218de62d0ea8193c3cf407475462e644b541d",
|
||||||
"masks": [
|
"masks": [
|
||||||
"<WORKSPACE>",
|
"<WORKSPACE>",
|
||||||
"<HOME>",
|
"<HOME>",
|
||||||
|
|||||||
@@ -97,6 +97,20 @@ If you genuinely need column-aligned data because the user asked for a table or
|
|||||||
Progress updates narrate the user's task — what you found, what you are doing next, what you decided — not the harness. Do not narrate tool plumbing: sandboxing, network routing, schema loading, tool search, retries, batching, or which tool you will call. When a gate actually blocks the work and needs the user, say what is blocked and what they can do, in their terms; otherwise just proceed.
|
Progress updates narrate the user's task — what you found, what you are doing next, what you decided — not the harness. Do not narrate tool plumbing: sandboxing, network routing, schema loading, tool search, retries, batching, or which tool you will call. When a gate actually blocks the work and needs the user, say what is blocked and what they can do, in their terms; otherwise just proceed.
|
||||||
|
|
||||||
<project_instructions source="project">
|
<project_instructions source="project">
|
||||||
|
<!-- global: runtime:mcp-registry-first -->
|
||||||
|
<instructions source="runtime:mcp-registry-first">
|
||||||
|
## MCP Registry
|
||||||
|
|
||||||
|
The Registry installs and connects a local MCP server when this session lacks a capability. It is a fallback for a capability you do not have, not a step before ordinary work.
|
||||||
|
|
||||||
|
Prefer what is already available, in order: tools already in this catalog, the project's own scripts, tests, and dev tooling, and platform capabilities. Creating a file, reading a fixture, running a repo command, and checking your own output are ordinary work — do them directly.
|
||||||
|
|
||||||
|
Reach for the Registry once you have identified a specific capability that no available tool covers and that you would otherwise install or reimplement, such as a document or media converter, access to an external database or service, or a protocol client. Then call `registry_sync` with a `query` naming that capability; it scores the local Registry snapshot host-side and returns at most eight matches, so the full index never enters the conversation. When a returned server plausibly covers that capability, call `start_registry_mcp_server` with its exact name rather than installing or running its package command through the shell. If nothing matches, refine the query once, then continue with local tools.
|
||||||
|
|
||||||
|
Both Registry tools are deferred: load one with `tool_search` before its first call, and use the returned schema. If a call instead reports that it only loaded the schema, retry once with that schema. Do not go searching for them for work you can already do.
|
||||||
|
</instructions>
|
||||||
|
|
||||||
|
<!-- project (overrides global where they conflict) -->
|
||||||
# Project Context (Auto-generated, ephemeral)
|
# Project Context (Auto-generated, ephemeral)
|
||||||
|
|
||||||
> This context was generated in memory by Codewhale.
|
> This context was generated in memory by Codewhale.
|
||||||
@@ -150,19 +164,6 @@ lists.
|
|||||||
- platform: <PLATFORM>
|
- platform: <PLATFORM>
|
||||||
- shell: <SHELL>
|
- shell: <SHELL>
|
||||||
|
|
||||||
<!-- cw:ctx:permissions -->
|
|
||||||
<instructions source="runtime:mcp-registry-first">
|
|
||||||
## MCP Registry
|
|
||||||
|
|
||||||
The Registry installs and connects a local MCP server when this session lacks a capability. It is a fallback for a capability you do not have, not a step before ordinary work.
|
|
||||||
|
|
||||||
Prefer what is already available, in order: tools already in this catalog, the project's own scripts, tests, and dev tooling, and platform capabilities. Creating a file, reading a fixture, running a repo command, and checking your own output are ordinary work — do them directly.
|
|
||||||
|
|
||||||
Reach for the Registry once you have identified a specific capability that no available tool covers and that you would otherwise install or reimplement, such as a document or media converter, access to an external database or service, or a protocol client. Then call `registry_sync` with a `query` naming that capability; it scores the local Registry snapshot host-side and returns at most eight matches, so the full index never enters the conversation. When a returned server plausibly covers that capability, call `start_registry_mcp_server` with its exact name rather than installing or running its package command through the shell. If nothing matches, refine the query once, then continue with local tools.
|
|
||||||
|
|
||||||
Both Registry tools are deferred: load one with `tool_search` before its first call, and use the returned schema. If a call instead reports that it only loaded the schema, retry once with that schema. Do not go searching for them for work you can already do.
|
|
||||||
</instructions>
|
|
||||||
|
|
||||||
<!-- cw:ctx:route -->
|
<!-- cw:ctx:route -->
|
||||||
verbosity: default
|
verbosity: default
|
||||||
translation: off
|
translation: off
|
||||||
|
|||||||
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"system_sha256": "cd367daa552060c0b30a60263a509fdd25a0d92c89f24aa9f69c174d52076f3d",
|
"system_sha256": "d7f21fbb26b7f6f5449c9be88ff5fac02e1289b607ec0b8189bae842aa956bd9",
|
||||||
"system_bytes": 8757,
|
"system_bytes": 8808,
|
||||||
"system_blocks": 5,
|
"system_blocks": 4,
|
||||||
"tools_sha256": "be2b1339653cbfba99a14a32777c896e766001ef0e62edca75386e749fb4940d",
|
"tools_sha256": "be2b1339653cbfba99a14a32777c896e766001ef0e62edca75386e749fb4940d",
|
||||||
"tools_bytes": 16189,
|
"tools_bytes": 16189,
|
||||||
"tool_count": 13,
|
"tool_count": 13,
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
"execute_tools",
|
"execute_tools",
|
||||||
"tool_search"
|
"tool_search"
|
||||||
],
|
],
|
||||||
"prefix_sha256": "88bae4628f69e12c76eee2880fc9f93e55fedf7a832a9f42f5dcaf6bbbf6e0a6",
|
"prefix_sha256": "2b90c6212b52f1225cb57a11370be00b2e0c7917f2ddf58602b17c68528d449a",
|
||||||
"masks": [
|
"masks": [
|
||||||
"<WORKSPACE>",
|
"<WORKSPACE>",
|
||||||
"<HOME>",
|
"<HOME>",
|
||||||
|
|||||||
Vendored
+14
-13
@@ -97,6 +97,20 @@ If you genuinely need column-aligned data because the user asked for a table or
|
|||||||
Progress updates narrate the user's task — what you found, what you are doing next, what you decided — not the harness. Do not narrate tool plumbing: sandboxing, network routing, schema loading, tool search, retries, batching, or which tool you will call. When a gate actually blocks the work and needs the user, say what is blocked and what they can do, in their terms; otherwise just proceed.
|
Progress updates narrate the user's task — what you found, what you are doing next, what you decided — not the harness. Do not narrate tool plumbing: sandboxing, network routing, schema loading, tool search, retries, batching, or which tool you will call. When a gate actually blocks the work and needs the user, say what is blocked and what they can do, in their terms; otherwise just proceed.
|
||||||
|
|
||||||
<project_instructions source="AGENTS.md">
|
<project_instructions source="AGENTS.md">
|
||||||
|
<!-- global: runtime:mcp-registry-first -->
|
||||||
|
<instructions source="runtime:mcp-registry-first">
|
||||||
|
## MCP Registry
|
||||||
|
|
||||||
|
The Registry installs and connects a local MCP server when this session lacks a capability. It is a fallback for a capability you do not have, not a step before ordinary work.
|
||||||
|
|
||||||
|
Prefer what is already available, in order: tools already in this catalog, the project's own scripts, tests, and dev tooling, and platform capabilities. Creating a file, reading a fixture, running a repo command, and checking your own output are ordinary work — do them directly.
|
||||||
|
|
||||||
|
Reach for the Registry once you have identified a specific capability that no available tool covers and that you would otherwise install or reimplement, such as a document or media converter, access to an external database or service, or a protocol client. Then call `registry_sync` with a `query` naming that capability; it scores the local Registry snapshot host-side and returns at most eight matches, so the full index never enters the conversation. When a returned server plausibly covers that capability, call `start_registry_mcp_server` with its exact name rather than installing or running its package command through the shell. If nothing matches, refine the query once, then continue with local tools.
|
||||||
|
|
||||||
|
Both Registry tools are deferred: load one with `tool_search` before its first call, and use the returned schema. If a call instead reports that it only loaded the schema, retry once with that schema. Do not go searching for them for work you can already do.
|
||||||
|
</instructions>
|
||||||
|
|
||||||
|
<!-- project (overrides global where they conflict) -->
|
||||||
# Conformance project rules
|
# Conformance project rules
|
||||||
|
|
||||||
- Keep every change inside the fixture workspace.
|
- Keep every change inside the fixture workspace.
|
||||||
@@ -129,19 +143,6 @@ lists.
|
|||||||
- platform: <PLATFORM>
|
- platform: <PLATFORM>
|
||||||
- shell: <SHELL>
|
- shell: <SHELL>
|
||||||
|
|
||||||
<!-- cw:ctx:permissions -->
|
|
||||||
<instructions source="runtime:mcp-registry-first">
|
|
||||||
## MCP Registry
|
|
||||||
|
|
||||||
The Registry installs and connects a local MCP server when this session lacks a capability. It is a fallback for a capability you do not have, not a step before ordinary work.
|
|
||||||
|
|
||||||
Prefer what is already available, in order: tools already in this catalog, the project's own scripts, tests, and dev tooling, and platform capabilities. Creating a file, reading a fixture, running a repo command, and checking your own output are ordinary work — do them directly.
|
|
||||||
|
|
||||||
Reach for the Registry once you have identified a specific capability that no available tool covers and that you would otherwise install or reimplement, such as a document or media converter, access to an external database or service, or a protocol client. Then call `registry_sync` with a `query` naming that capability; it scores the local Registry snapshot host-side and returns at most eight matches, so the full index never enters the conversation. When a returned server plausibly covers that capability, call `start_registry_mcp_server` with its exact name rather than installing or running its package command through the shell. If nothing matches, refine the query once, then continue with local tools.
|
|
||||||
|
|
||||||
Both Registry tools are deferred: load one with `tool_search` before its first call, and use the returned schema. If a call instead reports that it only loaded the schema, retry once with that schema. Do not go searching for them for work you can already do.
|
|
||||||
</instructions>
|
|
||||||
|
|
||||||
<!-- cw:ctx:route -->
|
<!-- cw:ctx:route -->
|
||||||
verbosity: default
|
verbosity: default
|
||||||
translation: off
|
translation: off
|
||||||
|
|||||||
+82
-12
@@ -76,14 +76,14 @@ controls do not blur together.
|
|||||||
root down to the workspace, and puts your global instruction file (such as
|
root down to the workspace, and puts your global instruction file (such as
|
||||||
`~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last
|
`~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last
|
||||||
and wins: the workspace file overrides its parents, and project files
|
and wins: the workspace file overrides its parents, and project files
|
||||||
override the global file. A file over 100 KiB is cut to its first 100 KiB
|
override the global file. Global and project instructions are budgeted
|
||||||
and ends with a `[…truncated: N of M bytes omitted …]` marker, and
|
separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)
|
||||||
`/context report` names the file. Instruction and rules files share one
|
below): a scope over its budget drops whole files from the broadest end
|
||||||
48 KiB budget, and instruction files claim it first. Instruction text past
|
first, and a single file larger than the remaining budget keeps its head
|
||||||
the budget is dropped from the front, broadest scope first (the global
|
with a marker that names the bytes and tokens dropped. A file larger than
|
||||||
file, then the repository root), so a file larger than the budget keeps
|
`file_max_bytes` is skipped with a notice, never truncated in place. Every
|
||||||
only its end; a marker gives the bytes dropped. Rules past what is left
|
loss — trimmed, skipped, or refused — reaches the operator, the model, and
|
||||||
are cut from the end.
|
`/context report`.
|
||||||
- **Memory and handoffs** — recalled state. Useful, but lower authority than
|
- **Memory and handoffs** — recalled state. Useful, but lower authority than
|
||||||
constitutions and project instructions.
|
constitutions and project instructions.
|
||||||
|
|
||||||
@@ -1563,15 +1563,85 @@ instructions = [
|
|||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
- Paths run through `expand_path` so `~` and env vars work.
|
- Paths run through `expand_path` so `~` and env vars work.
|
||||||
- Each file is capped at 100 KiB; oversized files are
|
- These entries are **global-scope** instruction authority: they are charged
|
||||||
truncated with a `[…elided]` marker rather than skipped.
|
to the global budget together with the global `AGENTS.md`/`instructions.md`
|
||||||
- Missing files are skipped with a tracing warning so a stale
|
fallbacks and `~/.claude/CLAUDE.md`.
|
||||||
entry doesn't fail the launch.
|
- A file larger than `[instruction_budget] file_max_bytes` (8 MiB by default)
|
||||||
|
is skipped with a notice, not truncated. Missing and empty files are skipped
|
||||||
|
with a notice so a stale entry doesn't fail the launch.
|
||||||
- Only user-owned config, profiles, and managed config may set this array.
|
- Only user-owned config, profiles, and managed config may set this array.
|
||||||
Project config (`<workspace>/.codewhale/config.toml`, or legacy
|
Project config (`<workspace>/.codewhale/config.toml`, or legacy
|
||||||
`<workspace>/.deepseek/config.toml`) ignores `instructions` so a cloned repo
|
`<workspace>/.deepseek/config.toml`) ignores `instructions` so a cloned repo
|
||||||
cannot choose arbitrary local files to place into the prompt.
|
cannot choose arbitrary local files to place into the prompt.
|
||||||
|
|
||||||
|
### Instruction budget (`[instruction_budget]`, #6526)
|
||||||
|
|
||||||
|
Standing instruction files are assembled into two independent scopes and
|
||||||
|
budgeted in **estimated tokens** — the same conservative estimator `/context`
|
||||||
|
uses, never a `bytes/4` shortcut, which underestimates Cyrillic by 30–50%.
|
||||||
|
|
||||||
|
- **global** — `~/.codewhale/AGENTS.md` (or the `.agents`/`.deepseek`
|
||||||
|
fallbacks), the global `instructions.md` fallbacks, `~/.claude/CLAUDE.md`
|
||||||
|
as a last resort, and the `instructions = [...]` array.
|
||||||
|
- **project** — the repository-root → workspace `AGENTS.md` chain, co-read
|
||||||
|
foreign files, and the `.codewhale/rules`/`.claude/rules` block.
|
||||||
|
|
||||||
|
Each scope is capped at `min(absolute_tokens, percent × usable_window)`, where
|
||||||
|
the usable window is the route window minus the response ceiling and a safety
|
||||||
|
reserve of 10% (minimum 2,048 tokens).
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[instruction_budget]
|
||||||
|
global_budget_tokens = 100000
|
||||||
|
project_budget_tokens = 100000
|
||||||
|
global_budget_percent = 25
|
||||||
|
project_budget_percent = 25
|
||||||
|
static_prefix_guard_percent = 80
|
||||||
|
file_max_bytes = 8388608
|
||||||
|
policy = "trim"
|
||||||
|
claude_fallback = true
|
||||||
|
```
|
||||||
|
|
||||||
|
| window | usable (approx) | per scope | both scopes | guard 80% |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 32k | 26.8k | 6,700 | 13,400 | 21,400 |
|
||||||
|
| 128k | 113.1k | 28,275 | 56,550 | 90,500 |
|
||||||
|
| 1M | 897.9k | 100,000 | 200,000 | 718,300 |
|
||||||
|
|
||||||
|
Behaviour:
|
||||||
|
|
||||||
|
- **Nothing is trimmed while a source is within its scope budget.**
|
||||||
|
- A scope over budget drops whole files from the broadest end first — the
|
||||||
|
root-most ancestor goes first, the workspace file last — and a single file
|
||||||
|
that alone exceeds the remaining budget keeps its **head** and drops its
|
||||||
|
tail with a marker carrying bytes and estimated tokens.
|
||||||
|
- Rules are charged to the project scope after the instruction chain and keep
|
||||||
|
a head-first trim.
|
||||||
|
- `file_max_bytes` is a hard ceiling: a larger file is **skipped**, never
|
||||||
|
trimmed. The same applies to a whitespace-only file and to files beyond the
|
||||||
|
50-per-directory rules cap.
|
||||||
|
- `policy = "trim"` (default) trims and reports. `policy = "refuse"` stops the
|
||||||
|
session when a scope budget or the rules budget would trim; skips stay skips,
|
||||||
|
because one bad file must not make the project unopenable. The static-prefix
|
||||||
|
guard is a safety net rather than a promise: it trims, reports, and lets the
|
||||||
|
session start even under `refuse`.
|
||||||
|
- `static_prefix_guard_percent` caps the stable prefix (base prompt,
|
||||||
|
instructions, skills index, tool schemas). When it trips, the largest scope
|
||||||
|
is trimmed by the overage, the header is recomposed and re-pinned once, and
|
||||||
|
a notice is recorded. The guard must leave at least 10 points above the two
|
||||||
|
scope percents.
|
||||||
|
- `claude_fallback = true` (default): a directory with no `AGENTS.md` (or
|
||||||
|
codewhale `instructions.md`) uses its `CLAUDE.md`. A skipped `AGENTS.md`
|
||||||
|
counts as absent; a file already loaded from a wider scope (a symlink) is a
|
||||||
|
reported duplicate, not a fallback trigger.
|
||||||
|
- Every loss — trimmed, skipped, refused — is reported as a system cell at
|
||||||
|
session start, on stderr in `exec`, to the model as a runtime-provenance
|
||||||
|
message, and per source in `/context report`.
|
||||||
|
- The table is **user config only**, like `instructions`: project config is
|
||||||
|
ignored and a cloned repository cannot shrink the global budget or force
|
||||||
|
`refuse`. An invalid table (unknown key, zero budget, percent outside
|
||||||
|
1–100, guard without room left) fails the launch.
|
||||||
|
|
||||||
### Hooks
|
### Hooks
|
||||||
|
|
||||||
Hooks are a **TUI runtime feature**. They fire from the interactive TUI and
|
Hooks are a **TUI runtime feature**. They fire from the interactive TUI and
|
||||||
|
|||||||
@@ -0,0 +1,539 @@
|
|||||||
|
# Standing instructions: selection, budgets, and visible truncation
|
||||||
|
|
||||||
|
Plan for issue #6526 (and the comment at
|
||||||
|
https://github.com/codewhale-hq/Codewhale/issues/6526#issuecomment-6393297659).
|
||||||
|
|
||||||
|
- **Status:** v5 — implemented on this branch (see §10 for what shipped and
|
||||||
|
where the plan was written against an older tree).
|
||||||
|
- **Branch:** `fix/instruction-budget-6526` (from `main` at `498dd5a87`, 2026-10-24).
|
||||||
|
- **Scope:** standing instruction files (`AGENTS.md`, `CLAUDE.md`, `instructions.md`,
|
||||||
|
the top-level `instructions = [...]` array, `rules/`). Skills are a separate issue (#7073).
|
||||||
|
- **Orientation:** OpenCode's selection model (AGENTS.md wins, otherwise CLAUDE.md) with
|
||||||
|
codewhale's own sources preserved; no content truncation while a source is within budget.
|
||||||
|
|
||||||
|
## 1. Problem
|
||||||
|
|
||||||
|
Today codewhale has four instruction sources, three ceilings, and two different trim
|
||||||
|
directions — none of them visible to the operator.
|
||||||
|
|
||||||
|
### 1.1 Global layer
|
||||||
|
|
||||||
|
`load_global_agents_context` tries six candidates in order, first existing wins, and falls
|
||||||
|
through to the next candidate on a read error (`crates/tui/src/project_context.rs:1000-1033`):
|
||||||
|
|
||||||
|
1. `~/.codewhale/AGENTS.md`
|
||||||
|
2. `~/.agents/AGENTS.md`
|
||||||
|
3. `~/.deepseek/AGENTS.md` (legacy)
|
||||||
|
4. `~/.codewhale/instructions.md`
|
||||||
|
5. `~/.agents/instructions.md`
|
||||||
|
6. `~/.deepseek/instructions.md`
|
||||||
|
|
||||||
|
`~/.claude/CLAUDE.md` is not read today.
|
||||||
|
|
||||||
|
### 1.2 Project layer
|
||||||
|
|
||||||
|
Per directory, root → workspace:
|
||||||
|
|
||||||
|
- `AGENTS.md` (canonical);
|
||||||
|
- `.agents/AGENTS.md` (always, as a cross-agent fragment, `:196-208`);
|
||||||
|
- `.codewhale/instructions.md`;
|
||||||
|
- `.deepseek/instructions.md` (legacy);
|
||||||
|
- `CLAUDE.md` and other foreign formats only when `project_instruction_imports` enables them
|
||||||
|
(`crates/tui/src/config.rs:2346-2352`, empty by default);
|
||||||
|
- `WHALE.md` is ignored and reported as a migration diagnostic.
|
||||||
|
|
||||||
|
### 1.3 Rules
|
||||||
|
|
||||||
|
`.codewhale/rules/*.md` and `.claude/rules/*.md` (`project_context.rs:341`), plus foreign rules
|
||||||
|
directories when imports are enabled (`:254-265`). `MAX_RULES_FILES = 50` per directory
|
||||||
|
(`:390`) silently drops the rest with a `tracing::warn!` only (`:1144-1155`).
|
||||||
|
|
||||||
|
### 1.4 Configured extra files
|
||||||
|
|
||||||
|
The top-level `instructions = [...]` array (`crates/tui/src/config.rs:2327`, user config only;
|
||||||
|
the project overlay ignores it). Each entry is capped at 100 KiB and trimmed with
|
||||||
|
`[…truncated: N of M bytes omitted — …]` (`crates/tui/src/prompts.rs:113`, `:154-164`,
|
||||||
|
`:464-540`). This is not a `[context]` key: `ContextConfig` only has `project_pack`
|
||||||
|
(`config.rs:1675`) and deliberately ignores unknown fields (#6516).
|
||||||
|
|
||||||
|
### 1.5 Ceilings and trim directions
|
||||||
|
|
||||||
|
- Aggregate `MAX_PROJECT_INSTRUCTION_BYTES = 48 * 1024` (`project_context.rs:386`), shared by
|
||||||
|
instructions and rules.
|
||||||
|
- Per-file `MAX_CONTEXT_SIZE = 100 * 1024` (`:368`): a larger file is skipped (`TooLarge`,
|
||||||
|
`:1070-1075`); a whitespace-only file is skipped as `Empty` (`:1078-1082`).
|
||||||
|
- Instructions are trimmed with `keep_tail_within` (`:397-410`): keep the tail, drop the
|
||||||
|
front (broadest scope first), marker `[…broader-scope project instructions dropped at the
|
||||||
|
aggregate budget…]`.
|
||||||
|
- Rules are trimmed with `keep_head_within` (`:413-426`): keep the head, drop the tail (the
|
||||||
|
last rule files), marker `[…rules block truncated at the aggregate budget…]` (`:456-470`).
|
||||||
|
- The global text is merged into the project text (`merge_global_and_project_instructions`,
|
||||||
|
`:985-999`) before `enforce_project_instruction_budget` runs (`:552`, `:749`). Per-scope
|
||||||
|
budgets require a pipeline change: keep the scopes separate until each is measured and
|
||||||
|
trimmed, then merge.
|
||||||
|
|
||||||
|
### 1.6 Visibility
|
||||||
|
|
||||||
|
`ProjectContext.warnings` appears only in `/context report`
|
||||||
|
(`crates/tui/src/context_report.rs:560-571`) and the Runtime API source map. Nothing is shown
|
||||||
|
at session start; the model sees inline markers without sizes or file names.
|
||||||
|
|
||||||
|
### 1.7 Measured on a real setup (approximate; markers and labels excluded)
|
||||||
|
|
||||||
|
Global `~/.agents/AGENTS.md` = 71,522 B, project `AGENTS.md` = 37,571 B. Assembled 109,093 B;
|
||||||
|
the 48 KiB tail keeps the project and about 11.3 KiB (16%) of the global; about 58.5 KiB are
|
||||||
|
dropped from the top. A 26 KiB project leaves ~22.0 KiB (31%) of the global. A 120 KiB project
|
||||||
|
is skipped by the per-file ceiling and the global is still cut to 49,152 B.
|
||||||
|
|
||||||
|
### 1.8 Data model and cache today
|
||||||
|
|
||||||
|
`enforce_project_instruction_budget` sees one flattened `String` (`ctx.instructions`) plus
|
||||||
|
`ctx.rules_block`; the per-file segments (`ancestor_docs: Vec<(PathBuf, String)>`) are local to
|
||||||
|
the loader and gone by trim time. The project-context cache
|
||||||
|
(`crates/tui/src/project_context_cache.rs`, wrapper
|
||||||
|
`load_project_context_with_parents_cached_and_home`) is keyed on files, not on the context
|
||||||
|
window. Both facts constrain the design: segment-level trimming needs segments kept on
|
||||||
|
`ProjectContext`, and window-derived budgets must not be baked into a file-keyed cache.
|
||||||
|
|
||||||
|
## 2. What we compared
|
||||||
|
|
||||||
|
### Claude Code (`code.claude.com/docs/en/memory`)
|
||||||
|
|
||||||
|
- Load order, broadest → most specific: managed policy → `~/.claude/CLAUDE.md` → project
|
||||||
|
(`./CLAUDE.md` or `./.claude/CLAUDE.md`, every ancestor) → `./CLAUDE.local.md`.
|
||||||
|
- All discovered files are concatenated, order root → cwd; **no size cap, no truncation**.
|
||||||
|
- `AGENTS.md` is read only when no `CLAUDE.md`/`.claude/CLAUDE.md`/`CLAUDE.local.md` exists in
|
||||||
|
cwd or above; a setting can switch to both / CLAUDE-only / managed-only.
|
||||||
|
- Imports `@path` up to 4 hops; `.claude/rules/` and path-scoped rules.
|
||||||
|
- Only explicit limit: auto memory, first 200 lines or 25 KB.
|
||||||
|
|
||||||
|
### OpenCode (`opencode.ai/docs/rules`, source `anomalyco/opencode@dev`)
|
||||||
|
|
||||||
|
- Per category: local `AGENTS.md` beats `CLAUDE.md`; global
|
||||||
|
`~/.config/opencode/AGENTS.md` beats `~/.claude/CLAUDE.md`.
|
||||||
|
- `instructions` in `opencode.json` adds paths, globs, and remote URLs (5 s timeout), combined
|
||||||
|
with `AGENTS.md`.
|
||||||
|
- **No content truncation.** `config/plugin/instruction.ts` reads global + ancestor
|
||||||
|
`AGENTS.md` + configured sources; `session/instructions.ts` injects each file whole as a
|
||||||
|
synthetic message `Instructions from: <path>\n<content>`.
|
||||||
|
- Order: global → project (nearest → farthest) → configured.
|
||||||
|
|
||||||
|
### codewhale today
|
||||||
|
|
||||||
|
Concatenate global + project + rules, then trim to 48 KiB: instructions keep the tail, rules
|
||||||
|
keep the head, global is discarded first. The only one of the three that cuts content.
|
||||||
|
|
||||||
|
## 3. Decisions
|
||||||
|
|
||||||
|
### 3.1 Selection
|
||||||
|
|
||||||
|
- **Project, per directory:** `AGENTS.md` wins; if absent, `CLAUDE.md` is the default
|
||||||
|
fallback (OpenCode parity), controlled by `claude_fallback = true`. `CLAUDE.md` is also read
|
||||||
|
alongside `AGENTS.md` when `project_instruction_imports` includes `claude`.
|
||||||
|
|
||||||
|
| AGENTS.md | CLAUDE.md | `claude` import | `claude_fallback` | read |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| loaded | absent | any | any | AGENTS.md |
|
||||||
|
| loaded | loaded | no | any | AGENTS.md |
|
||||||
|
| loaded | loaded | yes | any | both |
|
||||||
|
| absent/skipped | loaded | yes | any | CLAUDE.md (explicit import outranks the fallback flag) |
|
||||||
|
| absent/skipped | loaded | no | true | CLAUDE.md |
|
||||||
|
| absent/skipped | loaded | no | false | nothing (explicit opt-out) |
|
||||||
|
| absent/skipped | absent/skipped | any | any | nothing |
|
||||||
|
|
||||||
|
`CLAUDE.md` here means `CLAUDE.md` or `.claude/CLAUDE.md` in the directory;
|
||||||
|
`CLAUDE.local.md` is Claude-specific and is not imported.
|
||||||
|
|
||||||
|
**Skip counts as absent:** if the winning `AGENTS.md` is too large or empty, it is treated
|
||||||
|
as absent and the next candidate (CLAUDE.md) is tried; the notice names both the skipped
|
||||||
|
file and the fallback selected. The same rule applies to the global chain: a skipped
|
||||||
|
candidate falls through to the next one. An **empty project scope after a symlink dedupe**
|
||||||
|
is not "absent": the content is already loaded as global, so no fallback runs; the notice
|
||||||
|
says `duplicate`.
|
||||||
|
|
||||||
|
- **Global:** the six existing candidates in order, then `~/.claude/CLAUDE.md` as the last
|
||||||
|
resort when none of the six loads and `claude_fallback = true`. Foreign files never outrank
|
||||||
|
codewhale's own.
|
||||||
|
- **All ancestor directories** are read, root → workspace.
|
||||||
|
- **Symlinks are followed** (current `open_read` behaviour); duplicates are detected by
|
||||||
|
canonical path (inode only on Unix) and loaded once. A project file symlinked to the global
|
||||||
|
file is counted as global; the project duplicate is reported, not counted twice.
|
||||||
|
- `.agents/AGENTS.md` is always a fragment; it is not deduped against a same-directory
|
||||||
|
`AGENTS.md` unless the canonical path is identical.
|
||||||
|
|
||||||
|
### 3.2 Budgets
|
||||||
|
|
||||||
|
- Budgets are in **tokens**. Files and the per-file ceiling are in bytes; conversion uses the
|
||||||
|
engine's token estimator on the assembled text, the same estimator the context report uses.
|
||||||
|
No `bytes/4` shortcut (it underestimates Cyrillic by 30–50%).
|
||||||
|
- **Usable window** = `context_window − max_output_tokens − safety_reserve`
|
||||||
|
(`safety_reserve` = 10% of the window, minimum 2,048 tokens). All percentages are of the
|
||||||
|
usable window, not the raw window.
|
||||||
|
- Per-scope budgets, independent:
|
||||||
|
`effective = min(absolute_tokens, percent × usable_window)`.
|
||||||
|
Defaults: **100,000 tokens** and **25%** per scope. All values are positive; `0` is invalid
|
||||||
|
(a finite budget is the point). `percent` is 1–100.
|
||||||
|
- **Scope assignment:** global scope = the six global candidates + `~/.claude/CLAUDE.md` + the
|
||||||
|
configured `instructions = [...]` entries (user-level authority). Project scope = the
|
||||||
|
per-directory chain + fragments + `CLAUDE.md` + rules. Configured entries are therefore
|
||||||
|
global, not project; rules are project.
|
||||||
|
- **Static-prefix guard:** the static prefix = base system prompt + tool-use policy +
|
||||||
|
environment block + instructions + skills index + tool schemas + memory. It must stay under
|
||||||
|
`static_prefix_guard_percent` (default 80% of the usable window). The guard is the safety
|
||||||
|
net for non-instruction static content; it is not a substitute for a total-instructions key.
|
||||||
|
- **No separate "total instructions" key.** A key equal to the sum of the two scope caps
|
||||||
|
(50%) would be inert; a smaller one would contradict the per-scope decision. The guard
|
||||||
|
covers the actual risk.
|
||||||
|
- Effective table (raw windows, before the output/safety reserve; the reserve only lowers the
|
||||||
|
usable window):
|
||||||
|
|
||||||
|
| window | usable (approx) | per scope | both scopes | guard 80% | residual for other static |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 32k | ~26.8k | 6,700 | 13,400 | 21,400 | 8,000 |
|
||||||
|
| 120k | ~105.8k | 26,450 | 52,900 | 84,600 | 31,700 |
|
||||||
|
| 128k | ~113.1k | 28,275 | 56,550 | 90,500 | 33,950 |
|
||||||
|
| 150k | ~132.8k | 33,200 | 66,400 | 106,200 | 39,800 |
|
||||||
|
| 1M | ~897.9k | 100,000 (10%) | 200,000 (20%) | 718,300 | 518,300 |
|
||||||
|
|
||||||
|
The owner's 1M expectation (10% per scope, 20% total) is preserved by the absolute cap.
|
||||||
|
A 32k window is best-effort. Two thresholds are distinct: guard trimming begins when
|
||||||
|
non-instruction static content exceeds the residual (~8,000 tokens at 32k); the guard is
|
||||||
|
`unsatisfiable` only when non-instruction static content alone exceeds the guard
|
||||||
|
(~21,400 tokens at 32k). In the first case instructions are trimmed by the overage; in the
|
||||||
|
second they reach zero and the session proceeds (or refuses, under `policy = refuse`).
|
||||||
|
|
||||||
|
### 3.3 Per-file ceiling
|
||||||
|
|
||||||
|
- `file_max_bytes = 8 MiB` is a memory-safety limit, not truncation. The size is checked with
|
||||||
|
`metadata()` before reading; a larger file is `skipped`, with a notice naming the file and
|
||||||
|
the limit. There is no `0`/unlimited value: a hard ceiling must always exist.
|
||||||
|
- A whitespace-only file is `skipped (empty)`.
|
||||||
|
- `MAX_RULES_FILES = 50` stays as a safety cap, but its overflow is reported as
|
||||||
|
`skipped (rules cap)` in the notice and `/context`; it is no longer silent.
|
||||||
|
- Files are read as a whole up to the ceiling. For files near 8 MiB, tokenization is the
|
||||||
|
expensive step; the scope budget will trim after tokenization. A byte-side pre-cut is an
|
||||||
|
allowed optimization, not a behaviour change.
|
||||||
|
|
||||||
|
### 3.4 Trim semantics
|
||||||
|
|
||||||
|
- `ProjectContext` keeps `instruction_segments: Vec<(PathBuf, String)>` (scope, path, text) in
|
||||||
|
addition to the joined string. Per-scope trimming operates on segments; the joined string is
|
||||||
|
rebuilt after trimming.
|
||||||
|
- Each scope is trimmed only against its own budget; the other scope is untouched.
|
||||||
|
- Within the project scope, drop whole chain segments from the broadest end first (root-most
|
||||||
|
ancestors go first, the workspace file last). If a single file alone exceeds the remaining
|
||||||
|
budget, keep its head, drop its tail with `keep_head_within`, and insert a marker with
|
||||||
|
bytes/tokens. This reverses today's single-file direction (today `keep_tail_within` keeps
|
||||||
|
the tail); the change is listed in §9. `keep_tail_within` is no longer used for instructions.
|
||||||
|
- Within the global scope there is normally one file; the same single-file rule applies.
|
||||||
|
- Rules are charged to the project scope after project instructions; they keep the current
|
||||||
|
head-first trim (rules are a flat filename-sorted list, with no nearest-scope hierarchy to
|
||||||
|
preserve), and the marker and notice carry the numbers.
|
||||||
|
- The trim amount is `over = content_tokens − budget_tokens`; trim `min(scope_tokens, over)`
|
||||||
|
from the chosen scope, then re-measure once. There is no loop.
|
||||||
|
- The marker is included in the budget (as today: `max − marker_len`). The cut point is found
|
||||||
|
by binary search on UTF-8 boundaries so the result is deterministic.
|
||||||
|
- If trimming leaves zero source bytes (marker excluded), report `skipped (budget)`, not
|
||||||
|
`trimmed`. This is reachable when the budget is smaller than the marker or when the static
|
||||||
|
guard trims a scope to zero.
|
||||||
|
- If the static guard trips, trim the largest instruction scope first, by `over`; re-render
|
||||||
|
once, re-measure once. If both scopes reach zero and the guard is still exceeded, emit
|
||||||
|
`static_guard_unsatisfied` and continue (policy `trim`) or stop (policy `refuse`). There is
|
||||||
|
no retry loop and only one notice per scope per session.
|
||||||
|
|
||||||
|
### 3.5 Policy
|
||||||
|
|
||||||
|
- Default: `trim` + warning in the chat. `refuse` remains an opt-in value for operators who
|
||||||
|
prefer a hard stop. `refuse` stops on **trim** losses (scope budget or static guard) and on
|
||||||
|
the unsatisfiable guard; **skips** (file ceiling, empty file, rules cap) remain skips and are
|
||||||
|
reported, because a single bad file must not make the project unopenable.
|
||||||
|
- The `warn` value is removed; "warn" is what `trim` does.
|
||||||
|
- Validation rejects unknown policy values, non-positive budgets, percents outside 1–100, a
|
||||||
|
guard outside 1–100, and any config where `guard − (global_percent + project_percent) < 10`;
|
||||||
|
the remaining 10 points are the minimum room for non-instruction static content.
|
||||||
|
|
||||||
|
### 3.6 Notice contract
|
||||||
|
|
||||||
|
| Trigger | Action | User | Model |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| file > `file_max_bytes` | skip | startup line | first-turn synthetic |
|
||||||
|
| empty file | skip | startup line | first-turn synthetic |
|
||||||
|
| rules cap 50 | skip files | startup line | first-turn synthetic |
|
||||||
|
| duplicate via symlink | skip duplicate | startup line | first-turn synthetic |
|
||||||
|
| scope > budget, `trim` | trim that scope | startup line + numbers | first-turn synthetic + marker |
|
||||||
|
| zero source bytes left | skip | "skipped", not "trimmed" | same |
|
||||||
|
| static guard, `trim` | trim largest scope by `over` | startup line | first-turn synthetic |
|
||||||
|
| static guard unsatisfiable | proceed (trim) / stop (refuse) | named line | first-turn synthetic |
|
||||||
|
| `refuse` policy on trim | stop | named error | session does not start |
|
||||||
|
| all within budgets | no notice | no | no |
|
||||||
|
|
||||||
|
Fields: `path`, `scope` (`global`/`project`), `source` (`agents`/`claude`/`instructions`/
|
||||||
|
`configured`/`rules`), `action` (`kept`/`trimmed`/`skipped`/`refused`), `bytes_kept`,
|
||||||
|
`bytes_dropped`, `tokens_kept` (estimated), `tokens_dropped`, `limit_kind` (`scope_budget`/
|
||||||
|
`static_guard`/`file_ceiling`/`empty_file`/`rules_cap`/`rules_budget`/`duplicate_path`), and
|
||||||
|
`fallback_selected` when a skipped file was replaced by a lower-precedence candidate.
|
||||||
|
|
||||||
|
Channels:
|
||||||
|
|
||||||
|
- **User:** a system history cell at session start (`app.add_message(HistoryCell::System {
|
||||||
|
content })`), not the `startup_notice` status-line field (that one belongs to auto-resume and
|
||||||
|
would be overwritten); stderr for headless/`exec` (text output). The notice is recomputed
|
||||||
|
deterministically from files + config, so a resumed session states the same losses.
|
||||||
|
- **Model:** a first-turn synthetic runtime-provenance message (the LSP-diagnostics mechanism;
|
||||||
|
see §10), which is part of the logged transcript.
|
||||||
|
- **`/context report`:** per-source action and budget, alongside the existing warnings at
|
||||||
|
`context_report.rs:560`.
|
||||||
|
|
||||||
|
## 4. Config surface
|
||||||
|
|
||||||
|
Top-level `instructions = [...]` stays as is (array). New table **`[instruction_budget]`**
|
||||||
|
(not `[instructions]`, which would collide with the array; not `[context]`, which ignores
|
||||||
|
unknown keys and would hide typos):
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[instruction_budget]
|
||||||
|
global_budget_tokens = 100000 # positive
|
||||||
|
project_budget_tokens = 100000 # positive
|
||||||
|
global_budget_percent = 25 # 1..=100
|
||||||
|
project_budget_percent = 25 # 1..=100
|
||||||
|
static_prefix_guard_percent = 80 # 1..=100
|
||||||
|
file_max_bytes = 8388608 # hard ceiling, positive
|
||||||
|
policy = "trim" # trim | refuse
|
||||||
|
claude_fallback = true # AGENTS.md wins; otherwise CLAUDE.md
|
||||||
|
```
|
||||||
|
|
||||||
|
- `deny_unknown_fields` on this table so a typo inside it fails loud.
|
||||||
|
- The **project overlay ignores this table** (like `instructions`), so a cloned repository
|
||||||
|
cannot shrink the user's global budget or force `refuse`. The overlay/base merge happens at
|
||||||
|
`config.rs:10625-10628`; the new table follows the `instructions` rule there (user config
|
||||||
|
only).
|
||||||
|
- Because the top-level config is tolerant of unknown tables (#6516), a misspelled table name
|
||||||
|
(`[instruction_budjet]`) would be ignored; `codewhale doctor` inspects the raw TOML before
|
||||||
|
deserialization and warns about unknown `instruction*` tables and keys.
|
||||||
|
|
||||||
|
## 5. Implementation plan
|
||||||
|
|
||||||
|
1. **New assembly order** in `crates/tui/src/project_context.rs`:
|
||||||
|
`assemble_instruction_scopes(raw: &ProjectContext, configured: &[PathBuf], budgets:
|
||||||
|
&InstructionBudgets)` reads configured entries fresh (they are user config and are not in
|
||||||
|
the cache key), joins them to the global scope, applies per-scope trims, then merges with
|
||||||
|
labels. Imports are already baked into the raw cache; budgets are passed separately. The
|
||||||
|
new function owns the split; the configured entries move out of
|
||||||
|
`prompts.rs::render_instructions_block`, so there is exactly one owner and one charge
|
||||||
|
(global).
|
||||||
|
2. **Cache**: `project_context_cache` keeps **raw segments** keyed by the existing file
|
||||||
|
candidate set (`project_context_cache_candidate_paths`, `project_context.rs:754-800`).
|
||||||
|
`apply_instruction_budgets` runs after cache lookup, per request/window; configured
|
||||||
|
`instructions` entries are read after cache lookup, so a change there is picked up without
|
||||||
|
cache invalidation; `InstructionSource::Inline` stays outside the cache. A window change
|
||||||
|
must not invalidate the content cache, and a content change must not reuse trimmed text.
|
||||||
|
3. **`crates/tui/src/project_context.rs`**
|
||||||
|
- `ProjectContext` gains `instruction_segments: Vec<InstructionSegment>` (scope, path,
|
||||||
|
text); the joined string is rebuilt after trimming.
|
||||||
|
- Delete both `enforce_project_instruction_budget` call sites (`:552`, `:749`) and replace
|
||||||
|
them with `apply_instruction_budgets`, called by the assembly after cache lookup.
|
||||||
|
- `load_global_agents_context`: add `~/.claude/CLAUDE.md` as the last candidate; keep the
|
||||||
|
six existing candidates and fall-through; report the selected fallback.
|
||||||
|
- `load_dir_instructions` / `active_fragment_candidates`: implement the truth table from
|
||||||
|
§3.1 (skip-as-absent, explicit import co-reads, symlink dedupe).
|
||||||
|
- `apply_instruction_budgets`: per-scope budgets; segments dropped whole; `keep_head_within`
|
||||||
|
for a single oversized file and the rules block; `keep_tail_within` no longer used for
|
||||||
|
instructions.
|
||||||
|
- `MAX_RULES_FILES` overflow → structured warning, not only `tracing::warn!`.
|
||||||
|
- Module-level known-limitations note (repo rule) describing the estimator and trim order.
|
||||||
|
4. **`crates/tui/src/prompts.rs`**
|
||||||
|
- `INSTRUCTIONS_FILE_MAX_BYTES` from `file_max_bytes`; split the relay-artifact cap from
|
||||||
|
the instruction cap so `policy = refuse` does not change relay behaviour.
|
||||||
|
`render_instructions_block` renders only the assembled instruction block (no configured
|
||||||
|
entries).
|
||||||
|
5. **Static guard**: `prompts.rs` exposes `measure_static_prefix(&SystemPrompt, &[ToolSpec])`;
|
||||||
|
`core/engine/preview.rs` (`compose_stable_system_prompt` + `system_prompt_text`) calls
|
||||||
|
`enforce_static_prefix_guard` after the stable prefix is composed and the tool registry is
|
||||||
|
known. The guard covers the **stable** prefix only (base prompt, tool policy, instructions,
|
||||||
|
skills index, tool schemas); volatile memory/environment blocks are excluded (memory has
|
||||||
|
its own budget). On trim, rebuild the instruction block once, recompose, re-measure once;
|
||||||
|
if both scopes are zero and the stable prefix is still over, return `Unsatisfiable`.
|
||||||
|
6. **Startup notice**
|
||||||
|
- Emit the user line as a `HistoryCell::System` at session start (TUI), stderr
|
||||||
|
(headless/exec), and a session event (Runtime API); re-emit on resume from the persisted
|
||||||
|
event. Do not reuse `startup_notice` (auto-resume status line).
|
||||||
|
7. **`crates/tui/src/context_report.rs`**
|
||||||
|
- Show per-source action, bytes/tokens, budget and limit kind.
|
||||||
|
8. **Docs and registry**
|
||||||
|
- `docs/CONFIGURATION.md`, `docs/zh_hans/CONFIGURATION.md` (mirror, or an explicit deferral
|
||||||
|
note with the sync date), `config.example.toml` (replace the fixed 48 KiB comment),
|
||||||
|
`CHANGELOG.md`, and a `docs/features.toml` row with id/name/summary/status/surfaces/
|
||||||
|
since/flag/docs/owner (the registry test fails on drift).
|
||||||
|
9. **Tests** (one per branch, plus the boundary cases):
|
||||||
|
- `agents_md_wins_over_claude_md`
|
||||||
|
- `claude_md_is_default_fallback`
|
||||||
|
- `claude_fallback_can_be_disabled`
|
||||||
|
- `claude_import_reads_when_agents_absent_and_fallback_disabled`
|
||||||
|
- `explicit_claude_import_reads_both`
|
||||||
|
- `agents_md_skipped_falls_back_to_claude`
|
||||||
|
- `global_claude_is_last_resort_after_instructions_md`
|
||||||
|
- `symlinked_duplicate_is_loaded_once` (canonical path; no fallback)
|
||||||
|
- `no_trim_within_scope_budget`
|
||||||
|
- `per_scope_budgets_are_independent`
|
||||||
|
- `trim_keeps_the_nearest_scope` (segments)
|
||||||
|
- `single_oversized_file_keeps_the_head`
|
||||||
|
- `zero_bytes_left_is_reported_as_skipped`
|
||||||
|
- `rules_share_the_project_scope_budget`
|
||||||
|
- `rules_block_is_head_trimmed_with_numbers`
|
||||||
|
- `static_guard_trims_by_the_overage`
|
||||||
|
- `static_guard_unsatisfied_terminates`
|
||||||
|
- `guard_trim_begins_at_residual`
|
||||||
|
- `unsatisfiable_only_above_guard`
|
||||||
|
- `static_prefix_guard_measures_tool_schemas`
|
||||||
|
- `stable_prefix_guard_ignores_memory_growth`
|
||||||
|
- `cache_returns_untrimmed_segments_across_budgets`
|
||||||
|
- `configured_instructions_are_read_fresh`
|
||||||
|
- `inline_instructions_stay_outside_cache`
|
||||||
|
- `relay_artifact_cap_unchanged`
|
||||||
|
- `refuse_policy_stops_on_trim_but_not_on_skip`
|
||||||
|
- `file_over_ceiling_is_skipped_not_trimmed`
|
||||||
|
- `file_max_bytes_zero_is_rejected`
|
||||||
|
- `empty_file_is_skipped`
|
||||||
|
- `rules_cap_is_reported`
|
||||||
|
- `unknown_window_uses_128k`
|
||||||
|
- `percent_and_guard_bounds_are_validated`
|
||||||
|
- `project_overlay_cannot_set_global_budget`
|
||||||
|
- `instructions_array_counts_toward_global`
|
||||||
|
- `startup_notice_is_a_system_cell_and_stderr`
|
||||||
|
- `model_notice_is_a_session_event`
|
||||||
|
- `resumed_session_reemits_notice`
|
||||||
|
- `context_report_shows_actions_and_budgets`
|
||||||
|
- update the existing 48 KiB tests to the new defaults.
|
||||||
|
|
||||||
|
## 6. Acceptance criteria
|
||||||
|
|
||||||
|
- Default behaviour: no content is trimmed while a source is within its scope budget; the
|
||||||
|
only losses are scope-budget trims, static-guard trims, and skips (file ceiling, empty,
|
||||||
|
rules cap) — each with a notice.
|
||||||
|
- `AGENTS.md` selection and the CLAUDE fallback match the truth table, including
|
||||||
|
skip-as-absent and `claude_fallback = false`; `project_instruction_imports` still works for
|
||||||
|
co-reading.
|
||||||
|
- Global and project budgets are independent; a huge project does not shrink the global; rules
|
||||||
|
are charged to project; configured `instructions` entries are charged to global.
|
||||||
|
- Any loss (`trimmed`/`skipped`/`refused`) is reported to the user, to the model, and in
|
||||||
|
`/context`, with path, scope, source, bytes, tokens and limit kind.
|
||||||
|
- No silent drop remains: rules cap, empty files, fall-through substitution, symlink
|
||||||
|
duplicates and per-file skips are all in the contract; the guard has a terminal
|
||||||
|
unsatisfiable branch and trims by a defined overage.
|
||||||
|
- All existing tests pass; the new tests above cover every branch; the old 48 KiB tests are
|
||||||
|
updated.
|
||||||
|
|
||||||
|
## 7. Risks
|
||||||
|
|
||||||
|
- **Prompt-cache and cost:** a larger static prefix changes the cached prefix and increases
|
||||||
|
cost per request; the PR must include a measured estimate.
|
||||||
|
- **Upstream acceptance:** the maintainers said the absolute ceiling is their decision
|
||||||
|
(#6526); 100k tokens per scope is 6–8× the current 48 KiB. Present the defaults and the
|
||||||
|
knobs, and be ready to keep the current default.
|
||||||
|
- **CLAUDE fallback is a policy change:** a repo with only `CLAUDE.md` previously got no
|
||||||
|
project instructions; now it does. Migration note and `claude_fallback = false` escape.
|
||||||
|
- **Small windows:** at 32k the residual is ~8,000 tokens for skills/tool schemas; exceeding
|
||||||
|
it starts guard trimming, and only non-instruction static content above ~21,400 tokens is
|
||||||
|
`unsatisfiable`. The PR should include a 32k measurement and may lower the default percents
|
||||||
|
for small windows.
|
||||||
|
- **Estimator drift:** tokens are estimated, not counted; the notice must say "estimated"
|
||||||
|
where it matters.
|
||||||
|
- **Refactor size:** moving configured `instructions` into the assembly and keeping segments
|
||||||
|
on `ProjectContext` touches the loader, cache and prompt builder; the PR should be split
|
||||||
|
into a behaviour-preserving refactor and the budget change if maintainers prefer.
|
||||||
|
|
||||||
|
## 8. Out of scope
|
||||||
|
|
||||||
|
- Skills index budgets and truncation (#7073).
|
||||||
|
- Skill installation/disable policy (#7066).
|
||||||
|
- Compaction engine changes.
|
||||||
|
- Moving instructions from the system prompt into a synthetic message (decided against for
|
||||||
|
this PR).
|
||||||
|
|
||||||
|
## 9. PR framing
|
||||||
|
|
||||||
|
- Reference OpenCode's selection model with source links (`opencode.ai/docs/rules`,
|
||||||
|
`anomalyco/opencode` `config/plugin/instruction.ts`, `session/instructions.ts`) and Claude
|
||||||
|
Code's no-truncation model (`code.claude.com/docs/en/memory`).
|
||||||
|
- Include the measured evidence from §1.7, the effective-budget table from §3.2, and the
|
||||||
|
cost/cache estimate.
|
||||||
|
- State the behaviour changes (CLAUDE fallback; 48 KiB → per-scope budgets; a single
|
||||||
|
oversized file now keeps its head instead of its tail; configured `instructions` entries
|
||||||
|
move from `prompts.rs` rendering into the instruction assembly; the guard covers the stable
|
||||||
|
prefix only) and the migration path (`claude_fallback = false`, `[instruction_budget]`).
|
||||||
|
- Use `Refs #6526`, not `fix`/`close`. Maintainers land verified slices on `main`; this branch
|
||||||
|
is a contributor branch, not a maintainer PR branch.
|
||||||
|
|
||||||
|
## 10. Implementation notes (v5)
|
||||||
|
|
||||||
|
What landed, and where it differs from the plan above. The differences are
|
||||||
|
places where the plan cited a tree that has since changed, or where the plan's
|
||||||
|
mechanism did not exist; none of them changes the behaviour contract in §3.
|
||||||
|
|
||||||
|
- **Where the code lives.** Per-scope budgets, segments, notices, trim
|
||||||
|
markers, and the static-guard decision are in
|
||||||
|
`crates/tui/src/project_context/budget.rs`. Loader selection and the
|
||||||
|
assembly are in `crates/tui/src/project_context.rs`; `types.rs` carries the
|
||||||
|
segments and notices on `ProjectContext`. Tests for the behaviour named in
|
||||||
|
§5.9 are in `project_context/instruction_budget_tests.rs` (selection, skips,
|
||||||
|
trims, rules, configured entries, `refuse`), `budget.rs` (budget math and the
|
||||||
|
guard decision), `prompts.rs` (stable-prefix measurement, configured-entry
|
||||||
|
rendering), `lib.rs` (project overlay) and `config.rs`.
|
||||||
|
- **The "first-turn synthetic message" mechanism** named in §3.6 does not exist
|
||||||
|
in this tree (`crates/tui/src/core/agent.rs` and `announce_active_skill` are
|
||||||
|
gone). The equivalent is the runtime-provenance synthetic message the LSP
|
||||||
|
diagnostics path already uses
|
||||||
|
(`runtime_text_message_with_turn_metadata(..., UserInputProvenance::Runtime)`),
|
||||||
|
delivered once from turn preparation. It is part of the transcript, so it is
|
||||||
|
logged (model-visible ⇒ logged) and a resumed session already has it in
|
||||||
|
history — that replaces the separate persisted event.
|
||||||
|
- **The static guard** runs in turn preparation rather than
|
||||||
|
`core/engine/preview.rs`, because that is the first point where the pinned
|
||||||
|
prefix and the real tool catalog are known together. It measures the first
|
||||||
|
system block plus the tool schemas (`prompts::measure_static_prefix`), trims
|
||||||
|
the largest scope by the overage, recomposes and re-pins once under a
|
||||||
|
declared reason, and classifies the result with the pure
|
||||||
|
`static_guard_outcome`. `compose_stable_system_prompt` now has an
|
||||||
|
assembly-returning sibling used by the guard.
|
||||||
|
- **`policy = "refuse"`** is enforced at session start (TUI and `exec`) by
|
||||||
|
`project_context::startup_instruction_notice`, which returns an error for a
|
||||||
|
trim loss or a rules-budget loss. Skips stay skips. The engine itself never
|
||||||
|
refuses: by the time it runs, the session has already started, and the guard
|
||||||
|
reports `static_guard_unsatisfied` and continues. **Known limit:** the
|
||||||
|
embed/ACP/app-server surfaces do not run the startup gate, so `refuse` is a
|
||||||
|
no-op there — they trim and report like `trim`.
|
||||||
|
- **Symlinks.** §3.1 assumed the project loader follows them. It does not:
|
||||||
|
`fs_confined::open_read` refuses a link that leaves the scope root, so a
|
||||||
|
project file linked to the user's global file is now recognised as a
|
||||||
|
**duplicate** before the read (the content is already loaded from the wider
|
||||||
|
scope), and any other symlinked instruction file is *skipped with a notice*
|
||||||
|
rather than followed. That refusal is pre-existing sandbox behaviour; this
|
||||||
|
change only makes the duplicate case honest instead of an unreadable-file
|
||||||
|
error.
|
||||||
|
- **Foreign fragments** (`.cursorrules`, `.clinerules`, …) keep their own
|
||||||
|
bounded-fragment cap and are not charged to the project scope; they are
|
||||||
|
reported in `/context` and named as a known limitation in `budget.rs`.
|
||||||
|
- **Window used by the startup line.** The TUI/exec startup notice resolves
|
||||||
|
budgets for the route window they are about to launch with
|
||||||
|
(`route_context_window_tokens` from the app's effective model, or from the
|
||||||
|
validated CLI route in `exec`); the 128k default is only a fallback for a
|
||||||
|
caller that has no route. A first pass used that default unconditionally,
|
||||||
|
which could report a trim that would not happen and, under
|
||||||
|
`policy = "refuse"`, stop a session that would have fitted.
|
||||||
|
- **The user/model channels** are: a `HistoryCell::System` cell at TUI start,
|
||||||
|
`eprintln!` for `exec` text output, one runtime-provenance transcript
|
||||||
|
message for the model, and per-source rows in `/context report`. The notice
|
||||||
|
is recomputed deterministically from files + config, so a resumed session
|
||||||
|
states the same losses.
|
||||||
|
- **`/context report`** rows cover the file-based sources and the resolved
|
||||||
|
budgets; the TUI variant passes no configured `instructions` paths (the App
|
||||||
|
does not hold the config), so configured entries appear only in the headless
|
||||||
|
report and in the startup notice. `exec` prints the notice for text output
|
||||||
|
only; a JSON consumer reads the same losses from `/context report`.
|
||||||
|
- **Build note for this environment.** The shared cargo registry and target
|
||||||
|
directory are read-only inside the session sandbox, so the verification
|
||||||
|
build used an isolated `CARGO_HOME`/target under `/tmp` (`cargo check -p
|
||||||
|
codewhale-tui`), not the shared target dir. The `codewhale-tui` test binary
|
||||||
|
is the long pole: full codegen for that crate did not finish inside this
|
||||||
|
session's time budget, so test *execution* is the one gate still open here;
|
||||||
|
the crate and its `cfg(test)` code were typechecked instead.
|
||||||
@@ -29,7 +29,10 @@ Codewhale 有多个指令层级(instruction surfaces)。它们刻意保持
|
|||||||
- **内置全局宪章(Bundled global Constitution)**——编译进二进制的基础规则。它是每个会话的默认底线。
|
- **内置全局宪章(Bundled global Constitution)**——编译进二进制的基础规则。它是每个会话的默认底线。
|
||||||
- **用户全局宪章(User-global constitution)**——常规引导式设置的产物。用 `/constitution` 或 `/setup` 管理;Codewhale 把结构化数据存放在 `$CODEWHALE_HOME/constitution.json`(默认 `~/.codewhale/constitution.json`),并渲染成独立的 `<codewhale_user_constitution>` 散文块(prose block)。它可以表达偏好和停止条件,但不会改变运行时审批策略、沙箱、shell、网络、信任或 MCP 权限。
|
- **用户全局宪章(User-global constitution)**——常规引导式设置的产物。用 `/constitution` 或 `/setup` 管理;Codewhale 把结构化数据存放在 `$CODEWHALE_HOME/constitution.json`(默认 `~/.codewhale/constitution.json`),并渲染成独立的 `<codewhale_user_constitution>` 散文块(prose block)。它可以表达偏好和停止条件,但不会改变运行时审批策略、沙箱、shell、网络、信任或 MCP 权限。
|
||||||
- **仓库本地宪章(Repo-local constitution)**——可选的 `.codewhale/constitution.json` 项目策略,见下文。
|
- **仓库本地宪章(Repo-local constitution)**——可选的 `.codewhale/constitution.json` 项目策略,见下文。
|
||||||
- **`AGENTS.md`**——跨智能体**项目指令**(散文)。这是"智能体应如何在这个仓库工作"的规范文件。运行 `/init` 生成一份。为其他工具编写的指令文件(`CLAUDE.md`、`.claude/instructions.md`、`.cursorrules`、`.github/copilot-instructions.md` 等)只有在你通过顶层配置项 `project_instruction_imports` 选择导入时才会被读取,例如 `["claude"]`、`["cursor"]` 或 `["all"]`(见 `config.example.toml`);发现未加载的此类文件时,Codewhale 会给出提示。
|
- **`AGENTS.md`**——跨智能体**项目指令**(散文)。这是"智能体应如何在这个仓库工作"的规范文件。运行 `/init` 生成一份。为其他工具编写的指令文件(`CLAUDE.md`、`.claude/instructions.md`、`.cursorrules`、`.github/copilot-instructions.md` 等)默认不读取;通过顶层配置项 `project_instruction_imports` 选择导入,例如 `["claude"]`、`["cursor"]` 或 `["all"]`(见 `config.example.toml`);发现未加载的此类文件时,Codewhale 会给出提示。**例外(`#6526`)**:某目录没有 `AGENTS.md`(也没有 Codewhale 自己的 `instructions.md`)时,默认回退读取该目录的 `CLAUDE.md`;`claude_fallback = false` 可关闭。
|
||||||
|
|
||||||
|
> **说明(2026-10-25)**:指令预算表 `[instruction_budget]`(#6526)——按 `global` / `project` 两个作用域分别用**估算 token** 设上限,并在会话开始时报告每一次裁剪、跳过和拒绝——尚未翻译到本文档。权威说明见英文版 `docs/CONFIGURATION.md` 的 “Instruction budget” 一节;`config.example.toml` 里有完整示例。
|
||||||
|
> The `[instruction_budget]` section (per-scope token budgets with a trim/skip/refuse notice) is not translated yet; see the English `docs/CONFIGURATION.md`.
|
||||||
- **记忆与交接(Memory and handoffs)**——被召回的状态。有用,但权威低于宪章和项目指令。
|
- **记忆与交接(Memory and handoffs)**——被召回的状态。有用,但权威低于宪章和项目指令。
|
||||||
|
|
||||||
### 管理用户全局宪章(`/setup` 和 `/constitution`)
|
### 管理用户全局宪章(`/setup` 和 `/constitution`)
|
||||||
|
|||||||
Reference in new issue
Block a user