feat(skill-use): progressive disclosure, as an arm and not a switch

Trigger — did the agent reach for the skill when it applied? — cannot be
measured while every body is inlined into the prompt. Nothing was reached
for. `skill_use` has been reporting `NotObservable` for that reason, and it
was right to.

The skills door made retrieval possible; this makes it a delivery arm.
`index` sends each pinned skill's name, description, `when_to_use` and the
uri that returns its body, and the agent fetches what it judges relevant.
`inline` is unchanged and stays the default.

An A/B rather than a switch, because `index` can only cost Compliance: under
`inline` the procedure sits in front of the model whether or not it noticed
it applied. Trading a measured axis for an unmeasured regression in another
is not an improvement, so both arms stay runnable and the arm is recorded on
the mission row.

Three things the mechanism refuses to do:

- `index` without a door falls back to `inline`. An index names bodies and
  says how to fetch them; with no `clawmates_skills` server reachable that is
  a list of dead ends, and it fails as an agent ignoring its skills rather
  than as a missing config. `install_skills_door` now returns whether it
  installed, because the caller needs the answer and not just the log line.

- The scorer reads the arm off the recorded PROMPT, not off the mission row.
  The row says what the mission is configured to do now; the score is being
  computed against a turn that ran then.

- Under `index`, a skill that was offered and never read is a Fail, not the
  inline arm's `NotObservable` — but only where the skill had a checkable
  consequence in that phase. Reusing the inline text would have said "this
  skill was inlined into the prompt" about a skill whose body was never sent,
  and scoring a real miss as a structural blind spot is the failure this
  measurement already made once.

The arm is per mission (`config.skill_delivery`), not only per deployment.
Both arms run against one server process; restarting between them would put a
confound in the comparison that the numbers would not show.

829 tests, 108 binaries, green.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-25 07:17:55 -05:00
co-authored by Claude Opus 5
parent b58f0347e6
commit f52cff3e04
11 changed files with 735 additions and 48 deletions
+10 -1
View File
@@ -1141,7 +1141,16 @@ async fn launch_phase(
// `topology_exec`, with the running node's own role, and appending here too
// would put every crew member's skills in every turn twice.
let task_with_skills = match phase_skills_text(pool, mission_id).await {
Some(skills) => crate::topology_exec::compose_turn_prompt(&task, Some(&skills)),
// Always `Inline` here, and not because it is the default: the solo
// tiers get no skills door (`install_skills_door` runs only for a
// mission with its own container), so an index would list uris nothing
// in the VM can fetch. When the microVM tier folds onto
// `container_tool_hooks` this becomes a real choice; today it is a fact.
Some(skills) => crate::topology_exec::compose_turn_prompt(
&task,
Some(&skills),
crate::skill_delivery::Mode::Inline,
),
None => task.clone(),
};