@@ -651,3 +651,144 @@ func BenchmarkBuildSummaryPrompt(b *testing.B) {
651651 })
652652 }
653653}
654+
655+func TestPreparePrompt_OrphanedToolUse(t *testing.T) {
656+ t.Parallel()
657+ env := testEnv(t)
658+ sa := testSessionAgent(env, nil, nil, "test prompt")
659+ agent := sa.(*sessionAgent)
660+
661+ ctx := t.Context()
662+ sess, err := env.sessions.Create(ctx, "test")
663+ require.NoError(t, err)
664+
665+ // Create a user message.
666+ _, err = env.messages.Create(ctx, sess.ID, message.CreateMessageParams{
667+ Role: message.User,
668+ Parts: []message.ContentPart{
669+ message.TextContent{Text: "hello"},
670+ },
671+ })
672+ require.NoError(t, err)
673+
674+ // Create an assistant message with a tool call but no tool result —
675+ // this simulates a cancelled/interrupted agent tool call.
676+ _, err = env.messages.Create(ctx, sess.ID, message.CreateMessageParams{
677+ Role: message.Assistant,
678+ Parts: []message.ContentPart{
679+ message.TextContent{Text: "let me check"},
680+ message.ToolCall{
681+ ID: "call_orphaned_1",
682+ Name: "agent",
683+ Input: `{"prompt":"do something"}`,
684+ Finished: true,
685+ },
686+ },
687+ })
688+ require.NoError(t, err)
689+
690+ // Create the next user message (the one that interrupted the tool call).
691+ _, err = env.messages.Create(ctx, sess.ID, message.CreateMessageParams{
692+ Role: message.User,
693+ Parts: []message.ContentPart{
694+ message.TextContent{Text: "Fix #2"},
695+ },
696+ })
697+ require.NoError(t, err)
698+
699+ msgs, err := env.messages.List(ctx, sess.ID)
700+ require.NoError(t, err)
701+
702+ history, _ := agent.preparePrompt(msgs)
703+
704+ // The history must contain a synthetic tool result for the orphaned call.
705+ found := false
706+ for _, msg := range history {
707+ if msg.Role != fantasy.MessageRoleTool {
708+ continue
709+ }
710+ for _, part := range msg.Content {
711+ if tr, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part); ok {
712+ if tr.ToolCallID == "call_orphaned_1" {
713+ found = true
714+ _, isError := tr.Output.(fantasy.ToolResultOutputContentError)
715+ require.True(t, isError, "orphaned tool result should be an error")
716+ }
717+ }
718+ }
719+ }
720+ require.True(t, found, "expected synthetic tool result for orphaned tool call")
721+}
722+
723+func TestPreparePrompt_OrphanedToolUseMixed(t *testing.T) {
724+ t.Parallel()
725+ env := testEnv(t)
726+ sa := testSessionAgent(env, nil, nil, "test prompt")
727+ agent := sa.(*sessionAgent)
728+
729+ ctx := t.Context()
730+ sess, err := env.sessions.Create(ctx, "test")
731+ require.NoError(t, err)
732+
733+ _, err = env.messages.Create(ctx, sess.ID, message.CreateMessageParams{
734+ Role: message.User,
735+ Parts: []message.ContentPart{
736+ message.TextContent{Text: "hello"},
737+ },
738+ })
739+ require.NoError(t, err)
740+
741+ // Assistant with 2 tool calls: one has a result, one is orphaned.
742+ _, err = env.messages.Create(ctx, sess.ID, message.CreateMessageParams{
743+ Role: message.Assistant,
744+ Parts: []message.ContentPart{
745+ message.ToolCall{
746+ ID: "call_ok",
747+ Name: "view",
748+ Input: `{"path":"/foo"}`,
749+ Finished: true,
750+ },
751+ message.ToolCall{
752+ ID: "call_orphaned",
753+ Name: "agent",
754+ Input: `{"prompt":"search"}`,
755+ Finished: true,
756+ },
757+ },
758+ })
759+ require.NoError(t, err)
760+
761+ // Only one tool result — for call_ok.
762+ _, err = env.messages.Create(ctx, sess.ID, message.CreateMessageParams{
763+ Role: message.Tool,
764+ Parts: []message.ContentPart{
765+ message.ToolResult{
766+ ToolCallID: "call_ok",
767+ Name: "view",
768+ Content: "file contents",
769+ },
770+ },
771+ })
772+ require.NoError(t, err)
773+
774+ msgs, err := env.messages.List(ctx, sess.ID)
775+ require.NoError(t, err)
776+
777+ history, _ := agent.preparePrompt(msgs)
778+
779+ // Should have a synthetic result only for the orphaned call.
780+ var syntheticCount int
781+ for _, msg := range history {
782+ if msg.Role != fantasy.MessageRoleTool {
783+ continue
784+ }
785+ for _, part := range msg.Content {
786+ if tr, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part); ok {
787+ if tr.ToolCallID == "call_orphaned" {
788+ syntheticCount++
789+ }
790+ }
791+ }
792+ }
793+ require.Equal(t, 1, syntheticCount, "expected exactly one synthetic result for the orphaned call")
794+}