Skip to content

Commit 7f4fa65

Browse files
gavrielcclaude
andcommitted
fix(poll-loop): nudge agent when output lacks message wrapping
When the agent outputs bare text without <message to="..."> blocks, nothing gets delivered — silent failure. Now the poll-loop pushes a one-shot correction back into the active query telling the agent to re-send with proper wrapping. Capped at once per user turn to avoid loops; resets when a new follow-up message arrives. Also updates destination instructions to require explicit <internal> wrapping for scratchpad instead of treating bare text as implicit scratchpad. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e0f5967 commit 7f4fa65

3 files changed

Lines changed: 23 additions & 9 deletions

File tree

container/agent-runner/src/destinations.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ describe('buildSystemPromptAddendum — multi-destination routing guidance', ()
3838

3939
const prompt = buildSystemPromptAddendum('Casa');
4040

41-
expect(prompt).toContain('Every response must be wrapped');
41+
expect(prompt).toContain('All output must be wrapped');
4242
expect(prompt).toContain('<message to="name">');
4343
expect(prompt).toContain('`casa`');
4444
});
@@ -55,7 +55,7 @@ describe('buildSystemPromptAddendum — multi-destination routing guidance', ()
5555

5656
const prompt = buildSystemPromptAddendum('Casa');
5757

58-
expect(prompt).toContain('Every response must be wrapped');
58+
expect(prompt).toContain('All output must be wrapped');
5959
expect(prompt).toContain('<message to="name">');
6060
expect(prompt).toContain('Default routing');
6161
expect(prompt).toContain('`casa`');

container/agent-runner/src/destinations.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,9 @@ function buildDestinationsSection(): string {
115115
}
116116
}
117117
lines.push('');
118-
lines.push('**Every response must be wrapped** in a `<message to="name">...</message>` block.');
118+
lines.push('**All output must be wrapped.** Use `<message to="name">...</message>` for content to send, or `<internal>...</internal>` for scratchpad.');
119119
lines.push('You can include multiple `<message>` blocks in one response to send to multiple destinations.');
120-
lines.push('Text outside of `<message>` blocks is scratchpad — logged but not sent anywhere.');
121-
lines.push('Use `<internal>...</internal>` to make scratchpad intent explicit.');
120+
lines.push('Bare text (outside of `<message>` or `<internal>` blocks) is not allowed and will not be delivered.');
122121
lines.push('');
123122
lines.push(
124123
'**Default routing**: when replying to an incoming message, address the same destination the message came `from` — every inbound `<message>` tag carries a `from="name"` attribute that names the origin destination. Only address a different destination when the request itself asks you to (e.g., "tell Laura that…").',

container/agent-runner/src/poll-loop.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { findByName, type DestinationEntry } from './destinations.js';
1+
import { findByName, getAllDestinations, type DestinationEntry } from './destinations.js';
22
import { getPendingMessages, markProcessing, markCompleted, type MessageInRow } from './db/messages-in.js';
33
import { writeMessageOut } from './db/messages-out.js';
44
import { getInboundDb, touchHeartbeat, clearStaleProcessingAcks } from './db/connection.js';
@@ -265,6 +265,7 @@ async function processQuery(
265265
): Promise<QueryResult> {
266266
let queryContinuation: string | undefined;
267267
let done = false;
268+
let unwrappedNudged = false;
268269

269270
// Concurrent polling: push follow-ups into the active query as they arrive.
270271
// We do NOT force-end the stream on silence — keeping the query open avoids
@@ -338,6 +339,7 @@ async function processQuery(
338339
const keptIds = keep.map((m) => m.id);
339340
const prompt = formatMessages(keep);
340341
log(`Pushing ${keep.length} follow-up message(s) into active query`);
342+
unwrappedNudged = false;
341343
query.push(prompt);
342344
markCompleted(keptIds);
343345
} catch (err) {
@@ -376,7 +378,18 @@ async function processQuery(
376378
// at all — either way the turn is finished.
377379
markCompleted(initialBatchIds);
378380
if (event.text) {
379-
dispatchResultText(event.text, routing);
381+
const { hasUnwrapped } = dispatchResultText(event.text, routing);
382+
if (hasUnwrapped && !unwrappedNudged) {
383+
unwrappedNudged = true;
384+
const destinations = getAllDestinations();
385+
const names = destinations.map((d) => d.name).join(', ');
386+
query.push(
387+
`<system>Your response was not delivered — it was not wrapped in <message to="name">...</message> blocks. ` +
388+
`All output must be wrapped: use <message to="name"> for content to send, or <internal> for scratchpad. ` +
389+
`Your destinations: ${names}. ` +
390+
`Please re-send your response with the correct wrapping.</system>`,
391+
);
392+
}
380393
}
381394
}
382395
}
@@ -415,7 +428,7 @@ function handleEvent(event: ProviderEvent, _routing: RoutingContext): void {
415428
* The agent must always wrap output in <message to="name">...</message>
416429
* blocks, even with a single destination. Bare text is scratchpad only.
417430
*/
418-
function dispatchResultText(text: string, routing: RoutingContext): void {
431+
function dispatchResultText(text: string, routing: RoutingContext): { sent: number; hasUnwrapped: boolean } {
419432
const MESSAGE_RE = /<message\s+to="([^"]+)"\s*>([\s\S]*?)<\/message>/g;
420433

421434
let match: RegExpExecArray | null;
@@ -450,9 +463,11 @@ function dispatchResultText(text: string, routing: RoutingContext): void {
450463
log(`[scratchpad] ${scratchpad.slice(0, 500)}${scratchpad.length > 500 ? '…' : ''}`);
451464
}
452465

453-
if (sent === 0 && text.trim()) {
466+
const hasUnwrapped = sent === 0 && !!scratchpad;
467+
if (hasUnwrapped) {
454468
log(`WARNING: agent output had no <message to="..."> blocks — nothing was sent`);
455469
}
470+
return { sent, hasUnwrapped };
456471
}
457472

458473
function sendToDestination(dest: DestinationEntry, body: string, routing: RoutingContext): void {

0 commit comments

Comments
 (0)