[Bug] "missing finish_reason for choice 0" when using DelegatingAIAgent with multi-turn sessions
- 主要语言
- Java
- 星标
- 10.5k
- 派生
- 1.5k
- 平均合并
- 1 天 11 小时
- 30 天内合并 PR
- 128
描述
## Description
When wrapping a Copilot `AIAgent` in a `DelegatingAIAgent` that **buffers all streaming updates** (e.g., to capture structured tool output), sessions with heavy built-in tool usage (file reads, shell commands, git operations) intermittently fail with:
```
Session error: Execution failed: Error: missing finish_reason for choice 0
```
The same sessions succeed when using a **plain agent** (no `DelegatingAIAgent` wrapper) — even with identical prompts, models, tools, and session configuration. The issue appears to be caused by the streaming buffering pattern breaking the SDK's internal message flow during long multi-turn sessions.
## Potential Root Cause
The `DelegatingAIAgent.RunCoreStreamingAsync` override buffers all `AgentResponseUpdate` items before yielding them:
```csharp
// This pattern causes the bug:
protected override async IAsyncEnumerable RunCoreStreamingAsync(...)
{
List updates = [];
await foreach (var update in base.RunCoreStreamingAsync(...))
{
updates.Add(update); // Buffer ALL updates
}
// ... yield updates after buffering
}
```
During long sessions (50+ built-in tool calls), this buffering appears to cause the Copilot CLI to mishandle the streaming response, resulting in a missing `finish_reason` on the final chat completion choice.
## Evidence
### Controlled comparison (same prompt, same model, same repo)
| Agent Type | DelegatingAIAgent? | Built-in tool calls | Result |
|-----------|-------------------|---------------------|--------|
| Worker (plain agent) | ❌ No | **24** permission requests | ✅ **Success** |
| Planner (DelegatingAIAgent) | ✅ Yes | **0** permission requests | ✅ **Success** |
| Reviewer (DelegatingAIAgent) | ✅ Yes | **15-183** permission requests | ❌ **Fails ~60-100%** |
| Reviewer (plain agent, no wrapper) | ❌ No | **183** permission requests | ✅ **Success** |
The pattern is clear: **DelegatingAIAgent + heavy built-in tool usage = failure**. Either factor alone works fine.
### Production sandbox validation
After removing the `DelegatingAIAgent` wrapper from the reviewer and switching to text-based structured output:
- **Before (with wrapper):** 3/3 failures in production, 3/5 failures locally
- **After (plain agent):** Success with 183 permission requests, 1,476 session events, 605 lifecycle events — the heaviest session we've tested
### Local reproduction (5 runs each)
```
Reviewer with DelegatingAIAgent + file reading: 2/5 PASS (40%)
Reviewer as plain agent + file reading: 5/5 PASS (100%)
```
## Steps to Reproduce
```csharp
// 1. Create a DelegatingAIAgent that buffers streaming (mimics ToolCaptureAgent)
class BufferingAgent(AIAgent inner) : DelegatingAIAgent(inner)
{
protected override async IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages, AgentSession? session = null,
AgentRunOptions? options = null, CancellationToken ct = default)
{
List updates = [];
await foreach (var update in base.RunCoreStreamingAsync(messages, session, options, ct))
updates.Add(update);
foreach (var update in updates)
yield return update;
}
}
// 2. Create session with any model
var client = new CopilotClient(new() { GithubToken = token });
var config = new SessionConfig { WorkingDirectory = "/path/to/repo", Model = "claude-opus-4.6" };
var inner = client.AsAIAgent(config, ownsClient: false, name: "test");
var agent = new BufferingAgent(inner); // ← Wrapping causes the bug
// 3. Send prompt that triggers heavy built-in tool usage
var session = await agent.CreateSessionAsync();
var response = await agent.RunAsync(
"Read all .cs files in src/ and summarize them.", session);
// ❌ Intermittently throws: Session error: Execution failed: Error: missing finish_reason for choice 0
```
**Without the wrapper** (using `inner` directly), the same prompt succeeds consistently.
## Expected Behavior
`DelegatingAIAgent` subclasses that buffer streaming updates should work reliably regardless of session length or built-in tool usage count.
## Actual Behavior
Sessions fail intermittently with `missing finish_reason for choice 0` when a `DelegatingAIAgent` buffers streaming updates during long multi-turn sessions with heavy built-in tool usage. Failure rate increases with session length.
## Environment
- **SDK**: `GitHub.Copilot.SDK` v0.1.23 (NuGet, .NET)
- **Also uses**: `Microsoft.Agents.AI.GitHub.Copilot` v1.0.0-preview.260225.1
- **Runtime**: .NET 10
- **OS**: Reproduced on both Windows (local) and Linux (ADC sandbox/Azure Linux 3.0)
- **Models tested**: `claude-opus-4.6`, `gpt-5.1-codex` — both exhibit the same behavior
## Workaround Used
Avoid `DelegatingAIAgent` / streaming buffering for agents that perform heavy built-in tool usage. Use text-based structured output (prompt the model to include a parseable JSON line in its response) instead of intercepting tool calls via a wrapper agent.
贡献指南
调研方向
从 DelegatingAIAgent.RunCoreStreamingAsync 和 issue 中的 BufferingAgent reproduction 开始。在会触发内置工具的长会话期间,将缓冲更新与直接流式传输进行比较,然后使用列出的 SDK 版本运行提供的多轮 reproduction。完成标准是:缓冲的 DelegatingAIAgent 会话不再因缺少 finish_reason 而间歇性失败。
由索引模型根据 Issue 内容生成。
评估
- 技术栈
- csharp
- 领域
- api, backend
- Issue 类型
- 缺陷
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 活跃度
- 冷清
- 描述清晰度
- 基本清楚
- 新手友好度
- 48/100