Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/docs/content/docs/concepts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,10 @@ Do not add product users, RBAC, cross-Project shared assets, administrator execu
private-protocol compatibility to this management model. Existing public
execution and Runtime ownership rules remain in [CONTRIBUTING.md](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/CONTRIBUTING.md).

Native failure classification is adapter-owned and uses finite structured native
values. Optional Runtime error metadata is normalized once and retained through
normal delivery and journal draining; it never replaces Core terminal authority,
cancellation receipts, Usage or native identity. See
[native failure classification](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/native-error-classification.md).

[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/docs/design-principles.md)
4 changes: 2 additions & 2 deletions apps/docs/content/guide-sources.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"revision": "f36ac3592bfe9f77a2172680aa52b14c6f525062",
"sources": {
"docs/getting-started/README.md": "e4ca3bcbfdcbf3832c1d9e0525f98083ca11a4e42148f9f45090bbe1bc0f2b5c",
"docs/design-principles.md": "5e1bff3cdcc77cd02ad692f67977cb4e0a5654989982d722bac0e3c79dbf82cb",
"docs/design-principles.md": "71285ff0b9a7796481b9fe42f7c4b71d0377c767157980ca6386792d9ca8951b",
"docs/web/architecture.md": "80bdb8053c06c2b2030b193aae17d5434545f9c72af50e9d3df3ade522fe25ed",
"docs/getting-started/install.md": "15cfe81d4005a7337ac82bf6c35a8b441e9f9b0f2a2299021ad6c539f3f336a3",
"docs/configuration.md": "e911757a03fbce4b803a284c8bb40bdb7000ca2f70e27938706dcdcd74bc7f9a",
Expand All @@ -24,7 +24,7 @@
"outputs": {
"content/docs/index.mdx": "75339c35292bb1f3c6b4313d717b82686bee8caa265bb575fcecbd0ba33bb780",
"content/docs/index.zh.mdx": "5bdd41893946f1d229eb4cf1599a97f2e38cb9e09722d2a69f62053376ac88a9",
"content/docs/concepts.mdx": "304e18737cdf807562501981e5b8312466a7c7ce29db3bf4823075850dec10c4",
"content/docs/concepts.mdx": "2c2516963cccafed4f66083201a9f84e8d12c4d7f4406ce06179bc26616ea019",
"content/docs/concepts.zh.mdx": "c60644f395dbac5ee607cdb053249e05ec8352d28de97ef1c38d6c1e4443bbb1",
"content/docs/execution-model.mdx": "8dd05250255fdb39a4aee6ffa51a16379fbd19d2761f510e1ad9cc3a193f750c",
"content/docs/execution-model.zh.mdx": "9976a9203c6a83d49c2c753e3a8528a369078e87aeb88850654da948ac8c17e1",
Expand Down
52 changes: 52 additions & 0 deletions apps/docs/openapi/core-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4357,6 +4357,32 @@ components:
api.DiagnosticFailure:
properties:
code:
enum:
- harness_error
- model_provider_required
- runtime_unavailable
- runtime_disconnected
- runtime_preparation_failed
- execution_interrupted
- delivery_unconfirmed
- input_rejected
- executor_protocol_error
- core_storage_failed
- internal_error
- environment_connection_timeout
- environment_unavailable
- environment_provisioning_failed
- authentication_error
- rate_limit_exceeded
- usage_limit_exceeded
- server_overloaded
- server_error
- invalid_request
- resource_not_found
- request_timeout
- context_length_exceeded
- cyber_policy
- connection_failed
type: string
failed_at:
anyOf:
Expand Down Expand Up @@ -4667,6 +4693,32 @@ components:
api.SessionDiagnosticFailure:
properties:
code:
enum:
- harness_error
- model_provider_required
- runtime_unavailable
- runtime_disconnected
- runtime_preparation_failed
- execution_interrupted
- delivery_unconfirmed
- input_rejected
- executor_protocol_error
- core_storage_failed
- internal_error
- environment_connection_timeout
- environment_unavailable
- environment_provisioning_failed
- authentication_error
- rate_limit_exceeded
- usage_limit_exceeded
- server_overloaded
- server_error
- invalid_request
- resource_not_found
- request_timeout
- context_length_exceeded
- cyber_policy
- connection_failed
type: string
failed_at:
anyOf:
Expand Down
4 changes: 2 additions & 2 deletions apps/docs/openapi/sources.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"sources": {
"contracts/agents-api/openapi.yaml": "a8896e8b67d78ed0608c7cc5636077ec33f4b4895a04bd6cfb8b038afe941e01",
"contracts/agents-api/core.openapi.yaml": "ff8526ba681f7561e49cd8e1818ddb7c6e57ce044021b92d2d69d5b087356900",
"contracts/agents-api/core.openapi.yaml": "8fe158844c170eea78d4b24e30e7caa2adbedd58ffb4a863bdd9c98ecbb50618",
"contracts/agents-api/runtime.openapi.yaml": "23852205d3b45892f2de9ecf3d52518c0dd0808962c0c512f219b565aa7ccef2"
},
"outputs": {
Expand Down Expand Up @@ -40,7 +40,7 @@
"content/docs/api-reference/core/vaults.mdx": "12d0aac96cd4a3472ef4eaa287f7945f78862017956e79bfc3ed0ef8e772e78a",
"content/docs/api-reference/core/credentials.mdx": "27d586b9a8fd738b9947802f32c80458195370252886bc26abedccf2125ee981",
"content/docs/api-reference/core/sandbox-manager.mdx": "6be7c52e057365949f0247c18c710561d680847a955f78f1bbe57825d4d5f549",
"openapi/core-api.yaml": "18e100de2375835350d172e8a472db6966e36f1dd3aaae5e5d8132ba3039fa9c",
"openapi/core-api.yaml": "4d72411e4fc9945aba575a91b9c95e7231262e2749f87796646ef17768022238",
"content/docs/api-reference/core/meta.json": "588b163ed2de2100b0f8376688274f80698a6bf6ac037eef56d17c93fa8a5062",
"content/docs/api-reference/core/index.mdx": "ddf0a8d87a6c21761beac703a32f2a40486a1c74abb341bd726f40c44a866405",
"content/docs/api-reference/machine/sandbox-node.mdx": "5b367a08d43cea82686345d2ba1c75cc90627a7b2970fe42a0df9d77fb3e1e80",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//go:build unix

package claudesdk

import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"

"github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto"
)

func TestClassifiedBridgeFailurePreservesTerminalEvidence(t *testing.T) {
for _, mode := range []string{"valid", "wrong-session", "wrong-result", "success-usage", "cancelled", "unknown", "malformed-code", "after-terminal", "scanner-error", "process-error"} {
t.Run(mode, func(t *testing.T) {
root := t.TempDir()
t.Setenv("OAC_RUNTIME_HOME", root)
config := Config{Node: os.Args[0], Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state"), Env: []string{"GO_CLAUDE_SDK_HELPER=1", "SDK_HELPER_MODE=classified-" + mode, "GORACE=atexit_sleep_ms=0"}}
req := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("hello"), AgentSessionID: "native-session", AgentOptions: map[string]any{"model": "fake-model", "system_prompt": "instructions"}}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
out := make(chan proto.Envelope, 16)
s, err := NewFactory(config)(ctx, req, out)
if err != nil {
t.Fatal(err)
}
defer s.Cancel(context.Background())
var failure proto.ErrorPayload
var done proto.DonePayload
usage, errors, dones := 0, 0, 0
for event := range out {
switch event.Type {
case proto.TypeUsage:
usage++
case proto.TypeError:
errors++
_ = event.DecodePayload(&failure)
case proto.TypeDone:
dones++
_ = event.DecodePayload(&done)
}
}
want := ""
if mode == "valid" {
want = "authentication_error"
}
if errors != 1 || dones != 1 || usage != 1 || failure.Code != want || done.Metadata[proto.DoneMetaAgentSessionID] != "native-session" || done.Usage.Raw["claude_sdk_result"] == nil {
t.Fatalf("lost evidence: %d/%d/%d %+v %+v", errors, dones, usage, failure, done)
}
})
}
}

func runClassifiedFailureHelper(request startRequest, mode string, encode func(bridgeEvent)) {
mode = strings.TrimPrefix(mode, "classified-")
encode(bridgeEvent{Type: "input_ready", SessionID: request.Resume})
raw := strings.ReplaceAll(strings.ReplaceAll(usageFixture, `"subtype":"success"`, `"subtype":"error_during_execution"`), `"is_error":false`, `"is_error":true`)
if mode == "success-usage" {
raw = usageFixture
}
encode(bridgeEvent{Type: "usage", SessionID: request.Resume, ResultID: "result", Usage: json.RawMessage(raw)})
e := bridgeEvent{Type: "error", Code: "execution_failed", SessionID: request.Resume, ResultID: "result", EngineErrorCode: json.RawMessage(`"authentication_error"`)}
switch mode {
case "wrong-session":
e.SessionID = "foreign"
case "wrong-result":
e.ResultID = "foreign"
case "cancelled":
e.Code = "cancelled"
case "unknown":
e.EngineErrorCode = json.RawMessage(`"future"`)
case "malformed-code":
e.EngineErrorCode = json.RawMessage(`{"secret":"value"}`)
}
encode(e)
if mode == "process-error" {
os.Exit(7)
}
if mode == "after-terminal" {
fmt.Fprintln(os.Stdout, `{"type":"delta","delta":"late"}`)
}
if mode == "scanner-error" {
fmt.Fprintln(os.Stdout, strings.Repeat("x", 2*1024*1024))
}
}
70 changes: 47 additions & 23 deletions apps/parsar-daemon/internal/agent/claudesdk/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,23 +78,24 @@ func NewFactory(config Config) agent.Factory {
}

type bridgeEvent struct {
Fact json.RawMessage `json:"fact"`
InputID string `json:"input_id"`
ResultID string `json:"result_id"`
Usage json.RawMessage `json:"usage,omitempty"`
Type string `json:"type"`
Delta string `json:"delta"`
SessionID string `json:"session_id"`
Text string `json:"text"`
Code string `json:"code"`
ItemID string `json:"item_id"`
Message *proto.OutputMessagePayload `json:"message"`
Call *proto.FunctionCallPayload `json:"call"`
CallID string `json:"call_id"`
DeliveryID string `json:"delivery_id"`
ID string `json:"id"`
Stage string `json:"stage"`
Observation *proto.ToolObservation `json:"observation"`
EngineErrorCode json.RawMessage `json:"engine_error_code"`
Fact json.RawMessage `json:"fact"`
InputID string `json:"input_id"`
ResultID string `json:"result_id"`
Usage json.RawMessage `json:"usage,omitempty"`
Type string `json:"type"`
Delta string `json:"delta"`
SessionID string `json:"session_id"`
Text string `json:"text"`
Code string `json:"code"`
ItemID string `json:"item_id"`
Message *proto.OutputMessagePayload `json:"message"`
Call *proto.FunctionCallPayload `json:"call"`
CallID string `json:"call_id"`
DeliveryID string `json:"delivery_id"`
ID string `json:"id"`
Stage string `json:"stage"`
Observation *proto.ToolObservation `json:"observation"`
}

func (s *session) run(ctx context.Context, runID string, start startRequest, out chan<- proto.Envelope, prepared *prepared) {
Expand Down Expand Up @@ -141,7 +142,7 @@ func (s *session) run(ctx context.Context, runID string, start startRequest, out
if prepared != nil {
binding, err := prepared.awaitStart(scanner, failure)
if binding == nil {
prepared.failure = s.drain(scanner, stderrDone, err)
prepared.failure, _ = s.drain(scanner, stderrDone, err)
close(s.settled)
return
}
Expand All @@ -159,8 +160,11 @@ func (s *session) run(ctx context.Context, runID string, start startRequest, out
}
var content strings.Builder
var result *bridgeEvent
var classifiedFailure error
var engineCode string
var usage proto.Usage
var usageSession string
var failedResultID string
usageIDs := map[string]bool{}
var sequence uint64
terminal := false
Expand Down Expand Up @@ -236,6 +240,14 @@ func (s *session) run(ctx context.Context, runID string, start startRequest, out
usage = nextUsage
usageIDs[event.ResultID] = true
usageSession = event.SessionID
failedResultID = ""
var nativeResult struct {
Subtype string `json:"subtype"`
IsError bool `json:"is_error"`
}
if json.Unmarshal(event.Usage, &nativeResult) == nil && (nativeResult.Subtype == "error_during_execution" || nativeResult.Subtype == "success" && nativeResult.IsError) {
failedResultID = event.ResultID
}
emit(proto.TypeUsage, proto.UsagePayload{Usage: usage})
case "result":
if !s.matchesInputSession(event.SessionID) || event.SessionID == "" || start.Resume != "" && event.SessionID != start.Resume || usageSession != "" && event.SessionID != usageSession || !s.functionsComplete() || !s.steeringComplete() || !mcp.complete() || !commands.complete() {
Expand All @@ -247,6 +259,13 @@ func (s *session) run(ctx context.Context, runID string, start startRequest, out
terminal = true
case "error":
failure = bridgeFailure(event.Code)
if event.Code == "execution_failed" && event.SessionID != "" && s.matchesInputSession(event.SessionID) &&
event.SessionID == usageSession && event.ResultID != "" && event.ResultID == failedResultID {
var code string
_ = json.Unmarshal(event.EngineErrorCode, &code)
engineCode, _ = proto.NormalizeEngineFailure(code, nil)
classifiedFailure = failure
}
terminal = true
default:
failure = fmt.Errorf("claudesdk: unknown SDK bridge event")
Expand All @@ -256,7 +275,8 @@ func (s *session) run(ctx context.Context, runID string, start startRequest, out
break
}
}
failure = s.drain(scanner, stderrDone, failure)
var bridgeClean bool
failure, bridgeClean = s.drain(scanner, stderrDone, failure)
if result == nil && failure == nil {
failure = fmt.Errorf("claudesdk: SDK result is missing")
}
Expand All @@ -277,7 +297,10 @@ func (s *session) run(ctx context.Context, runID string, start startRequest, out
// Router completion cleanup calls Cancel while consuming Done. Settle first.
close(s.settled)
if failure != nil {
emit(proto.TypeError, proto.ErrorPayload{Error: failure.Error()})
if failure != classifiedFailure || !bridgeClean {
engineCode = ""
}
emit(proto.TypeError, proto.ErrorPayload{Error: failure.Error(), Code: engineCode})
}
emit(proto.TypeDone, s.outcome)
}
Expand All @@ -294,7 +317,7 @@ func launch(ctx context.Context, config Config, start startRequest, env []string
return &session{process: process, functions: functionState{calls: map[string]*pendingFunction{}}, settled: make(chan struct{})}, nil
}

func (s *session) drain(scanner *bridgeOutput, stderrDone <-chan struct{}, failure error) error {
func (s *session) drain(scanner *bridgeOutput, stderrDone <-chan struct{}, failure error) (error, bool) {
for scanner.Scan() {
}
if scanner.Err() != nil {
Expand All @@ -304,10 +327,11 @@ func (s *session) drain(scanner *bridgeOutput, stderrDone <-chan struct{}, failu
<-stderrDone
s.stopWorkspaceReads()
s.stopWorkspaceDirectories()
if err := s.process.Wait(); err != nil && failure == nil {
waitErr := s.process.Wait()
if waitErr != nil && failure == nil {
failure = fmt.Errorf("claudesdk: SDK process failed")
}
return failure
return failure, scanner.Err() == nil && waitErr == nil
}

func bridgeFailure(code string) error {
Expand Down
4 changes: 4 additions & 0 deletions apps/parsar-daemon/internal/agent/claudesdk/session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ func runSDKHelper() {
runSteeringHelper(request, mode, scanner, encode)
return
}
if strings.HasPrefix(mode, "classified-") {
runClassifiedFailureHelper(request, mode, encode)
return
}
if strings.HasPrefix(mode, "usage-") {
runUsageHelper(request, mode, encode)
return
Expand Down
Loading
Loading