anthropics / anthropics/anthropic-sdk-python

Self-hosted environments: outcome grader's tool results rejected with 400, so grading never completes

Đang mở
#1,790 1 bình luận 0 reaction 0 người được giao Xem trên GitHub
Ngôn ngữ chính
Python
Star
3.9k
Fork
853
Merge trung bình
1 ngày 18 giờ
Pull request đã merge (30 ngày)
11

Mô tả

On a self-hosted environment, the outcome grader's tool calls are executed by the worker, but posting their results back is rejected with a 400. The grader then waits indefinitely and `outcome_evaluations[].result` stays `pending` forever. The same agent and the same rubric grade normally on a `type: cloud` environment.

The rejection is self-inconsistent. `GET /v1/sessions/{session_id}/events` returns the grader's `agent.tool_use` events. `POST /v1/sessions/{session_id}/events` with a `user.tool_result` for one of those same event ids returns:

```
`tool_use_id` "sevt_01C7v5e8VgTT71gnLSYkUnn3" does not match any `agent.tool_use` event in this session
```

A writer-thread tool result posted to that identical endpoint seven seconds earlier succeeds with a 200. The only difference I can find between the two is that the grader's events carry a `session_thread_id` and the writer's do not.

## Environment

- `anthropic==0.120.0` (Python), running `EnvironmentWorker` in a long-lived Kubernetes deployment
- Self-hosted environment, `agent_toolset_20260401`, permission policy `always_allow`
- Agent model `claude-opus-5`, effort `low`, `max_iterations: 1`
- `src/anthropic/lib/tools/_beta_session_runner.py` and `src/anthropic/lib/environments/` are byte-identical across `v0.117.0`, `v0.120.0`, and `v0.120.2`, so this is not specific to the version we run.

## Reproduction

Create a session on a self-hosted environment with a single `user.define_outcome` initial event whose rubric requires the grader to read the deliverable back from the sandbox:

```json
{
"type": "user.define_outcome",
"description": "Create a file /workspace/probe.txt whose only line is exactly OUTCOME-PROBE-OK. Then stop.",
"rubric": {
"type": "text",
"content": "## File exists and is correct\nThe file /workspace/probe.txt exists and its only line is exactly `OUTCOME-PROBE-OK`. You MUST verify this by reading the file from the sandbox filesystem with a tool call."
},
"max_iterations": 1
}
```

The writer completes and writes the file. The grader starts, issues two tool calls, and the evaluation never finishes.

## Worker log

One session, one worker, no stream reconnect between these lines:

```
19:11:35 _poller INFO claimed work work_id=sesn_01JeEaW44N8VQs3r23sHqaAG work_type=session
19:11:36 _beta_session_runner INFO session tool runner starting session_id=sesn_01JeEaW44N8VQs3r23sHqaAG
19:11:37 httpx GET /v1/sessions/sesn_01JeEaW44N8VQs3r23sHqaAG/events/stream "200 OK"
19:11:37 httpx GET /v1/sessions/sesn_01JeEaW44N8VQs3r23sHqaAG/events?limit=1000 "200 OK"
19:11:37 _beta_session_runner INFO executing tool tool=write tool_use_id=sevt_013KfLN9Vgqh4wcLJPA4oU8W
19:11:37 httpx POST /v1/sessions/sesn_01JeEaW44N8VQs3r23sHqaAG/events "200 OK"

19:11:44 _beta_session_runner INFO executing tool tool=bash tool_use_id=sevt_01C7v5e8VgTT71gnLSYkUnn3
19:11:44 httpx POST /v1/sessions/sesn_01JeEaW44N8VQs3r23sHqaAG/events "400 Bad Request"
19:11:44 _beta_session_runner ERROR failed to send tool result tool_use_id=sevt_01C7v5e8VgTT71gnLSYkUnn3
error=Error code: 400 - {'type': 'error', 'error': {'type': 'invalid_request_error',
'message': '`tool_use_id` "sevt_01C7v5e8VgTT71gnLSYkUnn3" does not match any
`agent.tool_use` event in this session'}, 'request_id': 'req_011CdYmkQdC493iPnk2tkFPZ'}

19:11:44 _beta_session_runner INFO executing tool tool=read tool_use_id=sevt_015ZVxsWuDgG2ywnmpTwBa8p
19:11:44 httpx POST /v1/sessions/sesn_01JeEaW44N8VQs3r23sHqaAG/events "400 Bad Request"
19:11:44 _beta_session_runner ERROR failed to send tool result tool_use_id=sevt_015ZVxsWuDgG2ywnmpTwBa8p
error=Error code: 400 - ... same message, request_id: req_011CdYmkRzYFj4URAB2hY44N

19:12:08 onward: work-item heartbeat every ~30s, indefinitely
```

The first POST is the writer's `write` result and succeeds. The second and third are the grader's `bash` and `read` results and are rejected.

## The rejected events are present in the session

`GET /v1/sessions/sesn_01JeEaW44N8VQs3r23sHqaAG/events` returns both of them:

