fix(early-stop): decide skill activation on the tool call, not its result#43
Open
mohsen-uipath wants to merge 1 commit into
Open
fix(early-stop): decide skill activation on the tool call, not its result#43mohsen-uipath wants to merge 1 commit into
mohsen-uipath wants to merge 1 commit into
Conversation
…sult Armed skill-activation rows that clearly trigger a skill were not early-stopping and burned their full turn budget. Two facts combined to break the stop at scale: 1. The live watcher evaluated armed criteria on the tool *result* (`ToolEndEvent`). When a turn is cut short (e.g. a timeout), the `Skill` tool call is left unresolved: the deadline check discards the pending result and `finalize` force-closes the call as UNRESOLVED, which the watcher (correctly) ignores. So the decision, though fully determined by the call itself, was never observed and the run kept going. 2. The final `skill_triggered` scorer used "any engagement" while the live verdict used "first engagement". Because the run kept going, the agent could engage additional skills, and each extra engagement was counted as a false positive against its own criterion. This change: - (early_stop) Evaluates armed criteria on the tool *call* (`ToolStartEvent`) in addition to the result. For an observable criterion the verdict is fully determined by the call's inputs (which skill / which command), so the watcher latches the instant the call is dispatched. The agent polls `should_stop` immediately after dispatching each message, so the loop breaks before a cut-short turn can strip the result. The in-flight call (not yet in the collector, which reduces commands from `ToolEndEvent`) is appended to the partial trajectory and reported at `tool_call_index + 1`; the counter still advances on the resolved `ToolEndEvent`. UNRESOLVED ends remain ignored, so an orphaned tool closed post-loop can never latch a false stop. - (skill_triggered) Scores the final check on the FIRST engaged skill, matching the live verdict, via a shared `_first_engaged_skill_names` helper. A second skill invoked alongside or after the first is no longer counted as a competing activation, so it is not a false positive. The live verdict and the authoritative score now agree by construction, whether or not the run stopped early. Removes the now-unused `_engaged_skill` helper. - (formatting) Skips the SDK `RateLimitEvent` (an out-of-band throttling notice) in `format_messages` alongside `StreamEvent`, so it no longer logs a spurious "unhandled SDK message type" warning. Cosmetic; does not affect scoring. Adds unit coverage for the tool-call decision (call fires before/without a result, latches before a later UNRESOLVED end, index accounting), the orchestrator cut on a resultless call, first-engagement scoring including the parallel-call case, and the rate-limit event skip. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
Claude finished @mohsen-uipath's task in 1m 12s —— View job Code Review in Progress
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Armed skill-activation rows that clearly trigger a skill were not early-stopping and burned their full turn budget. Two facts combined to break the stop at scale:
ToolEndEvent). When a turn is cut short (e.g. a timeout), theSkilltool call is left unresolved — the deadline check discards the pending result andfinalizeforce-closes the call asUNRESOLVED, which the watcher (correctly) ignores. So the decision, though fully determined by the call itself, was never observed and the run kept going.skill_triggeredscorer used "any engagement" while the live verdict used "first engagement". Because the run kept going, the agent could engage additional skills, and each extra engagement was counted as a false positive against its own criterion.Changes
orchestration/early_stop.py— decide on the tool call. The watcher now evaluates armed criteria onToolStartEvent(the call) in addition to the result. For an observable criterion the verdict is fully determined by the call's inputs (which skill / which command), so the watcher latches the instant the call is dispatched. The agent pollsshould_stopimmediately after dispatching each message, so the loop breaks before a cut-short turn can strip the result. The in-flight call (not yet in the collector, which reduces commands fromToolEndEvent) is appended to the partial trajectory and reported attool_call_index + 1; the counter still advances on the resolvedToolEndEvent.UNRESOLVEDends remain ignored, so an orphaned tool closed post-loop can never latch a false stop.criteria/skill_triggered.py— first-engagement scoring. The final check now scores the FIRST engaged skill, matching the live verdict, via a shared_first_engaged_skill_nameshelper. A second skill invoked alongside or after the first is no longer counted as a competing activation, so it is not a false positive. The live verdict and the authoritative score now agree by construction, whether or not the run stopped early. Removes the now-unused_engaged_skillhelper.formatting.py— quiet the rate-limit warning. Skips the SDKRateLimitEvent(an out-of-band throttling notice) informat_messagesalongsideStreamEvent, so it no longer logs a spurious "unhandled SDK message type" warning. Cosmetic; does not affect scoring.Tests
Adds coverage for:
UNRESOLVEDend,tool_call_indexaccounting);All existing early-stop / skill-triggered tests pass unchanged.
Verification
ruff format+ruff checkcleanpyright— 0 errors