Skip to content

A __finish__ call is a tool-call step (StructuredOutputStepResult removed) ​

This guide covers the release that makes every structured-output completion leave a trace (RM-35). It is a breaking release of @helix-agents/core, @helix-agents/llm-vercel and @helix-agents/tracing-langfuse, all 0.x packages, so each takes a minor bump. @helix-agents/sdk re-exports core, so it takes a minor bump too.

What was wrong ​

@helix-agents/llm-vercel returned any step whose tool calls included __finish__ as a structured_output step result carrying only __finish__'s input. Core's structured_output branch then planned no assistant message, so:

  • every call co-emitted with __finish__, and the step's text, were dropped: never executed, persisted or answered;
  • the turn persisted nothing after its user message. The next turn sent two adjacent user messages, and the model could answer the previous question instead of the new one (observed live);
  • a forced completion call that returned __finish__ plus another call completed the run, bypassing the protocol check that makes it a recoverable miss.

What changed ​

BeforeAfter
StepResult<TOutput> = text, tool_calls, structured_output, errorStepResult = text, tool_calls, error (not generic)
StructuredOutputStepResult<TOutput> exported from coreremoved
llm-vercel returned a __finish__ step as structured_outputit returns tool_calls with every call and the text
a structured-output turn persisted no assistant messageit persists the assistant message (every call, its text and ordered parts) and the __finish__ call's {"acknowledged":true} result
a call co-emitted with __finish__ vanishedit is answered not_executed (superseded_by_terminal_call) and never runs (RM-29 C2)
AgentConfig.stopWhen: (result: StepResult<TOutput>) => booleanstopWhen: (result: StepResult) => boolean
StopConfig<TOutput>, resolveStopDecision<TOutput>, shouldStopExecution<TOutput>, determineFinalStatus<TOutput>not generic

A step carrying a __finish__ call is terminal in resolveStopDecision, as the removed structured_output step was, so stopWhen is not called for it and maxSteps never preempts it.

Custom adapters and bring-your-own-loop code ​

Return a __finish__ call in tool_calls, like any other call. Keep every call co-emitted with it and the response's text, and leave its arguments uncoerced, since the output schema may be a non-object:

typescript
// Before
const finishCall = toolCalls.find((tc) => tc.name === FINISH_TOOL_NAME);
if (finishCall) {
  return {
    type: 'structured_output',
    output: finishCall.arguments,
    shouldStop: true,
    stopReason: 'tool_use',
  };
}

// After: no special case
return {
  type: 'tool_calls',
  toolCalls, // `__finish__` included, with its siblings
  subAgentCalls: [],
  content: text || undefined,
  shouldStop: false,
  stopReason: 'tool_use',
};

Core decides the step (planStepProcessing). The first __finish__ completes the run and keeps the assistant message, every other call is answered not_executed, and under forced completion a response carrying extra calls is a recoverable protocol_violation miss.

A loop or a predicate that branched on type === 'structured_output' should check for the __finish__ call instead. TypeScript flags the old comparison, since the literal no longer exists:

typescript
const finished =
  result.type === 'tool_calls' && result.toolCalls.some((tc) => tc.name === FINISH_TOOL_NAME);

Drop the type argument from StepResult<…>, StopConfig<…> and the stop-decision helpers.

Tests that script MockLLMAdapter ​

The { type: 'structured_output', output } script shorthand still works. The mock now returns it as a tool_calls step with one __finish__ call (id mock_finish_<message count>_<call>), so the turn persists an assistant message and its acknowledged result: a test that counted messages after a scripted structured output sees two more per turn. A hand-built StepResult literal with type: 'structured_output' (one carrying shouldStop) must become a tool_calls step with a __finish__ call.

Tracing ​

@helix-agents/core's tracing hooks still set gen_ai.completion for a structured-output step, from the __finish__ call's arguments. @helix-agents/tracing-langfuse reports such a step as a tool-call generation (its toolCalls, arguments included when includeToolArgs is on) instead of a bare output.

Released under the MIT License.