```json
{"evaluated_permission": "allow", "id": "sevt_01C7v5e8VgTT71gnLSYkUnn3", "input": {"command": "ls -la /workspace/ 2>&1 | head -50"}, "name": "bash", "processed_at": "2026-07-30T19:11:44.005467Z", "session_thread_id": "sthr_01Xc5CCvKXaeFNReSQFyiexs", "type": "agent.tool_use"}
{"evaluated_permission": "allow", "id": "sevt_015ZVxsWuDgG2ywnmpTwBa8p", "input": {"file_path": "/workspace/probe.txt"}, "name": "read", "processed_at": "2026-07-30T19:11:44.105355Z", "session_thread_id": "sthr_01Xc5CCvKXaeFNReSQFyiexs", "type": "agent.tool_use"}
```

The session's outcome, two hours later:

```json
{"outcome_id": "outc_01BScKAoJTMYrrc1wF6jpcer", "result": "pending", "iteration": 0, "completed_at": null, "explanation": null}
```

Across four self-hosted runs the arithmetic is identical every time: total `agent.tool_use` minus total `user.tool_result` equals exactly 2, and the two unanswered calls are always the grader's.

## The grader's thread is not addressable

The `session_thread_id` on those events is `sthr_01Xc5CCvKXaeFNReSQFyiexs`. `GET /v1/sessions/{id}/threads` does not return it — it lists only the writer thread, `sthr_01JoM7eNWheWGhMvV5QFQfZd`. Retrieving it directly 404s:

```
GET /v1/sessions/sesn_01JeEaW44N8VQs3r23sHqaAG/threads/sthr_01Xc5CCvKXaeFNReSQFyiexs
404 {"type": "not_found_error", "message": "Thread not found: sthr_01Xc5CCvKXaeFNReSQFyiexs"}
```

So the grader's thread is observable through session events but not through the threads API.

## Why there is no client-side workaround

If the 400 means that a result for a grader-thread tool call must be scoped to that thread, the SDK offers no way to express it:

- `BetaManagedAgentsUserToolResultEventParams` carries `tool_use_id`, `content`, and `is_error`. There is no `session_thread_id`. Same for the custom-tool variant.
- `resources/beta/sessions/threads/events.py` exposes `list` and `stream` only. There is no `send`.
- `SessionToolRunner` never references `session_thread_id`. It reads `events.list(session_id)` and `events.stream(session_id)` and posts to `events.send(session_id)`.

The worker has no other move available to it.

## Secondary: the grader's tool calls are re-executed indefinitely

Because the POST fails, no `user.tool_result` event is ever recorded, so `_answered` never contains the id. On every stream reconnect, `_reconcile` re-lists history, finds the call still unanswered, and executes it again. On a worker that stayed up for several hours I watched the same grader `tool_use_id`s re-run on each reconnect, each followed by the same 400.

For a grader that only inspects state this is wasted work, but the tools are general-purpose (`bash`, `write`, `edit`) and the re-execution is unbounded.

`SEND_RETRIES = 3` applies within a single attempt, after which the runner logs at ERROR and abandons the call. Nothing about the failure appears in the session event stream, so from the API side a permanently broken run is indistinguishable from a slow grader.

## Ruled out

To save you the first few replies, all of these were tested and are not the cause:

- **Rubric length.** Tightened from 1,388 to 745 words; stalled identically. The 745-word version is the one that graded cleanly on cloud.
- **Criterion count.** Seven criteria, each decomposed and scored correctly on cloud.
- **Deliverable location.** The rubric targets `/workspace`, which matches the self-hosted guidance that the `/mnt/session/outputs` instruction is omitted and deliverables land under the working directory.
- **Confirmation gating.** Every `agent.tool_use` event in these sessions carries `evaluated_permission: "allow"`, so `_route_tool_event` is not holding them awaiting a `user.tool_confirmation`.
- **Worker health.** The run above is on a worker restarted immediately beforehand, polling cleanly, serving only this session.

## Identifiers

| Role | Session | Environment |
| --- | --- | --- |
| Clean repro, minimal rubric | `sesn_01JeEaW44N8VQs3r23sHqaAG` | `env_01JPL6GM5GxUXcfLkyji11sb` (self-hosted) |
| Stall, seven-criterion rubric | `sesn_01Bc96NBxTeHwhWS6hok5YDL` | `env_01PCEAkT6FTZgo97u3zvEQyS` (self-hosted) |
| Stall, seven-criterion rubric | `sesn_01SDHETG8r6ckbqpmU2k1r3j` | `env_01DVvdTG8qCfNBMRQ7CRq7UR` (self-hosted) |
| Stall, tightened rubric | `sesn_01MKrw6ELdwGvNP64W5s6kJL` | `env_01JPL6GM5GxUXcfLkyji11sb` (self-hosted) |
| Control, graded successfully | `sesn_01SRfYcwC4GsX4bathVPAThh` | `env_01F4KWpuuBJpqPJwb4ysH1pD` (`type: cloud`) |

The control environment is retained in case you want its trace.

## What would help

Confirm whether a tool result for a grader-thread tool call is meant to be posted session-scoped. If it is, the 400 looks like a server-side bug in how `tool_use_id` is resolved for non-writer threads. If it is not, then the SDK is missing the ability to post a thread-scoped result, and a self-hosted worker cannot service an outcome grader at all.

For what it's worth, nothing under `examples/` or `tests/` in this repository mentions outcomes, and no example combines a self-hosted environment with an outcome. That is why I suspect this combination is untested rather than deliberately unsupported. Filing here rather than elsewhere following the precedent of #1779.

🤖 Co-authored with Claude Code

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.