fix(openai): artifact selector 스트림을 분류 전 버퍼링한다

This commit is contained in:
toki 2026-08-15 14:29:11 +09:00
parent 85c6fc837c
commit 1f1be982c9
2 changed files with 41 additions and 1 deletions

View file

@ -603,7 +603,10 @@ func (c *anthropicHotPathCodec) runInitialPresetTurn(
gate hotPathSelectorGate
err error
)
if c.stream {
selectorState := s.artifactFrontiers.selectorInstructionState(
runMeta["iop_logical_request_id"], s.edgeIDValue(),
)
if c.stream && selectorState == selectorInstructionNone {
outer := c.callerOuterTurn("", hotPathOutputTokenCap(runMeta))
if err := c.prepareProgressiveWriter(w, outer, false); err != nil {
return stage, false, err
@ -612,6 +615,8 @@ func (c *anthropicHotPathCodec) runInitialPresetTurn(
r.Context(), dispatch, "anthropic", runMeta["iop_stage_id"], result, outer,
)
} else {
// Artifact selector calls are private IOP control data. Keep them buffered
// until classification expands them into caller-visible workspace writes.
stage, gate, err = s.collectPresetSelectorResult(r.Context(), dispatch, "anthropic", result)
}
if err != nil {

View file

@ -12,6 +12,7 @@ import (
"testing"
"time"
"iop/packages/go/config"
iop "iop/proto/gen/iop"
)
@ -150,6 +151,40 @@ func TestHotPathAnthropicDirectStreamPreservesEmptyToolInput(t *testing.T) {
assertHotPathWaiting(t, srv, "msg-empty-tool-tool-1", "provider-zero-arg-tool")
}
func TestHotPathAnthropicArtifactSelectorBuffersPrivateCalls(t *testing.T) {
fixture := newScriptedLightFixture(t, "anthropic", false)
alternative := scriptedLightWorkspaceAlternative()
write := alternative.Operations["write"]
write.CreatesParents = true
alternative.Operations["write"] = write
preset := hotPathSelectorPreset([]string{config.ModeDirect, config.ModeLight})
preset.WorkspaceTools = []config.ExecutionWorkspaceToolAlternative{alternative}
fixture.server.SetExecutionPresets([]config.ExecutionPreset{preset})
fixture.service.responses[0] = func(requestID string) string {
return scriptedArtifactPair("anthropic", requestID)
}
response := fixture.requestWithOptions(64, true)
if response.Code != http.StatusOK || strings.Contains(response.Body.String(), `"type":"error"`) ||
strings.Contains(response.Body.String(), hotPathArtifactPairToolName) {
t.Fatalf("artifact selector leaked or failed: status=%d body=%s", response.Code, response.Body.String())
}
events := decodeHotPathAnthropicSSE(t, response.Body.String())
var writeCalls int
for _, event := range events {
if event.name != "content_block_start" {
continue
}
block := hotPathAnthropicMap(t, event.payload["content_block"])
if block["type"] == "tool_use" && block["name"] == "write_file" {
writeCalls++
}
}
if writeCalls != 2 {
t.Fatalf("caller-visible write calls=%d, want 2; body=%s", writeCalls, response.Body.String())
}
}
func TestHotPathAnthropicLightStreamAggregatesStages(t *testing.T) {
fixture := newScriptedLightFixture(t, "anthropic", false)
fixture.service.responses[4] = func(requestID string) string {