fix(agent): resolve a forced skill by the same rule as a forced snippet - #810
Conversation
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
There was a problem hiding this comment.
Automated approval for maintainer PR
All automated quality gates passed. See SECURITY_CONTROLS.md for compensating controls.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #810 +/- ##
============================================
- Coverage 87.25% 87.23% -0.03%
- Complexity 9761 9770 +9
============================================
Files 571 571
Lines 31597 31599 +2
============================================
- Hits 27570 27565 -5
- Misses 4027 4034 +7
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
ADR-166 said the skill half needed no counterpart. That was true of the path it was judging — the resume — and not of the two that compose a prompt. ADR-175 amends it and ADR-166 gains the matching :Amended:. The decision is derived from ADR-166's own sentences rather than being a new one: findByUids() is "the lookup for a prompt being assembled now", and "a fresh run that forces it gets it through the active-only lookup like any other". A queued run has composed nothing at enqueue, so dequeuing it is that assembly. Recorded here rather than in its own PR because it amends a decision instead of taking one; the implementation follows in the next commit. Refs #781, #777 Signed-off-by: Sebastian Mendel <sebastian.mendel@netresearch.de>
SkillRepository gains findByUids() (enabled only) and findExistingByUids() (existence), mirroring the snippet pair. Both keep the caller's order and the deleted restriction. The class is @internal, so no frozen surface moves. The three call sites stop disagreeing: - ToolPlaygroundController::resolveForcedSkills() and AgentRunRequestCodec::skillsByUids() compose now, so they filter on enabled. A run queued with a skill disabled before it starts runs without it, as it already did for a snippet (#781). - ToolLoopService::augmentationFrom() re-gates text already sent, so it keeps existence semantics — and picks up the caller's order, which it did not have. It iterated findAll() and so returned name ASC, while the other two returned uid order. On an equal data class the later source in the fold wins, so the same run named one skill in a refusal before it suspended and another after (#777). The codec docblock claiming that forcing overrides the global toggle is replaced. No record decided that and the picker does not offer it — availableSkills() lists enabled skills only. Tests: ten functional cases against real rows, on a fixture whose names sort against its uids so an order assertion cannot pass under both rules. Each was seen to fail first — dropping the enabled clause fails one, returning query order instead of the caller's fails three. The ToolLoopService unit test stubbed findAll(). It now stubs the existence lookup and additionally asserts which uids reach it, so the seam is covered here while the filtering is asserted against the database rather than against a double. Closes #781, closes #777 Signed-off-by: Sebastian Mendel <sebastian.mendel@netresearch.de>
c22d133 to
c3f3aac
Compare
|
Rebased onto
Verified after rather than assumed:
The review record still applies, and that is checked rather than claimed: the change set is One thing worth naming, because it was a real risk and not just a formality: #807 merged while this waited, and it added a test that reads the whole ADR corpus and asserts every
|
|
There was a problem hiding this comment.
Automated approval for maintainer PR
All automated quality gates passed. See SECURITY_CONTROLS.md for compensating controls.
|
Review record — no Copilot review exists for this diff (monthly, account-wide quota exhausted), so The issue was wrong about where the defect lives, and the docblock was the trap. #781 placed the disagreement inside The direction is ADR-166's, not mine. It writes the rule twice — Both new assertions were seen to fail. Dropping the An existing unit test broke and was treated as a signal, not noise. It stubbed Post-rebase: change set byte-identical at Not in this PR, deliberately: a forced source dropped before a run starts is silent — now uniformly so. That is #809, split out because making it visible needs a place to show it and a decision on whether a queued run should refuse instead. Merging on that basis. |



Two issues in the same three functions. #781 is a forced skill and a forced snippet answering the same question differently; #777 is the same forced skill set being ordered two ways.
What the issues got wrong, and what the code says
#781 described the disagreement as living inside
AgentRunRequestCodec. It is wider: three places rebuild a forced skill set from persisted uids, and the playground has the same disagreement as the codec.AgentRunRequestCodec::skillsByUids()also carried a docblock saying the missingenabledfilter was deliberate — "forcing a skill overrides its global toggle, the same semantics the playground's force-inject control has". That reads like an intended rule, so it was checked rather than taken: no record decides it, and the playground does not behave that way —availableSkills()lists enabled skills only, so a disabled skill can reach the forced set only from a stale form or a hand-built body. The sentence described neither an intent nor the thing it claimed to copy.The decision comes from ADR-166, not from me
ADR-166 already wrote the rule down twice.
findByUids()is "the lookup for a prompt being assembled now, and a snippet an operator switched off must not enter one", and "a fresh run that forces it gets it through the active-only lookup like any other".Its "the skill half was already correct" is true of the path it was judging — the resume — and says nothing about the two that compose. So the fix direction is not a coin toss between #781's two options; the record picks one, and this change applies it to the path ADR-166 never looked at. ADR-175 records that and ADR-166 gains the matching
:Amended:.ToolPlaygroundController::resolveForcedSkills()findByUids()— enabled onlyAgentRunRequestCodec::skillsByUids()findByUids()— enabled onlyToolLoopService::augmentationFrom()findExistingByUids()— existenceThe ordering half (#777)
The resume path iterated
findAll()and so returned$defaultOrderings,name ASC; the two composition paths returned the persisted uid order.InputContextClassification::withStricter()keeps the later source on an equal data class, and that source's name is what the refusal message and the governance row carry — so one run blamed one skill before it suspended and another after it resumed. Same ceiling, same outcome, different name in the audit. All three now use the caller's order.Verification
Ten functional cases against real rows. The fixture is its own and its names sort against its uids —
21is "Zulu",22is "Alpha" — so an order assertion distinguishes the caller's order fromname ASCinstead of passing under both.Each assertion was seen to fail before being trusted:
enabledclause fromorderedByUid()findByUidsSkipsDisabledSkillsfailsfindExisting…order casesRestored, green,
git statusclean.One existing unit test broke, and that was a real signal rather than noise.
ToolLoopServiceTest::resumeRebuildsTheForcedSkillsToostubbedfindAll(). Its assertion — a disabled skill still resolves on resume — is exactly what must not regress, so the double was moved to the new lookup rather than the test being relaxed. It now also asserts which uids reach that lookup, so the seam is covered here while the filtering is proven against the database, where a stub cannot flatter it.Gates:
phpstanlevel 10 clean,unit7137 pass,fuzzypass,functional -d sqliteon the touched classes 38 pass,cglandrector -nboth stable at PHP 8.2 after applying.Deliberately not in this PR
A forced source that disappears before a run starts is silent — now uniformly, where before it was silent for snippets and absent for skills. #781 asked for it to be visible. That needs somewhere to show it and a decision on whether a queued run should refuse instead, so it is filed as #809 rather than guessed at here.
The ADR is in this PR rather than ahead of it because it amends a decision instead of taking one.
Closes #781, closes #777