mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
fix(assistant): Claude Sonnet 5.5 no longer ends a turn with 'came back without an answer'
Sonnet 5.5 hands back the words written before a tool call as a hidden thinking block. After the ask for the answer, an empty step got the general push with tools open again; the model answered, offered follow-ups once more, and the answer was lost a second time. That push now asks for the answer as text and stays text-only.
This commit is contained in:
@@ -199,6 +199,10 @@ const GO_ON =
|
||||
'Your last reply came back empty or was cut off. Carry on from where you were: answer, or make the calls now. ' +
|
||||
'If the plan is large, propose part of it now; after the user decided you continue with the rest.';
|
||||
|
||||
/** The push for an empty step after the ask for the answer: no calls are open to it any more. */
|
||||
const ANSWER_NOW =
|
||||
'Your last reply came back empty, and the user still sees no answer. Write your answer to them now, as text.';
|
||||
|
||||
/**
|
||||
* Runs a read at once and records it: where it looks (streamed only), its
|
||||
* call and answer, and the cards its answer makes (its package's card first,
|
||||
@@ -590,7 +594,14 @@ async function runStep(turn: Turn, step: number): Promise<AgentOutcome | null> {
|
||||
// An answer that offered follow-ups is finished, however it ends.
|
||||
const settled = asks.length > 0 && !empty;
|
||||
const announced = !settled && received.length === 0 && step === 0 && ANNOUNCED.test(text.trimEnd());
|
||||
const push = empty || (!settled && received.length === 0 && cutOff) ? GO_ON : announced ? MAKE_THE_CALLS : null;
|
||||
const push =
|
||||
empty && turn.askedForAnswer
|
||||
? ANSWER_NOW
|
||||
: empty || (!settled && received.length === 0 && cutOff)
|
||||
? GO_ON
|
||||
: announced
|
||||
? MAKE_THE_CALLS
|
||||
: null;
|
||||
// Follow-ups with no answer a second time end the turn: the model was already asked for the answer.
|
||||
const wordlessAgain = empty && asks.length > 0;
|
||||
if (push && !turn.nudged && !wordlessAgain) {
|
||||
@@ -600,6 +611,12 @@ async function runStep(turn: Turn, step: number): Promise<AgentOutcome | null> {
|
||||
// An empty assistant message is one the providers refuse; only the push goes back.
|
||||
if (!empty) history.push({ role: 'assistant', text, toolCalls: [], providerState });
|
||||
history.push({ role: 'user', text: push });
|
||||
// Once the answer was asked for, the push is held to text as well. Claude
|
||||
// Sonnet 5.5 hands back the words it writes before a tool call as a hidden
|
||||
// thinking block: with tools open again it answers, offers follow-ups once
|
||||
// more, and the answer is lost a second time. The chips of the first offer
|
||||
// are kept in turn.followUps.
|
||||
if (turn.askedForAnswer) turn.textOnlyNext = true;
|
||||
return null;
|
||||
}
|
||||
if (empty) {
|
||||
|
||||
@@ -866,7 +866,7 @@ describe('runAgentLoop', () => {
|
||||
expect(t.requests).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('ASTAGT-047: the text-only request is the one step after the ask, never the ones before or after it (#3016)', async () => {
|
||||
it('ASTAGT-047: the text-only request starts with the step after the ask, never before it (#3016)', async () => {
|
||||
const t = setup([
|
||||
[{ type: 'tool-call', call: { id: 'l1', name: 'list_places', args: {} } }],
|
||||
[ask('toolu_9', ['More stops']), { type: 'finish', reason: 'tool-calls' }],
|
||||
@@ -880,6 +880,22 @@ describe('runAgentLoop', () => {
|
||||
expect(t.requests.map((request) => request.toolChoice)).toEqual([undefined, undefined, 'none']);
|
||||
});
|
||||
|
||||
it('ASTAGT-050: an empty step after the ask gets a push that is text-only too, so offered follow-ups cannot hide the answer again', async () => {
|
||||
const t = setup([
|
||||
[ask('toolu_10', ['Stops on the drive']), { type: 'finish', reason: 'tool-calls' }],
|
||||
[{ type: 'finish', reason: 'stop' }],
|
||||
[
|
||||
{ type: 'text-delta', delta: 'You leave Lisbon on day 4.' },
|
||||
{ type: 'finish', reason: 'stop' },
|
||||
],
|
||||
]);
|
||||
const outcome = await runAgentLoop(t.deps, t.input, t.writer, new AbortController().signal);
|
||||
expect(outcome.end).toBe('stop');
|
||||
expect(t.requests.map((request) => request.toolChoice)).toEqual([undefined, 'none', 'none']);
|
||||
expect(t.requests[2].messages.at(-1)).toMatchObject({ role: 'user', text: expect.stringContaining('as text') });
|
||||
expect(t.chunks).toContainEqual({ type: 'data-trek-follow-ups', data: { questions: ['Stops on the drive'] } });
|
||||
});
|
||||
|
||||
it('ASTAGT-045: follow-ups with no answer after an announcing first answer are still asked for the answer', async () => {
|
||||
const t = setup([
|
||||
[{ type: 'text-delta', delta: 'Let me check your places:' }],
|
||||
|
||||
Reference in New Issue
Block a user