-
Notifications
You must be signed in to change notification settings - Fork 16
fix(prose): failed sends release held prose; opt-in round-scoped silencing #177
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
base: main
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| - A round whose explicit sends all **failed** no longer silences its prose: | ||
| the held prose is released to the locus and the turn's silence lifts. | ||
| New opt-in `proseSilencing: 'round'` scopes a send's silencing to its own | ||
| round, so an early send in a long tool-using turn no longer discards the | ||
| turn's closing prose (default `'turn'` is unchanged). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1012,6 +1012,9 @@ export class AgentFramework { | |
| * author sees the segment's fate one turn later. Cleared each fresh | ||
| * turn; budget restarts keep it. */ | ||
| private turnProseSuppressed: Map<string, number> = new Map(); | ||
| /** Per agent: tool call id -> whether its result was an error, for the | ||
| * round most recently handed back to the stream (failed-send release). */ | ||
| private roundToolErrors: Map<string, Map<string, boolean>> = new Map(); | ||
| /** A tool boundary injected fresh CONVERSATIONAL input (a real message — | ||
| * not a reaction or a system marker) into the live stream. Tells | ||
| * driveStream to clear sticky explicit-send suppression before handling | ||
|
|
@@ -6589,6 +6592,10 @@ export class AgentFramework { | |
| const membraneResults = currentState.toolResults.map(tc => | ||
| this.toMembraneToolResult(tc.id, tc.result, maxChars, spilled.get(tc.id)) | ||
| ); | ||
| this.roundToolErrors.set(agent.name, new Map(currentState.toolResults.map((tc) => { | ||
| const r = tc.result as { success?: boolean; isError?: boolean } | undefined; | ||
| return [tc.id, r?.isError === true || r?.success === false]; | ||
| }))); | ||
| currentState.stream.provideToolResults( | ||
| membraneResults, | ||
| midTurnInjections.length > 0 ? { injectedMessages: midTurnInjections } : undefined, | ||
|
|
@@ -8524,6 +8531,28 @@ export class AgentFramework { | |
| // to prevent a redundant "sent it" postscript. Fresh injected input | ||
| // clears it, because the following prose is a reply to a new message. | ||
| let turnSilenced = false; | ||
| // Prose held because its round contained a send. If every send in that | ||
| // round then FAILS, the round did not actually speak: release the prose | ||
| // and lift the silence (a failed send must not cost the turn its words). | ||
| let heldSilence: { callIds: string[]; segments: string[] } | null = null; | ||
| const roundScopedSilencing = agent.proseSilencing === 'round'; | ||
| const releaseFailedSilence = (): void => { | ||
| if (!heldSilence) return; | ||
|
Comment on lines
+8539
to
+8540
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.
Prompt To Fix With AIThis is a comment left during a code review.
Path: src/framework.ts
Line: 8539-8540
Comment:
**Send-only failures still silence replies** A failed send with no prose in its own tool round creates no hold, so this return never clears default turn-scoped silencing. If the agent writes its answer at completion, that answer is suppressed too, leaving the failed-send dead-air case unresolved.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| const errors = this.roundToolErrors.get(agent.name); | ||
| const known = heldSilence.callIds.filter((id) => errors?.has(id)); | ||
| if (known.length < heldSilence.callIds.length) return; // results not in yet | ||
| const hold = heldSilence; | ||
| heldSilence = null; | ||
| if (!hold.callIds.every((id) => errors!.get(id) === true)) return; // a send landed | ||
| turnSilenced = false; | ||
|
Comment on lines
+8546
to
+8547
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.
Prompt To Fix With AIThis is a comment left during a code review.
Path: src/framework.ts
Line: 8546-8547
Comment:
**Later failure clears successful silence** In the default turn-scoped mode, an earlier successful send should keep the rest of the turn silent. If a later send fails, this code checks only that later round, releases its held prose, and clears the turn-wide flag. The result is prose posted after a send that already succeeded.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| const locus = resolveTurnLocus(); | ||
|
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.
How this was verified: Addressed messages can change the pin before failed-send results resume the stream, and speech routing publishes the held text to the pin read here. Prompt To Fix With AIThis is a comment left during a code review.
Path: src/framework.ts
Line: 8548
Comment:
**Held prose reaches another channel** When a send fails while the agent is replying in channel A, an addressed message from channel B can change the routing pin before this release runs. The prose written for A is then published to B. Preserve the held round’s destination instead of reading the current pin.
**How this was verified:** Addressed messages can change the pin before failed-send results resume the stream, and speech routing publishes the held text to the pin read here.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| console.error( | ||
| `[routing] ${agent.name}: silencing send(s) failed -> releasing ${hold.segments.length} held prose segment(s) -> ${locus ?? '(default)'}`, | ||
| ); | ||
| const suppressed = this.turnProseSuppressed.get(agent.name) ?? 0; | ||
| this.turnProseSuppressed.set(agent.name, Math.max(0, suppressed - hold.segments.length)); | ||
| for (const seg of hold.segments) enqueueSpeech(seg, locus); | ||
| }; | ||
|
|
||
| // Live routing is only trusted when the membrane provides verbatim | ||
| // round-scoped blocks (roundContent, native tool mode, membrane ≥0.5.64). | ||
|
|
@@ -8691,6 +8720,7 @@ export class AgentFramework { | |
|
|
||
| case 'tool-calls': { | ||
| adoptInjectedRound(); | ||
| releaseFailedSilence(); | ||
| hadToolCalls = true; | ||
| this.recordLogicalTurnToolCalls(agent, myTurnToken ?? -1, event.calls.length); | ||
| this.recordEphemeralToolCalls(agent.name, event.calls.length); | ||
|
|
@@ -8780,7 +8810,10 @@ export class AgentFramework { | |
| const hasSameRoundPrivateThink = | ||
| roundToolNames.includes('think') && | ||
| requestSnapshot.sameRoundThinkTextPolicy === 'private'; | ||
| if (roundToolNames.some(isSilencingTool)) { | ||
| if (roundScopedSilencing) { | ||
| // proseSilencing 'round': a send silences only its own round. | ||
| turnSilenced = roundToolNames.some(isSilencingTool); | ||
| } else if (roundToolNames.some(isSilencingTool)) { | ||
| turnSilenced = true; | ||
| } | ||
| if (roundContent && roundContent.length > 0) { | ||
|
|
@@ -8821,6 +8854,8 @@ export class AgentFramework { | |
| console.error( | ||
| `[routing] ${agent.name}: mid-turn round [${roundToolNames.join(', ')}] -> prose NOT routed (turn silenced)`, | ||
| ); | ||
| const sendIds = event.calls.filter((c) => isSilencingTool(c.name) && c.name !== 'skip_reply').map((c) => c.id); | ||
| if (sendIds.length > 0) heldSilence = { callIds: sendIds, segments: roundSegments.map(String) }; | ||
|
Comment on lines
+8857
to
+8858
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.
Prompt To Fix With AIThis is a comment left during a code review.
Path: src/framework.ts
Line: 8857-8858
Comment:
**Hybrid failed sends remain silent** With `proseRouting: 'hybrid'` and default turn-scoped silencing, a failed send suppresses the round’s prose in the hybrid branch, but only this locus branch creates a hold. Nothing then releases that prose or clears the sticky silence, so later prose is suppressed as well.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| // Visible in the turn-end receipt — silencing must never | ||
| // be a silent black hole (n=8: the flying-scene reply). | ||
| this.recordProseSuppression(agent.name, roundSegments.length); | ||
|
|
@@ -8846,6 +8881,7 @@ export class AgentFramework { | |
|
|
||
| case 'complete': { | ||
| adoptInjectedRound(); | ||
| releaseFailedSilence(); | ||
| const durationMs = Date.now() - startTime; | ||
| const response = event.response; | ||
|
|
||
|
|
@@ -9232,7 +9268,7 @@ export class AgentFramework { | |
| .filter((b) => b.type === 'tool_use') | ||
| .map((b) => (b as unknown as { name?: string }).name) | ||
| .filter((n): n is string => typeof n === 'string'); | ||
| const silenced = liveProseRouting | ||
| const silenced = liveProseRouting || roundScopedSilencing | ||
| ? turnSilenced | ||
| : turnSilenced || toolNames.some(isSilencingTool); | ||
|
Comment on lines
+9271
to
9273
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.
Prompt To Fix With AIThis is a comment left during a code review.
Path: src/framework.ts
Line: 9271-9273
Comment:
**Last send suppresses final answer** With `proseSilencing: 'round'`, a successful send in the last tool round leaves `turnSilenced` true. If the next event is a text-only completion, this check suppresses the closing answer even though it belongs to a later round. The new test places a non-send tool round between the send and the answer, so it misses this case.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
|
|
||
|
|
||
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.
Prompt To Fix With AI