Repository navigation
Conversation
… let a retry take refusal text back An OpenAI-shaped stream can end in a refusal two ways, and the hand-rolled text loops caught neither in HF Inference or llamacpp-server: delta.refusal was read only by the tool-calling helper, and finish_reason "content_filter" with an empty body completed as a normal, empty answer. - createChatRefusalReader (provider-utils) reads one choice: it emits delta.refusal and, when a stream ends in content_filter with no refusal text, one generic refusal. accumulateOpenAIChatStream, HFI's three text run-fns, llamacpp-server's text and tool-calling run-fns, and the nine xAI, DeepSeek and OpenRouter text loops all use it. - A text-retract naming the reserved refusal port takes back refusal text a failed attempt streamed (StreamProcessor, StreamEventAccumulator), so a retried call does not append to it or keep it after the retry succeeds. - Text streamed before a refusal is left as streamed. Ollama's wire format has no refusal signal (done_reason is a plain string), so there is nothing to map there. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015q4aqYW6CtC4bjAFbnDsJb
…pin them in the tool-turn contract - AgentRoundAttempt forwards a round's refusal as it arrives and, like the round's text, takes a failed attempt's back with a text-retract on the refusal port before the retry, so an agent turn reports the refusal of the round that stood. AgentRoundContext gains onRefusal for a host round runner. AgentTask's output now carries refusal where it was dropped before. - runToolTurnConformance gains an optional refusal block: a stated refusal, a reply withheld by the content filter, and an ordinary answer with none. The chat-completions harnesses write both shapes; the local providers whose wire has no refusal field opt out with a reason. - The xAI, DeepSeek and OpenRouter structured-generation run-fns read refusals through createChatRefusalReader too, so a content_filter ending is reported. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015q4aqYW6CtC4bjAFbnDsJb
This branch has not been deployed
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
Adds comprehensive support for reporting model refusals across the AI provider ecosystem. OpenAI-shaped providers stream refusals in two ways:
delta.refusalstrings andfinish_reason: "content_filter"endings with empty bodies. Previously these were silently dropped; now they're emitted asrefusalstream events on a reserved output port, allowing downstream consumers to distinguish between "the model said no" and "the model gave an empty answer."Key Changes
New
createChatRefusalReader()helper (@workglow/ai/provider-utils): Reads OpenAI-shaped chat completion choices and emits refusal events. Handles both explicitdelta.refusalstrings and barecontent_filterendings (emitting a generic refusal message when no text was provided).Refusal support in agent rounds:
AgentRoundContextnow includesonRefusal(refusal, category?)to allow run-fns to report refusals. Failed attempts' refusals are retracted before retry viatext-retractevents on the reservedrefusalport.Stream event types: Added
StreamRefusaltype to@workglow/task-graphfor the new event shape{ type: "refusal", refusal: string, category?: string }.Provider implementations updated: All OpenAI-shaped providers (DeepSeek, OpenRouter, xAI, Hugging Face Inference, LlamaCpp Server) now use
createChatRefusalReader()instead of manually tracking refusal state.Test conformance suite: New
refusalBlock()assertion in@workglow/test-contractvalidates that providers correctly report stated refusals, content-filtered responses, and ordinary answers without false refusals.Stream accumulation:
StreamProcessorandStreamEventAccumulatorhandle refusal retractions on the reservedrefusalport, allowing retried rounds to take back earlier attempts' refusals.Implementation Details
REFUSAL_OUTPUT_KEYoutput port (not a declared data port), so retractions don't need routing or accumulator logic beyond text manipulation.content_filterfinish reason with nodelta.refusaltext emitsCONTENT_FILTER_REFUSAL_TEXT("The response was withheld by the provider's content filter.") as a generic refusal with category"output-refusal"."output-refusal"for content-filtered responses).