Skip to content

load_skill silently truncates skill content over 32 KiB with no warning, log, or marker #4163

Description

@jkeskikangas

Describe the bug

load_skill silently truncates any skill content over MAX_SKILL_BODY_BYTES (32 KiB) and returns the truncated text as if it were complete. There is no log line, no error, no in-band marker, and is_error stays false — so neither the model nor the caller can tell a truncated skill from a whole one.

The failure mode is a skill that loads successfully and behaves subtly wrong. Instructions in the tail of a SKILL.md — which is often where the output format, the refusal rules, or the verification checklist live — simply are not there, and nothing anywhere says so.

crates/buzz-agent/src/builtin.rs (both the SKILL.md path and the supporting-file path):

let output = if output.len() > MAX_SKILL_BODY_BYTES {
    truncate_at_boundary(&output, MAX_SKILL_BODY_BYTES).to_owned()
} else {
    output
};

returned as:

ToolResult {
    provider_id: String::new(),
    content: vec![ToolResultContent::Text(output)],
    is_error: false,
}

Constant at crates/buzz-agent/src/hints.rs:7:

pub const MAX_SKILL_BODY_BYTES: usize = 32 * 1024;

The boundary handling itself is fine — truncate_at_boundary keeps the output valid UTF-8. The issue is purely that the truncation is unreported.

Steps to reproduce

  1. Create a skill whose body exceeds 32 KiB, with a sentinel line at the very end:

    mkdir -p .agents/skills/oversized
    {
      printf -- '---\nname: oversized\ndescription: Reproduces silent truncation of oversized skill bodies.\n---\n\n'
      for i in $(seq 1 900); do
        printf 'Filler line %s: padding to push this body past the 32 KiB cap.\n' "$i"
      done
      printf '\n## SENTINEL\n\nIf you can read this line, the skill was NOT truncated.\n'
    } > .agents/skills/oversized/SKILL.md
    
    wc -c .agents/skills/oversized/SKILL.md   # ~57 KB, comfortably over 32768
  2. Start an agent in that working directory so the skill is discovered (.agents/skills is the first entry in SKILL_DIRS).

  3. Ask the agent to call load_skill with oversized, then ask it to quote the ## SENTINEL section.

Expected behavior

One of:

  • the truncation is reported — a tracing::warn! on the server side, and an explicit in-band marker appended to the returned text (e.g. \n\n[truncated: N of M bytes shown]) so the model knows its instructions are incomplete; or
  • oversized skills are rejected at discovery with a clear error naming the file and its size, so the author fixes it rather than shipping a half-loaded skill.

Either is fine. Silence is the problem.

Actual behavior

The agent reports that the skill loaded, and the ## SENTINEL section is absent. Nothing in the logs, the tool result, or the agent's own view indicates anything was dropped.

Version and platform

  • Buzz version: relay crate 0.2.0; verified against main @ 4632c55041c5d423d572a6f6411bb7b279c26f67
  • OS: macOS (arm64) — but this is platform-independent; the behaviour is in buzz-agent, read from source at the commit above

Logs / additional context

There are no logs — that is the bug.

Two notes that may help scoping:

  1. The same truncation applies to supporting files read through load_skill, not just SKILL.md, so a skill can also lose the tail of a reference document it explicitly asked to read.
  2. MAX_HINTS_BYTES (128 KiB, for the AGENTS.md chain) has the same shape and is presumably worth the same treatment, though the consequence there is milder since the chain is additive rather than a single authored document.

I'm reporting this from a source read rather than a captured session — I don't have a model credential wired to this checkout, so I could not paste a real transcript. The reproduction above is what I'd expect a maintainer with a working agent to see in under five minutes; happy to be corrected if the runtime path differs from what builtin.rs suggests.

A suggested minimal fix, if useful: keep the truncation, append a marker, and log once per truncated load —

let output = if output.len() > MAX_SKILL_BODY_BYTES {
    tracing::warn!(
        skill = %name,
        bytes = output.len(),
        limit = MAX_SKILL_BODY_BYTES,
        "skill content truncated"
    );
    let mut t = truncate_at_boundary(&output, MAX_SKILL_BODY_BYTES).to_owned();
    t.push_str(&format!(
        "\n\n[truncated: {} of {} bytes shown]",
        t.len(),
        output.len()
    ));
    t
} else {
    output
};

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions