diff --git a/container/agent-runner/src/destinations.test.ts b/container/agent-runner/src/destinations.test.ts index 14243f2a7..807f0a623 100644 --- a/container/agent-runner/src/destinations.test.ts +++ b/container/agent-runner/src/destinations.test.ts @@ -38,7 +38,7 @@ describe('buildSystemPromptAddendum — multi-destination routing guidance', () const prompt = buildSystemPromptAddendum('Casa'); - expect(prompt).toContain('Every response must be wrapped'); + expect(prompt).toContain('All output must be wrapped'); expect(prompt).toContain(''); expect(prompt).toContain('`casa`'); }); @@ -55,7 +55,7 @@ describe('buildSystemPromptAddendum — multi-destination routing guidance', () const prompt = buildSystemPromptAddendum('Casa'); - expect(prompt).toContain('Every response must be wrapped'); + expect(prompt).toContain('All output must be wrapped'); expect(prompt).toContain(''); expect(prompt).toContain('Default routing'); expect(prompt).toContain('`casa`'); diff --git a/container/agent-runner/src/destinations.ts b/container/agent-runner/src/destinations.ts index 6a2939017..fec0b4e16 100644 --- a/container/agent-runner/src/destinations.ts +++ b/container/agent-runner/src/destinations.ts @@ -115,10 +115,9 @@ function buildDestinationsSection(): string { } } lines.push(''); - lines.push('**Every response must be wrapped** in a `...` block.'); + lines.push('**All output must be wrapped.** Use `...` for content to send, or `...` for scratchpad.'); lines.push('You can include multiple `` blocks in one response to send to multiple destinations.'); - lines.push('Text outside of `` blocks is scratchpad — logged but not sent anywhere.'); - lines.push('Use `...` to make scratchpad intent explicit.'); + lines.push('Bare text (outside of `` or `` blocks) is not allowed and will not be delivered.'); lines.push(''); lines.push( '**Default routing**: when replying to an incoming message, address the same destination the message came `from` — every inbound `` 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…").', diff --git a/container/agent-runner/src/poll-loop.ts b/container/agent-runner/src/poll-loop.ts index 4761b950d..e688eab5f 100644 --- a/container/agent-runner/src/poll-loop.ts +++ b/container/agent-runner/src/poll-loop.ts @@ -1,4 +1,4 @@ -import { findByName, type DestinationEntry } from './destinations.js'; +import { findByName, getAllDestinations, type DestinationEntry } from './destinations.js'; import { getPendingMessages, markProcessing, markCompleted, type MessageInRow } from './db/messages-in.js'; import { writeMessageOut } from './db/messages-out.js'; import { getInboundDb, touchHeartbeat, clearStaleProcessingAcks } from './db/connection.js'; @@ -265,6 +265,7 @@ async function processQuery( ): Promise { let queryContinuation: string | undefined; let done = false; + let unwrappedNudged = false; // Concurrent polling: push follow-ups into the active query as they arrive. // We do NOT force-end the stream on silence — keeping the query open avoids @@ -338,6 +339,7 @@ async function processQuery( const keptIds = keep.map((m) => m.id); const prompt = formatMessages(keep); log(`Pushing ${keep.length} follow-up message(s) into active query`); + unwrappedNudged = false; query.push(prompt); markCompleted(keptIds); } catch (err) { @@ -376,7 +378,18 @@ async function processQuery( // at all — either way the turn is finished. markCompleted(initialBatchIds); if (event.text) { - dispatchResultText(event.text, routing); + const { hasUnwrapped } = dispatchResultText(event.text, routing); + if (hasUnwrapped && !unwrappedNudged) { + unwrappedNudged = true; + const destinations = getAllDestinations(); + const names = destinations.map((d) => d.name).join(', '); + query.push( + `Your response was not delivered — it was not wrapped in ... blocks. ` + + `All output must be wrapped: use for content to send, or for scratchpad. ` + + `Your destinations: ${names}. ` + + `Please re-send your response with the correct wrapping.`, + ); + } } } } @@ -415,7 +428,7 @@ function handleEvent(event: ProviderEvent, _routing: RoutingContext): void { * The agent must always wrap output in ... * blocks, even with a single destination. Bare text is scratchpad only. */ -function dispatchResultText(text: string, routing: RoutingContext): void { +function dispatchResultText(text: string, routing: RoutingContext): { sent: number; hasUnwrapped: boolean } { const MESSAGE_RE = /([\s\S]*?)<\/message>/g; let match: RegExpExecArray | null; @@ -450,9 +463,11 @@ function dispatchResultText(text: string, routing: RoutingContext): void { log(`[scratchpad] ${scratchpad.slice(0, 500)}${scratchpad.length > 500 ? '…' : ''}`); } - if (sent === 0 && text.trim()) { + const hasUnwrapped = sent === 0 && !!scratchpad; + if (hasUnwrapped) { log(`WARNING: agent output had no blocks — nothing was sent`); } + return { sent, hasUnwrapped }; } function sendToDestination(dest: DestinationEntry, body: string, routing: RoutingContext): void {