iop/apps/edge/internal/openai/artifact_pair.go
toki 495996fee4 feat(openai): 핫패스 에이전트 실행 경로를 확장한다
Anthropic·Chat 게이트와 관찰·종료 제어를 통합하고 관련 계약·검증 산출물을 반영한다.
2026-08-06 00:09:24 +09:00

708 lines
25 KiB
Go

package openai
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"sync"
)
const defaultArtifactFrontierCapacity = 1024
type artifactFrontierPhase string
const (
artifactPhasePinned artifactFrontierPhase = "pinned"
artifactPhasePreparePending artifactFrontierPhase = "prepare_pending"
artifactPhasePairReady artifactFrontierPhase = "pair_ready"
artifactPhasePairPending artifactFrontierPhase = "pair_pending"
artifactPhaseLocalEligible artifactFrontierPhase = "local_eligible"
)
type artifactDispositionKind string
const (
artifactDispositionResumeSelector artifactDispositionKind = "resume_selector"
artifactDispositionLocalEligible artifactDispositionKind = "local_eligible"
)
type artifactDisposition struct {
Kind artifactDispositionKind
SelectorStageID string
PrimaryError *hotPathEndpointError
}
// presetIngressResult carries a control decision that the public handler must
// consume before it can construct or submit another provider-pool request.
// It deliberately keeps the artifact disposition out of caller-controlled
// metadata, which is only a transport for trusted logical request IDs.
type presetIngressResult struct {
Artifact artifactDisposition
Light hotPathLightDisposition
Cleanup *hotPathCleanupTurn
Terminal *hotPathTerminalIntent
}
func (r presetIngressResult) localStageEligible() bool {
return r.Artifact.Kind == artifactDispositionLocalEligible
}
func (r presetIngressResult) lightStageContinuation() bool {
return r.Light.RequestID != "" && r.Light.Terminal == nil
}
func (r presetIngressResult) cleanupIssued() bool {
return r.Cleanup != nil
}
func (r presetIngressResult) terminalReady() bool {
return r.Terminal != nil
}
type artifactFrontierRecord struct {
requestID string
ownerEdgeID string
principalRef string
protocol string
selectorStageID string
lineage logicalRequestLineage
binding *workspaceBinding
phase artifactFrontierPhase
pending map[string]*workspaceEncodedPayload
pendingHash string
consumedHashes map[string]struct{}
consumedIDs map[string]struct{}
}
// artifactFrontierStore owns the request-local workspace binding and the sole
// prepare/pair receipt frontier. Its fixed capacity prevents abandoned caller
// continuations from growing Edge-local state without bound.
type artifactFrontierStore struct {
mu sync.Mutex
capacity int
records map[string]*artifactFrontierRecord
}
func newArtifactFrontierStore(capacity int) *artifactFrontierStore {
if capacity <= 0 {
capacity = defaultArtifactFrontierCapacity
}
return &artifactFrontierStore{capacity: capacity, records: make(map[string]*artifactFrontierRecord)}
}
func (s *artifactFrontierStore) pin(
requestID, ownerEdgeID, principalRef, protocol, selectorStageID string,
lineage logicalRequestLineage,
binding *workspaceBinding,
) error {
if s == nil || binding == nil {
return fmt.Errorf("artifact frontier binding is unavailable")
}
if !validLogicalRequestID(requestID) || !validLogicalRequestID(selectorStageID) {
return fmt.Errorf("artifact frontier identity is invalid")
}
if strings.TrimSpace(ownerEdgeID) == "" || strings.TrimSpace(principalRef) == "" {
return fmt.Errorf("artifact frontier owner and principal are required")
}
if !artifactProtocolMatchesLineage(protocol, lineage) {
return fmt.Errorf("artifact frontier protocol does not match request lineage")
}
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.records[requestID]; exists {
return fmt.Errorf("artifact frontier already exists")
}
if len(s.records) >= s.capacity {
return fmt.Errorf("artifact frontier capacity reached")
}
s.records[requestID] = &artifactFrontierRecord{
requestID: requestID, ownerEdgeID: ownerEdgeID, principalRef: principalRef,
protocol: protocol, selectorStageID: selectorStageID, lineage: lineage,
binding: binding, phase: artifactPhasePinned,
consumedHashes: make(map[string]struct{}), consumedIDs: make(map[string]struct{}),
}
return nil
}
func artifactProtocolMatchesLineage(protocol string, lineage logicalRequestLineage) bool {
switch protocol {
case "openai":
return lineage.Endpoint == logicalRequestEndpointChat
case "anthropic":
return lineage.Endpoint == logicalRequestEndpointAnthropic
default:
return false
}
}
func (s *artifactFrontierStore) remove(requestID, ownerEdgeID string) {
if s == nil || requestID == "" {
return
}
s.mu.Lock()
defer s.mu.Unlock()
if record := s.records[requestID]; record != nil && record.ownerEdgeID == ownerEdgeID {
delete(s.records, requestID)
}
}
func (s *artifactFrontierStore) has(requestID, ownerEdgeID string) bool {
if s == nil || requestID == "" {
return false
}
s.mu.Lock()
defer s.mu.Unlock()
record := s.records[requestID]
return record != nil && record.ownerEdgeID == ownerEdgeID
}
// pairRequired reports whether the retained selector may only author the
// exact Plan/Review pair. The store owns the phase and keeps this observation
// lock-safe so a handler cannot infer it from untrusted request metadata.
func (s *artifactFrontierStore) pairRequired(requestID, ownerEdgeID string) bool {
if s == nil || requestID == "" {
return false
}
s.mu.Lock()
defer s.mu.Unlock()
record := s.records[requestID]
return record != nil && record.ownerEdgeID == ownerEdgeID && record.phase == artifactPhasePairReady
}
func (s *artifactFrontierStore) issue(
turn *hotPathTurn,
output normalizedStageOutput,
coordinator *logicalRequestCoordinator,
) (normalizedStageOutput, error) {
if s == nil || coordinator == nil || turn == nil {
return normalizedStageOutput{}, fmt.Errorf("artifact frontier is unavailable")
}
s.mu.Lock()
defer s.mu.Unlock()
record := s.records[turn.RequestID]
if record == nil {
return normalizedStageOutput{}, fmt.Errorf("artifact frontier is not pinned")
}
if record.ownerEdgeID != turn.OwnerEdgeID || record.principalRef != turn.PrincipalRef {
return normalizedStageOutput{}, fmt.Errorf("artifact frontier owner or principal mismatch")
}
if record.protocol != turn.Protocol || record.selectorStageID != turn.StageID {
return normalizedStageOutput{}, fmt.Errorf("artifact frontier selector stage mismatch")
}
wantPrepare := false
switch record.phase {
case artifactPhasePinned:
wantPrepare = !record.binding.createsParents()
case artifactPhasePairReady:
wantPrepare = false
default:
return normalizedStageOutput{}, fmt.Errorf("artifact frontier already has a pending or consumed turn")
}
mapped, payloads, err := mapArtifactOutput(record, output, wantPrepare, coordinator)
if err != nil {
return normalizedStageOutput{}, err
}
if turn.Protocol == "anthropic" {
mapped.TerminalReason = "tool_use"
}
if turn.OuterTurn != nil {
ctx := context.Background()
if turn.Request != nil {
ctx = turn.Request.Context()
}
if !output.ProgressivelyReleased {
if err := runHotPathCollectedStage(ctx, turn.OuterTurn, turn.StageID, mapped); err != nil {
return normalizedStageOutput{}, fmt.Errorf("collect artifact outer turn: %w", err)
}
}
visible := hotPathCompatibilityOutput(turn.OuterTurn, mapped, turn.Protocol)
if len(visible.ToolCalls) == 0 && turn.OuterTurn.outputBudget().Exhausted {
turn.OuterTurn.commitLengthTerminal()
return hotPathCompatibilityOutput(turn.OuterTurn, mapped, turn.Protocol), nil
}
if err := turn.OuterTurn.projectToolIdentities(mapped.ToolCalls); err != nil {
return normalizedStageOutput{}, err
}
mapped = hotPathCompatibilityOutput(turn.OuterTurn, mapped, turn.Protocol)
}
issuedHash, err := directIssuedCallHash(turn.Protocol, mapped)
if err != nil {
return normalizedStageOutput{}, fmt.Errorf("fingerprint artifact calls: %w", err)
}
expected := make([]logicalRequestExpectedTool, 0, len(mapped.ToolCalls))
for _, call := range mapped.ToolCalls {
expected = append(expected, logicalRequestExpectedTool{
PublicCallID: call.ID, ProviderCallID: call.ProviderCallID,
})
}
if _, err := coordinator.awaitToolResults(
turn.RequestID, turn.OwnerEdgeID, turn.StageID, expected, issuedHash,
); err != nil {
return normalizedStageOutput{}, fmt.Errorf("await artifact results: %w", err)
}
record.pending = payloads
record.pendingHash = issuedHash
if wantPrepare {
record.phase = artifactPhasePreparePending
} else {
record.phase = artifactPhasePairPending
}
return mapped, nil
}
func mapArtifactOutput(
record *artifactFrontierRecord,
output normalizedStageOutput,
wantPrepare bool,
coordinator *logicalRequestCoordinator,
) (normalizedStageOutput, map[string]*workspaceEncodedPayload, error) {
issued := newReservedPaths(record.requestID)
calls := append([]normalizedToolCall(nil), output.ToolCalls...)
if wantPrepare {
if len(calls) != 1 {
return normalizedStageOutput{}, nil, fmt.Errorf("artifact prepare turn must contain exactly one call")
}
mapped, payload, err := mapArtifactCall(record.binding, calls[0], opKindPrepare, issued.JobDir, coordinator)
if err != nil {
return normalizedStageOutput{}, nil, err
}
return artifactResponseOutput(output, []normalizedToolCall{mapped}), map[string]*workspaceEncodedPayload{mapped.ID: payload}, nil
}
if len(calls) != 2 {
return normalizedStageOutput{}, nil, fmt.Errorf("artifact pair turn must contain exactly two calls")
}
byPath := make(map[string]normalizedToolCall, len(calls))
for _, call := range calls {
paths := reservedPathsFromToolCall(call)
if len(paths) != 1 {
return normalizedStageOutput{}, nil, fmt.Errorf("artifact pair call has an ambiguous reserved path")
}
clean := cleanRelativePath(paths[0])
if _, duplicate := byPath[clean]; duplicate {
return normalizedStageOutput{}, nil, fmt.Errorf("artifact pair contains a duplicate path")
}
byPath[clean] = call
}
orderedPaths := []string{issued.PlanPath, issued.ReviewPath}
mappedCalls := make([]normalizedToolCall, 0, 2)
payloads := make(map[string]*workspaceEncodedPayload, 2)
for _, requiredPath := range orderedPaths {
call, ok := byPath[cleanRelativePath(requiredPath)]
if !ok {
return normalizedStageOutput{}, nil, fmt.Errorf("artifact pair is missing reserved path %q", requiredPath)
}
mapped, payload, err := mapArtifactCall(record.binding, call, opKindWrite, requiredPath, coordinator)
if err != nil {
return normalizedStageOutput{}, nil, err
}
mappedCalls = append(mappedCalls, mapped)
payloads[mapped.ID] = payload
}
return artifactResponseOutput(output, mappedCalls), payloads, nil
}
func mapArtifactCall(
binding *workspaceBinding,
providerCall normalizedToolCall,
operation workspaceOperationKind,
requiredPath string,
coordinator *logicalRequestCoordinator,
) (normalizedToolCall, *workspaceEncodedPayload, error) {
providerID := strings.TrimSpace(providerCall.ProviderCallID)
if providerID == "" {
providerID = strings.TrimSpace(providerCall.ID)
}
if !validLogicalRequestID(providerID) {
return normalizedToolCall{}, nil, fmt.Errorf("artifact provider tool id is invalid")
}
publicID, err := coordinator.newCallID()
if err != nil {
return normalizedToolCall{}, nil, fmt.Errorf("allocate artifact public tool id: %w", err)
}
providerCall.ID = publicID
providerCall.ProviderCallID = providerID
payload, err := encodeWorkspaceCall(binding, operation, providerCall)
if err != nil {
return normalizedToolCall{}, nil, fmt.Errorf("encode artifact %s call: %w", operation, err)
}
if payload.safePath != cleanRelativePath(requiredPath) {
return normalizedToolCall{}, nil, fmt.Errorf("artifact call targets %q, want %q", payload.safePath, requiredPath)
}
rawArgs, err := json.Marshal(payload.structuredArgs)
if err != nil {
return normalizedToolCall{}, nil, fmt.Errorf("encode artifact arguments: %w", err)
}
mapped := normalizedToolCall{
ID: publicID, ProviderCallID: providerID, Name: payload.toolName,
Arguments: cloneAnyMap(payload.structuredArgs), RawArgs: string(rawArgs), Path: payload.safePath,
}
return mapped, payload, nil
}
func artifactResponseOutput(source normalizedStageOutput, calls []normalizedToolCall) normalizedStageOutput {
return normalizedStageOutput{
ResponseID: source.ResponseID, Created: source.Created, Content: source.Content,
Reasoning: source.Reasoning, ReasoningSignature: source.ReasoningSignature, ToolCalls: calls,
TerminalReason: "tool_calls", Usage: cloneRawJSON(source.Usage), OpenAIUsage: source.OpenAIUsage,
}
}
func (s *Server) runArtifactPairTurn(turn *hotPathTurn, output normalizedStageOutput, gate hotPathSelectorGate) error {
if turn == nil {
return fmt.Errorf("artifact turn is unavailable")
}
if strings.TrimSpace(turn.PrincipalRef) == "" {
turn.PrincipalRef = strings.TrimSpace(turn.Dispatch.PrincipalRef)
if turn.PrincipalRef == "" {
turn.PrincipalRef = "anonymous"
}
}
mapped, err := s.artifactFrontiers.issue(turn, output, s.requestCoordinator)
if err != nil {
s.terminalPresetRequest(turn.RequestID, turn.OwnerEdgeID)
return s.writeDirectError(turn, 400, "invalid_request_error", fmt.Sprintf("artifact turn rejected: %v", err))
}
if turn.OuterTurn != nil && len(mapped.ToolCalls) == 0 && turn.OuterTurn.outputBudget().Exhausted {
s.terminalPresetRequest(turn.RequestID, turn.OwnerEdgeID)
return s.writeDirectResponse(turn, mapped)
}
if s.lightFlows.has(turn.RequestID, turn.OwnerEdgeID) {
if err := s.lightFlows.commitSelector(turn.RequestID, turn.OwnerEdgeID, output, gate); err != nil {
s.terminalPresetRequest(turn.RequestID, turn.OwnerEdgeID)
return s.writeDirectError(turn, 400, "invalid_request_error", fmt.Sprintf("light selector commit rejected: %v", err))
}
}
if turn.OuterTurn != nil {
turn.OuterTurn.commitTerminalSuccess(mapped.TerminalReason)
mapped = hotPathCompatibilityOutput(turn.OuterTurn, mapped, turn.Protocol)
}
if err := s.writeDirectResponse(turn, mapped); err != nil {
s.terminalPresetRequest(turn.RequestID, turn.OwnerEdgeID)
return err
}
return nil
}
func (s *Server) applyArtifactDisposition(
snap logicalRequestSnapshot,
disposition artifactDisposition,
metadata map[string]string,
) error {
if metadata == nil {
return fmt.Errorf("artifact continuation metadata is unavailable")
}
callID, err := s.requestCoordinator.newCallID()
if err != nil {
return err
}
metadata["iop_logical_request_id"] = snap.ID
metadata["iop_call_id"] = callID
metadata["iop_stage_id"] = disposition.SelectorStageID
return nil
}
func (s *artifactFrontierStore) consumeChat(
ownerEdgeID, principalRef string,
rawBody []byte,
lineage logicalRequestContinuationLineage,
coordinator *logicalRequestCoordinator,
lightFlows *hotPathLightStore,
) (logicalRequestSnapshot, artifactDisposition, bool, error) {
results, err := decodeChatWorkspaceResults(rawBody)
if err != nil {
return logicalRequestSnapshot{}, artifactDisposition{}, true, err
}
return s.consume(ownerEdgeID, principalRef, "openai", lineage, results, coordinator, lightFlows)
}
func (s *artifactFrontierStore) consumeAnthropic(
ownerEdgeID, principalRef string,
rawBody []byte,
lineage logicalRequestContinuationLineage,
coordinator *logicalRequestCoordinator,
lightFlows *hotPathLightStore,
) (logicalRequestSnapshot, artifactDisposition, bool, error) {
results, err := decodeAnthropicWorkspaceResults(rawBody)
if err != nil {
return logicalRequestSnapshot{}, artifactDisposition{}, true, err
}
return s.consume(ownerEdgeID, principalRef, "anthropic", lineage, results, coordinator, lightFlows)
}
func (s *artifactFrontierStore) consume(
ownerEdgeID, principalRef, protocol string,
lineage logicalRequestContinuationLineage,
results []workspaceResult,
coordinator *logicalRequestCoordinator,
lightFlows *hotPathLightStore,
) (logicalRequestSnapshot, artifactDisposition, bool, error) {
if s == nil || coordinator == nil {
return logicalRequestSnapshot{}, artifactDisposition{}, false, nil
}
s.mu.Lock()
defer s.mu.Unlock()
record, matched, err := s.matchRecordLocked(ownerEdgeID, principalRef, protocol, lineage)
if !matched || err != nil {
return logicalRequestSnapshot{}, artifactDisposition{}, matched, err
}
if record.pending == nil || record.pendingHash == "" {
return logicalRequestSnapshot{}, artifactDisposition{}, true, fmt.Errorf("artifact frontier has no pending calls")
}
if len(results) != len(record.pending) {
return logicalRequestSnapshot{}, artifactDisposition{}, true, fmt.Errorf("artifact result set size mismatch")
}
seen := make(map[string]struct{}, len(results))
var primaryFailure *hotPathEndpointError
for _, result := range results {
payload := record.pending[result.callID]
if payload == nil {
return logicalRequestSnapshot{}, artifactDisposition{}, true, fmt.Errorf("artifact result id is not in the pending frontier")
}
if _, duplicate := seen[result.callID]; duplicate {
return logicalRequestSnapshot{}, artifactDisposition{}, true, fmt.Errorf("artifact result id is duplicated")
}
seen[result.callID] = struct{}{}
receipt := matchResultReceipt(record.binding, payload, result)
if !receipt.matched {
// A valid request lineage, pending call, and immutable issue
// correlation route an exact receipt-matcher failure to primary
// cleanup without trusting the result as a success. An invalid issue
// correlation, or an opaque/malformed result that is not an exact
// caller report, stays an immediate fail-closed rejection.
if matchResultCorrelation(record.binding, payload, result) != "" || !workspaceResultIsExact(result) {
return logicalRequestSnapshot{}, artifactDisposition{}, true,
fmt.Errorf("artifact receipt rejected: %s", receipt.mismatchReason)
}
if primaryFailure == nil {
primaryFailure = &hotPathEndpointError{
Status: http.StatusBadRequest, Type: "invalid_request_error",
Message: "artifact continuation rejected: artifact receipt rejected: " + receipt.mismatchReason,
}
}
continue
}
}
if primaryFailure != nil && (lightFlows == nil || !lightFlows.has(record.requestID, record.ownerEdgeID)) {
return logicalRequestSnapshot{}, artifactDisposition{}, true, fmt.Errorf("artifact receipt rejected: result contains an explicit error signal")
}
snap, err := coordinator.consumeContinuationByLineage(ownerEdgeID, principalRef, lineage)
if err != nil {
return logicalRequestSnapshot{}, artifactDisposition{}, true, err
}
for id := range record.pending {
record.consumedIDs[id] = struct{}{}
}
record.consumedHashes[record.pendingHash] = struct{}{}
record.pending = nil
record.pendingHash = ""
record.lineage = lineage.Committed
if primaryFailure != nil {
return snap, artifactDisposition{
Kind: artifactDispositionLocalEligible, SelectorStageID: record.selectorStageID,
PrimaryError: primaryFailure,
}, true, nil
}
switch record.phase {
case artifactPhasePreparePending:
snap, err = coordinator.activateStage(record.requestID, record.ownerEdgeID, record.selectorStageID)
if err != nil {
return logicalRequestSnapshot{}, artifactDisposition{}, true, fmt.Errorf("resume artifact selector stage: %w", err)
}
record.phase = artifactPhasePairReady
return snap, artifactDisposition{Kind: artifactDispositionResumeSelector, SelectorStageID: record.selectorStageID}, true, nil
case artifactPhasePairPending:
record.phase = artifactPhaseLocalEligible
return snap, artifactDisposition{Kind: artifactDispositionLocalEligible, SelectorStageID: record.selectorStageID}, true, nil
default:
return logicalRequestSnapshot{}, artifactDisposition{}, true, fmt.Errorf("artifact frontier phase cannot consume results")
}
}
func (s *artifactFrontierStore) matchRecordLocked(
ownerEdgeID, principalRef, protocol string,
lineage logicalRequestContinuationLineage,
) (*artifactFrontierRecord, bool, error) {
var candidates []*artifactFrontierRecord
for _, record := range s.records {
pendingRelated := record.pending != nil && (record.pendingHash == lineage.IssuedCallHash || artifactIDsIntersect(record, lineage.ResultIDs) || record.lineage == lineage.Prefix)
_, consumedHash := record.consumedHashes[lineage.IssuedCallHash]
if pendingRelated || consumedHash || artifactConsumedIDsIntersect(record, lineage.ResultIDs) {
candidates = append(candidates, record)
}
}
if len(candidates) == 0 {
return nil, false, nil
}
for _, record := range candidates {
if _, replay := record.consumedHashes[lineage.IssuedCallHash]; replay {
return nil, true, fmt.Errorf("artifact frontier replay rejected")
}
}
for _, record := range candidates {
if record.pendingHash != lineage.IssuedCallHash {
continue
}
if record.ownerEdgeID != ownerEdgeID {
return nil, true, errLogicalRequestOwnerMismatch
}
if record.principalRef != principalRef {
return nil, true, errLogicalRequestPrincipal
}
if record.protocol != protocol || record.lineage != lineage.Prefix {
return nil, true, errLogicalRequestLineage
}
return record, true, nil
}
for _, record := range candidates {
if record.ownerEdgeID == ownerEdgeID && record.principalRef == principalRef && record.protocol == protocol && record.lineage == lineage.Prefix {
return record, true, nil
}
}
return nil, true, errLogicalRequestLineage
}
func artifactIDsIntersect(record *artifactFrontierRecord, ids []string) bool {
for _, id := range ids {
if record.pending[id] != nil {
return true
}
}
return false
}
func artifactConsumedIDsIntersect(record *artifactFrontierRecord, ids []string) bool {
for _, id := range ids {
if _, consumed := record.consumedIDs[id]; consumed {
return true
}
}
return false
}
func decodeChatWorkspaceResults(rawBody []byte) ([]workspaceResult, error) {
var envelope struct {
Messages []struct {
Role string `json:"role"`
ToolCallID string `json:"tool_call_id"`
Content json.RawMessage `json:"content"`
} `json:"messages"`
}
if err := json.Unmarshal(rawBody, &envelope); err != nil {
return nil, fmt.Errorf("decode Chat artifact results: %w", err)
}
var reversed []workspaceResult
for i := len(envelope.Messages) - 1; i >= 0; i-- {
message := envelope.Messages[i]
if message.Role != "tool" {
break
}
body, err := workspaceResultBody(message.Content)
if err != nil {
return nil, fmt.Errorf("decode Chat tool result %q: %w", message.ToolCallID, err)
}
reversed = append(reversed, workspaceResult{callID: message.ToolCallID, status: "success", body: body})
}
results := make([]workspaceResult, len(reversed))
for i := range reversed {
results[len(reversed)-1-i] = reversed[i]
}
if len(results) == 0 {
return nil, fmt.Errorf("Chat artifact continuation has no tool results")
}
return results, nil
}
func decodeAnthropicWorkspaceResults(rawBody []byte) ([]workspaceResult, error) {
var envelope struct {
Messages []struct {
Role string `json:"role"`
Content json.RawMessage `json:"content"`
} `json:"messages"`
}
if err := json.Unmarshal(rawBody, &envelope); err != nil {
return nil, fmt.Errorf("decode Messages artifact results: %w", err)
}
if len(envelope.Messages) == 0 || envelope.Messages[len(envelope.Messages)-1].Role != "user" {
return nil, fmt.Errorf("Messages artifact continuation has no trailing user results")
}
var blocks []struct {
Type string `json:"type"`
ToolUseID string `json:"tool_use_id"`
Content json.RawMessage `json:"content"`
IsError bool `json:"is_error,omitempty"`
}
if err := json.Unmarshal(envelope.Messages[len(envelope.Messages)-1].Content, &blocks); err != nil {
return nil, fmt.Errorf("decode Messages artifact result blocks: %w", err)
}
results := make([]workspaceResult, 0, len(blocks))
for _, block := range blocks {
if block.Type != "tool_result" {
return nil, fmt.Errorf("Messages artifact result contains non-tool_result block")
}
body, err := workspaceResultBody(block.Content)
if err != nil {
return nil, fmt.Errorf("decode Messages tool result %q: %w", block.ToolUseID, err)
}
status := "success"
if block.IsError {
status = "error"
}
results = append(results, workspaceResult{callID: block.ToolUseID, status: status, body: body})
}
if len(results) == 0 {
return nil, fmt.Errorf("Messages artifact continuation has no tool results")
}
return results, nil
}
func workspaceResultBody(raw json.RawMessage) (json.RawMessage, error) {
trimmed := strings.TrimSpace(string(raw))
if trimmed == "" || trimmed == "null" {
return nil, nil
}
var text string
if err := json.Unmarshal(raw, &text); err == nil {
return json.RawMessage(strings.TrimSpace(text)), nil
}
var value any
if err := json.Unmarshal(raw, &value); err != nil {
return nil, err
}
return append(json.RawMessage(nil), raw...), nil
}
func decodeArtifactTools(protocol string, rawBody []byte) (any, error) {
switch protocol {
case "openai":
var envelope struct {
Tools []any `json:"tools"`
}
decoder := json.NewDecoder(strings.NewReader(string(rawBody)))
decoder.UseNumber()
if err := decoder.Decode(&envelope); err != nil {
return nil, fmt.Errorf("decode Chat workspace tools: %w", err)
}
return envelope.Tools, nil
case "anthropic":
var envelope struct {
Tools []anthropicTool `json:"tools"`
}
if err := json.Unmarshal(rawBody, &envelope); err != nil {
return nil, fmt.Errorf("decode Messages workspace tools: %w", err)
}
return envelope.Tools, nil
default:
return nil, fmt.Errorf("unsupported artifact protocol %q", protocol)
}
}