|
1 | | -import { findByName, type DestinationEntry } from './destinations.js'; |
| 1 | +import { findByName, getAllDestinations, type DestinationEntry } from './destinations.js'; |
2 | 2 | import { getPendingMessages, markProcessing, markCompleted, type MessageInRow } from './db/messages-in.js'; |
3 | 3 | import { writeMessageOut } from './db/messages-out.js'; |
4 | 4 | import { getInboundDb, touchHeartbeat, clearStaleProcessingAcks } from './db/connection.js'; |
@@ -265,6 +265,7 @@ async function processQuery( |
265 | 265 | ): Promise<QueryResult> { |
266 | 266 | let queryContinuation: string | undefined; |
267 | 267 | let done = false; |
| 268 | + let unwrappedNudged = false; |
268 | 269 |
|
269 | 270 | // Concurrent polling: push follow-ups into the active query as they arrive. |
270 | 271 | // We do NOT force-end the stream on silence — keeping the query open avoids |
@@ -338,6 +339,7 @@ async function processQuery( |
338 | 339 | const keptIds = keep.map((m) => m.id); |
339 | 340 | const prompt = formatMessages(keep); |
340 | 341 | log(`Pushing ${keep.length} follow-up message(s) into active query`); |
| 342 | + unwrappedNudged = false; |
341 | 343 | query.push(prompt); |
342 | 344 | markCompleted(keptIds); |
343 | 345 | } catch (err) { |
@@ -376,7 +378,18 @@ async function processQuery( |
376 | 378 | // at all — either way the turn is finished. |
377 | 379 | markCompleted(initialBatchIds); |
378 | 380 | 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 | + } |
380 | 393 | } |
381 | 394 | } |
382 | 395 | } |
@@ -415,7 +428,7 @@ function handleEvent(event: ProviderEvent, _routing: RoutingContext): void { |
415 | 428 | * The agent must always wrap output in <message to="name">...</message> |
416 | 429 | * blocks, even with a single destination. Bare text is scratchpad only. |
417 | 430 | */ |
418 | | -function dispatchResultText(text: string, routing: RoutingContext): void { |
| 431 | +function dispatchResultText(text: string, routing: RoutingContext): { sent: number; hasUnwrapped: boolean } { |
419 | 432 | const MESSAGE_RE = /<message\s+to="([^"]+)"\s*>([\s\S]*?)<\/message>/g; |
420 | 433 |
|
421 | 434 | let match: RegExpExecArray | null; |
@@ -450,9 +463,11 @@ function dispatchResultText(text: string, routing: RoutingContext): void { |
450 | 463 | log(`[scratchpad] ${scratchpad.slice(0, 500)}${scratchpad.length > 500 ? '…' : ''}`); |
451 | 464 | } |
452 | 465 |
|
453 | | - if (sent === 0 && text.trim()) { |
| 466 | + const hasUnwrapped = sent === 0 && !!scratchpad; |
| 467 | + if (hasUnwrapped) { |
454 | 468 | log(`WARNING: agent output had no <message to="..."> blocks — nothing was sent`); |
455 | 469 | } |
| 470 | + return { sent, hasUnwrapped }; |
456 | 471 | } |
457 | 472 |
|
458 | 473 | function sendToDestination(dest: DestinationEntry, body: string, routing: RoutingContext): void { |
|
0 commit comments