@@ -651,3 +651,144 @@ func BenchmarkBuildSummaryPrompt(b *testing.B) {
655+func TestPreparePrompt_OrphanedToolUse(t *testing.T) {
658+ sa := testSessionAgent(env, nil, nil, "test prompt")
659+ agent := sa.(*sessionAgent)
662+ sess, err := env.sessions.Create(ctx, "test")
663+ require.NoError(t, err)
665+ // Create a user message.
666+ _, err = env.messages.Create(ctx, sess.ID, message.CreateMessageParams{
668+ Parts: []message.ContentPart{
669+ message.TextContent{Text: "hello"},
672+ require.NoError(t, err)
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"},
681+ ID: "call_orphaned_1",
683+ Input: `{"prompt":"do something"}`,
688+ require.NoError(t, err)
690+ // Create the next user message (the one that interrupted the tool call).
691+ _, err = env.messages.Create(ctx, sess.ID, message.CreateMessageParams{
693+ Parts: []message.ContentPart{
694+ message.TextContent{Text: "Fix #2"},
697+ require.NoError(t, err)
699+ msgs, err := env.messages.List(ctx, sess.ID)
700+ require.NoError(t, err)
702+ history, _ := agent.preparePrompt(msgs)
704+ // The history must contain a synthetic tool result for the orphaned call.
706+ for _, msg := range history {
707+ if msg.Role != fantasy.MessageRoleTool {
710+ for _, part := range msg.Content {
711+ if tr, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part); ok {
712+ if tr.ToolCallID == "call_orphaned_1" {
714+ _, isError := tr.Output.(fantasy.ToolResultOutputContentError)
715+ require.True(t, isError, "orphaned tool result should be an error")
720+ require.True(t, found, "expected synthetic tool result for orphaned tool call")
723+func TestPreparePrompt_OrphanedToolUseMixed(t *testing.T) {
726+ sa := testSessionAgent(env, nil, nil, "test prompt")
727+ agent := sa.(*sessionAgent)
730+ sess, err := env.sessions.Create(ctx, "test")
731+ require.NoError(t, err)
733+ _, err = env.messages.Create(ctx, sess.ID, message.CreateMessageParams{
735+ Parts: []message.ContentPart{
736+ message.TextContent{Text: "hello"},
739+ require.NoError(t, err)
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{
748+ Input: `{"path":"/foo"}`,
752+ ID: "call_orphaned",
754+ Input: `{"prompt":"search"}`,
759+ require.NoError(t, err)
761+ // Only one tool result — for call_ok.
762+ _, err = env.messages.Create(ctx, sess.ID, message.CreateMessageParams{
764+ Parts: []message.ContentPart{
766+ ToolCallID: "call_ok",
768+ Content: "file contents",
772+ require.NoError(t, err)
774+ msgs, err := env.messages.List(ctx, sess.ID)
775+ require.NoError(t, err)
777+ history, _ := agent.preparePrompt(msgs)
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 {
785+ for _, part := range msg.Content {
786+ if tr, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part); ok {
787+ if tr.ToolCallID == "call_orphaned" {
793+ require.Equal(t, 1, syntheticCount, "expected exactly one synthetic result for the orphaned call")