Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions node/api/scripts/test-build-tool-use-messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ check('msg3 has the gap marker', msgs[3].content.startsWith('--- gap: 3h ---\n[5
const noTime = buildToolUseMessages([{ from_agent: 'salem-engine', message: '# Your turn\nno time' }], NPC);
check('no sent_at -> raw content, no prefix', noTime[0].content === '# Your turn\nno time');

// ZBBS-HOME-436: a REJECTED quote-take replays its "bought" paraphrase with
// the [error] tool result immediately adjacent (paired by tool_call_id in
// stored row order), so the false-success window the wording opens is closed
// in the same breath. This pins the invariant the paraphrase relies on.
const rejected = buildToolUseMessages([
{ from_agent: 'salem-engine', message: '# Your turn', sent_at: agoISO(10) },
{ from_agent: NPC, message: '', tool_calls: [{ id: 't9', name: 'pay_with_item', input: { item: 'Meat', seller: 'John Ellis', quote_id: 1, consume_now: true } }], sent_at: agoISO(9) },
{ from_agent: 'salem-engine', message: '[error: quote expired]', tool_call_id: 't9', sent_at: agoISO(9) },
], NPC);
check('rejected quote-take: bought paraphrase', rejected[1].content === '(I bought Meat from John Ellis and ate it on the spot)');
check('rejected quote-take: [error] result directly follows', rejected[2].role === 'tool' && rejected[2].tool_call_id === 't9' && rejected[2].content === '[error: quote expired]');

if (failed > 0) {
console.log('FAILED ' + failed + ' / ' + (passed + failed));
failures.forEach(f => console.log(f));
Expand Down
28 changes: 28 additions & 0 deletions node/api/scripts/test-paraphrase-tool-call.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,34 @@ assertEqual(
''
);

// A quote-take (quote_id present) settles instantly engine-side
// (ZBBS-HOME-424 fast path), so the paraphrase states the completed
// purchase — "offered" misread a done deal as pending and primed a re-buy
// (ZBBS-HOME-436, the six-meat morning).
assertEqual(
'pay_with_item quote_id (number) + consume_now → bought + ate',
paraphraseToolCall('pay_with_item', { qty: 1, item: 'Meat', amount: 4, seller: 'John Ellis', quote_id: 1, consume_now: true }),
'(I bought Meat from John Ellis and ate it on the spot)'
);

assertEqual(
'pay_with_item quote_id (Llama-stringified) without consume_now → bought',
paraphraseToolCall('pay_with_item', { item: 'Bread', seller: 'John Ellis', quote_id: '3' }),
'(I bought Bread from John Ellis)'
);

assertEqual(
'pay_with_item quote_id zero → still an offer',
paraphraseToolCall('pay_with_item', { item: 'Water', seller: 'Hannah Boggs', quote_id: 0 }),
'(I offered to buy Water from Hannah Boggs)'
);

assertEqual(
'pay_with_item quote_id garbage string → still an offer',
paraphraseToolCall('pay_with_item', { item: 'Water', seller: 'Hannah Boggs', quote_id: 'abc' }),
'(I offered to buy Water from Hannah Boggs)'
);

// ---------- move_to (the field-name bug) ----------

assertEqual(
Expand Down
32 changes: 23 additions & 9 deletions node/api/src/services/virtual-agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -2128,15 +2128,29 @@ function paraphraseToolCall(name, input) {
case 'pay_with_item': {
if (typeof inp.item !== 'string' || !inp.item) return '';
const seller = (typeof inp.seller === 'string' && inp.seller) ? ' from ' + inp.seller : '';
// pay_with_item places an OFFER the seller must accept — the [ok]
// tool result means "offer placed", NOT "purchase completed" (the
// offer can later "fall through" if the seller never fulfils it).
// Paraphrase the attempt, not a success the history may contradict:
// "I bought and consumed cheese" while the NPC stays starving (and a
// later perception says the offer fell through) is exactly the
// incoherent self-history that degrades the model. Llama stringifies
// bools, so consume_now may be true or 'true'.
const eatNow = (inp.consume_now === true || inp.consume_now === 'true') ? ' to eat now' : '';
// Llama stringifies bools, so consume_now may be true or 'true'.
const eatsNow = (inp.consume_now === true || inp.consume_now === 'true');
// A quote-take (quote_id present) settles INSTANTLY in the engine
// (ZBBS-HOME-424 fast path): payment, goods, and any consume_now
// meal complete inside the call — "offered" misreads a done deal
// as still pending and primes a re-buy (ZBBS-HOME-436, the
// six-meat morning). A REJECTED quote-take replays with its
// [error] tool result right after this line, which corrects the
// claim in the same breath. Llama stringifies numbers too, so
// quote_id may be 1 or '1'.
const quoteId = Number(inp.quote_id);
if (Number.isFinite(quoteId) && quoteId > 0) {
const ateNow = eatsNow ? ' and ate it on the spot' : '';
return '(I bought ' + inp.item + seller + ateNow + ')';
}
// Without a quote this places an OFFER the seller must accept —
// the [ok] tool result means "offer placed", NOT "purchase
// completed" (the offer can later "fall through" if the seller
// never fulfils it). Paraphrase the attempt, not a success the
// history may contradict: "I bought and consumed cheese" while
// the NPC stays starving is exactly the incoherent self-history
// that degrades the model.
const eatNow = eatsNow ? ' to eat now' : '';
return '(I offered to buy ' + inp.item + seller + eatNow + ')';
}
case 'consume':
Expand Down