Skip to content

fix(skills): skills__read returns full body in content, not just structuredContent#365

Merged
mgoldsborough merged 2 commits into
mainfrom
fix/skills-read-body-in-content
Jun 3, 2026
Merged

fix(skills): skills__read returns full body in content, not just structuredContent#365
mgoldsborough merged 2 commits into
mainfrom
fix/skills-read-body-in-content

Conversation

@mgoldsborough
Copy link
Copy Markdown
Contributor

Problem

skills__read returns only a one-line header to the model — e.g. <name> (L3 workspace) · loads: always — and the full skill body is missing. This blocks an in-platform agent from reading or auditing any skill it doesn't already have loaded, despite the tool description promising it "Returns the markdown body plus parsed manifest fields."

Root cause

The engine surfaces only a tool result's content to the model (engine.ts: modelOutput ?? boundToolResultForModel(extractTextForModel(result.content), …)). structuredContent reaches /mcp clients and the UI but never the in-process agent loop. skills__read put the body + manifest only in structuredContent and a one-line summary (summarizeRead) in content — so the body was structurally invisible to the agent.

This is a stale-by-omission bug, not a design choice:

  • summarizeList was already fixed for this exact constraint — its comment reads "…without depending on structuredContent (which the engine doesn't surface to the model)." read was never given the same treatment.
  • Sibling read tools files__read, files__read_pdf_pages, and conversations__get already embed their body in content. skills__read was the lone straggler.

Fix

  • Add renderRead: header → promised manifest fields → full markdown body, in content. Manifest leads and the body trails so a body exceeding MAX_TOOL_RESULT_CHARS (50k) is trimmed tail-first, keeping metadata intact (same small-first ordering as conversations__get).
  • structuredContent is unchanged — typed SkillsReadOutput still serves /mcp clients and the UI.
  • Trim SKILLS_READ_DESCRIPTION's field list to the fields actually returned (dropped version, allowed_tools, requires_bundles, metadata, which aren't on SkillDetail).
  • Update the stale summarize-family comment that claimed agents consume structuredContent.

Tests

Regression assertions in both the unit and integration read tests confirm the body and manifest fields reach the model-visible content (via extractText / the content text blocks), not only structuredContent — and that manifest precedes the body. Existing structuredContent assertions are retained.

bun run verify static gate (tsc strict, lint, format, cycles) is green; targeted skills tests pass (22/22). The one unrelated failure in the run (automations/markdown.test.ts — missing dompurify bundle-UI dep) reproduces on the untouched base and is environmental to the worktree.

…cturedContent

The engine surfaces only a tool result's `content` to the model;
`structuredContent` reaches /mcp clients and the UI but never the
in-process agent loop. `skills__read` put the skill body and manifest
only in `structuredContent` and a one-line header in `content`, so an
in-agent caller saw `"<name> (L3 workspace) · loads: always"` and could
not read the skill — despite the tool description promising "the markdown
body plus parsed manifest fields."

`summarizeList` was already corrected for this exact constraint (its
comment notes structuredContent "the engine doesn't surface to the
model"); `read` was the lone straggler. Sibling read tools `files__read`
and `conversations__get` already embed their body in `content`.

Add `renderRead`: header, then the promised manifest fields, then the
full markdown body. Manifest leads and body trails so a body over
MAX_TOOL_RESULT_CHARS is trimmed tail-first, keeping metadata intact
(same small-first ordering as conversations__get). `structuredContent`
is unchanged for typed consumers. Trim the description's field list to
the fields actually returned.

Regression tests assert the body and manifest reach the model-visible
content, not only structuredContent.
Review follow-up. `renderRead` inherited `summarizeRead`'s
status !== "active" guard, so the model-visible content omitted `status`
for active skills even though the description promises it — the same
description-vs-model-reality gap this PR exists to close. `status` is
always populated (`buildReadResult` defaults it to "active") and is a
primary audit signal, so emit it as an unconditional manifest field.

Drop status from the header line to avoid double-rendering on disabled
skills; add a sync-contract comment on `renderRead`'s explicit field
list (deliberate subset; keep aligned with the description and
SkillDetail.metadata). Test asserts `status: active` reaches the
model-visible content.
@mgoldsborough mgoldsborough added the qa-reviewed QA review completed with no critical issues label Jun 3, 2026
@mgoldsborough mgoldsborough merged commit 1f03afe into main Jun 3, 2026
5 checks passed
@mgoldsborough mgoldsborough deleted the fix/skills-read-body-in-content branch June 3, 2026 07:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

qa-reviewed QA review completed with no critical issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant