fix(agent): emit function_call items from fromChatMessages and return Item[] (#11, #41) - #93
Open
LukasParke wants to merge 1 commit into
Open
fix(agent): emit function_call items from fromChatMessages and return Item[] (#11, #41)#93LukasParke wants to merge 1 commit into
LukasParke wants to merge 1 commit into
Conversation
LukasParke
marked this pull request as ready for review
August 6, 2026 21:31
LukasParke
force-pushed
the
fix/11-41-from-chat-messages
branch
from
August 6, 2026 21:32
aca6802 to
2c54b3a
Compare
… Item[] (#11, #41) The assistant branch of `fromChatMessages` never read `msg.toolCalls`, so a tool-calling assistant message (conventionally `content: null`) converted to `{ role: 'assistant', content: '' }` — the tool call vanished and the following `function_call_output` items were left orphaned, breaking agentic loops replayed through the helper. The `.map()` callback's return type also structurally prevented emitting function_call items at all. Converted the conversion to an accumulator loop that emits one `function_call` item per `toolCalls` entry, mirroring `fromClaudeMessages`. `arguments` is already a JSON string in the chat format, so it is forwarded as-is rather than re-stringified. The message item is skipped only when content is empty *and* tool calls take its place, keeping the pre-existing empty-message behavior for content-less assistant messages with no tool calls. Separately, both `fromChatMessages` and `fromClaudeMessages` declared `models.InputsUnion`, which is not assignable to callModel's `FieldOrAsyncFunction<Item[]> | string` input — so the documented `callModel({ input: fromChatMessages(msgs) })` usage did not typecheck. Both now return `Item[]`, and the `Item` union gains `NewAssistantMessageItem` and `NewSystemMessageItem` since it had no id-less member for either role. Role construction moved to per-role narrowed helpers: TypeScript will not distribute a union-typed `role` across the per-role members of `Item`, so mapping to the wide `EasyInputMessageRoleUnion` cannot produce an assignable value. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
LukasParke
force-pushed
the
fix/11-41-from-chat-messages
branch
from
August 11, 2026 18:09
2c54b3a to
dacf4d1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two bugs in the same function,
fromChatMessages— one runtime, one type-level.Fixes #11
Fixes #41
#11 — assistant
toolCallswere silently droppedThe assistant branch only read
msg.contentand nevermsg.toolCalls:A tool-calling assistant message conventionally carries
content: null, so this produced{ role: 'assistant', content: '' }— the tool call vanished, and the followingfunction_call_outputitems were left orphaned with no call to answer. Any agentic loop replayed through this helper lost its tool calls.The
.map()callback's declared return type (EasyInputMessage | FunctionCallOutputItem) also structurally prevented emittingfunction_callitems at all, and.map()is 1:1 while one chat message can fan out to several items.Converted to an accumulator loop mirroring
fromClaudeMessages: push a message item, then onefunction_callitem pertoolCallsentry.Two details worth flagging for review:
argumentsis forwarded as-is.ChatToolCall.function.argumentsis already a JSON string in the chat format, unlike the Claude path's structuredinputwhich needsJSON.stringify. Double-stringifying would send"{\"a\":1}"to the model. There is a dedicated regression test pinning this.handles null content in assistant messagetest is unchanged.#41 — return type was not assignable to
callModelinputfromChatMessagesdeclaredmodels.InputsUnion, butcallModel's input isFieldOrAsyncFunction<Item[]> | string. So the usage in the function's own doc comment did not typecheck:Both
fromChatMessagesandfromClaudeMessagesnow returnItem[].fromClaudeMessageshad the identical problem and is fixed here too, since this PR already touches the union.The
Itemunion had no id-less member for theassistantorsystemroles —AssistantMessageItemis the model'sOutputMessage, which requires anidand structured content. AddedNewAssistantMessageItemandNewSystemMessageItemfollowing the existingNew*pattern, and exported both.CallFunctionToolItem(=OutputFunctionCallItem, withid?: string) already covered the emitted function_call items as-is.Role construction moved into per-role narrowed helpers (
createMessageItem/createEasyInputMessage). This is load-bearing rather than cosmetic: TypeScript will not distribute a union-typedroleacross the per-role members ofItem, so returning the wideEasyInputMessageRoleUnionfrom a sharedmapChatRolecannot produce an assignable value.mapChatRolebecame dead and was removed.API example
Testing
Red-first. Both regressions were pinned and confirmed failing before the fix:
{ role: 'assistant', content: '' }.error TS2322: Type 'InputsUnion' is not assignable to type 'Item[]'.New tests cover the exact #11 repro (user → assistant with
content: null+ toolCall → tool), assistant with both text and toolCalls, parallel tool calls, the no-double-stringify guarantee, an emptytoolCallsarray, and a compile-levelconst items: Item[] = fromChatMessages(msgs)assertion for #41.After:
649 passed (649)for@openrouter/agent; build, typecheck, and lint green workspace-wide.Follow-up: CI typecheck blind spot
packages/agent/tsconfig.typecheck.jsonincludes onlysrc/**/*.tsand one.test-d.ts, excludingtests/e2e/. That is why ~48fromChatMessages/fromClaudeMessagescall sites intests/e2e/call-model.test.tspassed a wrong-typed value for so long without failing the build.Measured, as evidence the type fix is real: typechecking that file against
main'ssrcyields 69 errors; against this branch, 21 — a reduction of exactly 48, matching the call-site count, with zero overlap between the remaining error lines and the call-site lines.I deliberately did not widen the tsconfig
includehere — the remaining 21 errors are pre-existing and unrelated (implicitany, index-signature access, stale SDK module paths), so fixing them is its own PR. Worth doing as a follow-up so this class of bug fails CI.🤖 Generated with Claude Code