-
Notifications
You must be signed in to change notification settings - Fork 0
fix: accept event-driven routing exit #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
a9e177d
e44e81c
b41b72a
602e3c1
cd0ba30
218e847
dbda45b
9261a82
f2c697c
46af136
0d59344
3bb9edb
b4bca5d
76e66bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,8 @@ | |
| * KD-8 safe:只看"有无机械出口信号",零意图分类器。 | ||
| */ | ||
|
|
||
| import { hasEventDrivenExternalWaitExit } from '../final-routing-slot.js'; | ||
|
|
||
| /** Routing-tool substrings that count as a legitimate exit (持球/群发传球). */ | ||
| const ROUTING_TOOL_SUBSTRINGS = ['hold_ball', 'multi_mention'] as const; | ||
|
|
||
|
|
@@ -23,6 +25,8 @@ function hasRoutingToolCall(toolNames: readonly string[]): boolean { | |
| } | ||
|
|
||
| export interface RoutingExitInput { | ||
| /** Stored output text. Only the final routing slot is inspected for structural external-wait exits. */ | ||
| readonly text?: string; | ||
| /** Line-start @cat mentions parsed this turn (parseA2AMentions). */ | ||
| readonly lineStartMentions: readonly string[]; | ||
| /** Tool names invoked this turn (scan for hold_ball / multi_mention). */ | ||
|
|
@@ -36,13 +40,15 @@ export interface RoutingExitInput { | |
| /** | ||
| * True iff the turn has a legitimate routing exit (传球 / 持球 / 升级). | ||
| * Mirrors the suppression set of evaluateVoidHold + F177-G hook | ||
| * (line-start @, hold_ball, multi_mention, targetCats, co-creator). | ||
| * (line-start @, hold_ball, multi_mention, targetCats, co-creator, structural | ||
| * 2b external wait slot). | ||
| */ | ||
| export function hasValidRoutingExit(input: RoutingExitInput): boolean { | ||
| if (input.lineStartMentions.length > 0) return true; | ||
| if (input.structuredTargetCats.length > 0) return true; | ||
| if (input.hasCoCreatorLineStartMention) return true; | ||
| if (hasRoutingToolCall(input.toolNames)) return true; | ||
| if (hasEventDrivenExternalWaitExit(input.text)) return true; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a guarded reply contains Useful? React with 👍 / 👎.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in dbda45b ( What changed:
Red -> Green coverage:
Validation:
[砚砚/GPT-5.5🐾] |
||
| return false; | ||
| } | ||
|
|
||
|
|
@@ -74,6 +80,7 @@ export const REMEDIAL_PROMPT = | |
| '请只补一个出口,不要重做刚才的工作:\n' + | ||
| '- 传球:另起一行,行首独立写 @句柄(如 @opus48)\n' + | ||
| '- 持球等外部条件:调用 cat_cafe_hold_ball\n' + | ||
| '- 事件驱动外部等待(已有结构化回调 + EYES>0):另起一行写 External Wait: event-driven (<id>)\n' + | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a first pass has useful text but no exit, this new remedial option asks the model to return only Useful? React with 👍 / 👎.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a guarded cat first produces useful content but no route exit, this new prompt can now make the remedial call answer with only Useful? React with 👍 / 👎.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in |
||
| '- 升级co-creator:另起一行行首写 @co-creator'; | ||
|
|
||
| export function buildRemedialPrompt(): string { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -107,7 +107,11 @@ import { | |
| import { accumulateTextAggregate } from '../text-aggregation.js'; | ||
| import { formatA2AHandoffContent } from './a2a-handoff-label.js'; | ||
| import { extractContextEvalSignals } from './context-eval.js'; | ||
| import { validateRoutingSyntax } from './final-routing-slot.js'; | ||
| import { | ||
| hasEventDrivenExternalWaitExit, | ||
| stripTrailingCatSignatures, | ||
| validateRoutingSyntax, | ||
| } from './final-routing-slot.js'; | ||
| import { buildBriefingMessage } from './format-briefing.js'; | ||
| import { buildRemedialPrompt, hasValidRoutingExit, shouldRemediateRouting } from './guards/routing-guard-remedial.js'; | ||
| import { extractRichFromText, isValidRichBlock } from './rich-block-extract.js'; | ||
|
|
@@ -175,13 +179,22 @@ function stripMarkdownRoutePrefix(line: string): string { | |
| } | ||
|
|
||
| function normalizeRouteOnlyRemedialText(text: string): string | null { | ||
| const lines = text | ||
| const lines = stripTrailingCatSignatures(text) | ||
| .trim() | ||
| .split(/\r?\n/) | ||
| .map((line) => stripMarkdownRoutePrefix(line)) | ||
| .filter((line) => line.length > 0); | ||
| if (lines.length !== 1) return null; | ||
| return ROUTE_ONLY_REMEDIAL_TEXT_RE.test(lines[0]) ? lines[0] : null; | ||
| const line = lines[0]!; | ||
| if (ROUTE_ONLY_REMEDIAL_TEXT_RE.test(line)) return line; | ||
| return hasEventDrivenExternalWaitExit(line) ? line : null; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the remedial model signs the route-only Useful? React with 👍 / 👎.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in |
||
| } | ||
|
|
||
| function buildRoutingAnalysisContent(storedContent: string, routingContent: string): string { | ||
| if (!routingContent.trim()) return storedContent; | ||
| if (!storedContent.trim()) return routingContent; | ||
| if (routingContent.trim() === storedContent.trim()) return storedContent; | ||
| return `${storedContent}\n\n${routingContent}`; | ||
| } | ||
|
|
||
| function collectStructuredTargetCatsFromInput(input: unknown): string[] { | ||
|
|
@@ -1483,6 +1496,7 @@ export async function* routeSerial( | |
| allRichBlocks: RichBlock[]; | ||
| a2aMentions: CatId[]; | ||
| hasCoCreatorLineStartMention: boolean; | ||
| routingContent: string; | ||
| streamEvents: AgentMessage[]; | ||
| }> => { | ||
| routingGuardAttempted = true; | ||
|
|
@@ -1717,6 +1731,7 @@ export async function* routeSerial( | |
| allRichBlocks: remedialAllRichBlocks, | ||
| a2aMentions: remedialA2aMentions, | ||
| hasCoCreatorLineStartMention: remedialHasCoCreatorLineStartMention, | ||
| routingContent: remedialRoutingContent, | ||
| // Exit-only remedials validate the original text instead of replacing it; surface it after validation. | ||
| streamEvents: preservesOriginalVisibleContent | ||
| ? [...visibleRemedialStreamEvents, ...originalVisibleStreamEventsForRemedialTurn] | ||
|
|
@@ -1732,6 +1747,7 @@ export async function* routeSerial( | |
| shouldRemediateRouting({ | ||
| needsGuard: needsServerRoutingGuard, | ||
| attempted: routingGuardAttempted, | ||
| text: '', | ||
| lineStartMentions: getRoutingExitLineStartMentions(), | ||
| toolNames: collectedToolNames, | ||
| structuredTargetCats: [...structuredTargetCats], | ||
|
|
@@ -1748,6 +1764,7 @@ export async function* routeSerial( | |
| noTextBlocksOverride = result.allRichBlocks; | ||
| if ( | ||
| !hasValidRoutingExit({ | ||
| text: result.routingContent, | ||
| lineStartMentions: getRoutingExitLineStartMentions(result.a2aMentions), | ||
| toolNames: collectedToolNames, | ||
| structuredTargetCats: [...structuredTargetCats], | ||
|
|
@@ -1765,6 +1782,7 @@ export async function* routeSerial( | |
| // F22: Extract cc_rich blocks from text (Route B fallback for non-MCP cats) | ||
| const { cleanText, blocks: textBlocks } = extractRichFromText(sanitized); | ||
| let storedContent = cleanText; | ||
| let routingAnalysisContent = storedContent; | ||
| let allRichBlocks = [...bufferedBlocks, ...textBlocks, ...streamRichBlocks]; | ||
|
|
||
| // F34-b: Resolve voice blocks (audio with text, no url) — Route B path. | ||
|
|
@@ -1799,6 +1817,7 @@ export async function* routeSerial( | |
| shouldRemediateRouting({ | ||
| needsGuard: needsServerRoutingGuard, | ||
| attempted: routingGuardAttempted, | ||
| text: storedContent, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When this text makes Useful? React with 👍 / 👎.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in |
||
| lineStartMentions: routingExitLineStartMentions, | ||
| toolNames: collectedToolNames, | ||
| structuredTargetCats: [...structuredTargetCats], | ||
|
|
@@ -1809,13 +1828,15 @@ export async function* routeSerial( | |
| for (const event of result.streamEvents) yield event; | ||
| await flushDeferredVoice(); | ||
| storedContent = result.storedContent; | ||
| routingAnalysisContent = buildRoutingAnalysisContent(storedContent, result.routingContent); | ||
| allRichBlocks = result.allRichBlocks; | ||
| a2aMentions = result.a2aMentions; | ||
| routingExitLineStartMentions = getRoutingExitLineStartMentions(a2aMentions); | ||
| routingExitHasCoCreatorLineStartMention = result.hasCoCreatorLineStartMention; | ||
|
|
||
| if ( | ||
| !hasValidRoutingExit({ | ||
| text: routingAnalysisContent, | ||
| lineStartMentions: routingExitLineStartMentions, | ||
| toolNames: collectedToolNames, | ||
| structuredTargetCats: [...structuredTargetCats], | ||
|
|
@@ -1851,7 +1872,7 @@ export async function* routeSerial( | |
| } | ||
| } | ||
| const phaseHResult = validateRoutingSyntax({ | ||
| text: storedContent, | ||
| text: routingAnalysisContent, | ||
| lineStartMentions: routingExitLineStartMentions, | ||
| toolNames: collectedToolNames, | ||
| structuredTargetCats: [...structuredTargetCats], | ||
|
|
@@ -2005,7 +2026,7 @@ export async function* routeSerial( | |
| // frustrationAutoIssueEligible=false but still need verdict-pass handoff guards. | ||
| options.verdictPassWarningEnabled !== false && | ||
| shouldWarnVerdictWithoutPass({ | ||
| text: storedContent, | ||
| text: routingAnalysisContent, | ||
| lineStartMentions: routingExitLineStartMentions, | ||
| toolNames: collectedToolNames, | ||
| structuredTargetCats: [...structuredTargetCats], | ||
|
|
@@ -2030,7 +2051,7 @@ export async function* routeSerial( | |
| }); | ||
| const verdictFireAttr: Record<string, string> = { | ||
| ...c2BaseAttr, | ||
| [TRIGGER]: detectMatchedVerdictKeyword(storedContent) ?? 'unknown', | ||
| [TRIGGER]: detectMatchedVerdictKeyword(routingAnalysisContent) ?? 'unknown', | ||
| }; | ||
| c2VerdictHintEmitted.add(1, verdictFireAttr); | ||
| c2VerdictWithoutPassCount.add(1, verdictFireAttr); | ||
|
|
@@ -2067,7 +2088,7 @@ export async function* routeSerial( | |
| // hold-claim message, so drilldown lands on the original content, not on the hint. | ||
| let pendingC2VoidHoldSampleTrigger: string | null = null; | ||
| const voidHoldEval = evaluateVoidHold({ | ||
| text: storedContent, | ||
| text: routingAnalysisContent, | ||
| toolNames: collectedToolNames, | ||
| lineStartMentions: routingExitLineStartMentions, | ||
| structuredTargetCats: [...structuredTargetCats], | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the final slot uses a URL as the callback identifier, e.g.
External Wait: event-driven (https://github.com/org/repo/pull/32), this callsfinalRoutingSlot(), which strips allhttp(s)URLs before the event-driven regex runs. That leaves an empty()and the new legal 2b exit is treated as missing, so guarded cats still get unnecessary remedial retries and the verdict/void-hold suppressions do not apply for URL-based PR/check ids. Match theExternal Waittemplate before URL removal or preserve URLs inside the parenthesized id.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in
218e847338e039aa2b84490b54d96be266f22b59.hasEventDrivenExternalWaitExit()now selects the final routing slot without URL stripping, soExternal Wait: event-driven (https://github.com/...)is accepted whilefinalRoutingSlot()still strips URLs for inline-mention validation. Red/green: URL-id regression failed before the fix and passes now; expanded routing guard suite 149/149;pnpm checkpassed.