Skip to content
Merged
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
409 changes: 219 additions & 190 deletions CONTRIBUTING.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ native differences.
| [API reference](docs/api/README.md) | The `/v1`, `/core/v1` and `/api/v1` namespaces |
| [Nodes and sandbox backends: operator reference](services/agents-api/HOSTED-SANDBOX-MANAGER.md) | Node protocol, manual registration, placement, maintenance |
| [Protocol coverage](contracts/agents-api/README.md) and [harness selection](contracts/agents-api/harness-selection.md) | Supported operations and native differences |
| [Add a Harness](contracts/agents-api/harness-onboarding.md) | Required adapter interfaces, lifecycle, capabilities, registration and acceptance |
| [Landing page source](site/index.html) | The public site |

### Maintainers and advanced deployments
Expand Down
35 changes: 30 additions & 5 deletions apps/parsar-daemon/internal/agent/claudesdk/cancellation.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,51 @@ package claudesdk

import (
"context"
"errors"
"github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent"

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

// Cancel confirms process exit and output drain, independently of terminal delivery.
// Cancel addresses this fixed Turn. Settlement remains observable after the caller detaches.
func (s *session) Cancel(ctx context.Context) error {
if ctx == nil {
ctx = context.Background()
}
select {
case <-s.settled:
return nil
return s.settlementErr
default:
}
s.process.Cancel()
if err := ctx.Err(); err != nil {
return err
}
if s.owner == nil {
return errors.New("claudesdk: Turn owner is unavailable")
}
s.cancelOnce.Do(func() {
close(s.cancelOutput)
s.owner.mu.Lock()
current := s.owner.active == s && !s.owner.invalid
s.owner.mu.Unlock()
if current {
stopWrite := context.AfterFunc(ctx, s.invalidate)
defer stopWrite()
if err := s.owner.write(struct {
Type string `json:"type"`
TurnID string `json:"turn_id"`
}{"turn_cancel", s.runID}); err != nil {
s.invalidate()
}
}
})
select {
case <-s.settled:
return nil
return s.settlementErr
case <-ctx.Done():
select {
case <-s.settled:
return nil
return s.settlementErr
default:
return ctx.Err()
}
Expand All @@ -40,3 +63,5 @@ func (s *session) CancellationOutcome() proto.DonePayload {
return proto.DonePayload{}
}
}

var _ agent.Turn = (*session)(nil)
17 changes: 11 additions & 6 deletions apps/parsar-daemon/internal/agent/claudesdk/cancellation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,15 @@
package claudesdk

import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"os"
"os/signal"
"path/filepath"
"reflect"
"strings"
"syscall"
"testing"
"time"

Expand Down Expand Up @@ -171,12 +170,16 @@ func cancellationRequest() proto.PromptRequestPayload {
return proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("hello"), AgentSessionID: "native-session", AgentOptions: map[string]any{"model": "fake-model", "system_prompt": "instructions"}}
}

func runCancellationHelper(request startRequest, mode string, emit func(bridgeEvent)) {
func runCancellationHelper(request startRequest, mode string, scanner *bufio.Scanner, emit func(bridgeEvent)) {
if mode == "cancellation-wait" {
stopped := make(chan os.Signal, 1)
signal.Notify(stopped, syscall.SIGTERM)
emit(bridgeEvent{Type: "delta", Delta: "partial"})
<-stopped
if !scanner.Scan() {
return
}
var cancel map[string]any
if json.Unmarshal(scanner.Bytes(), &cancel) != nil || cancel["type"] != "turn_cancel" {
os.Exit(9)
}
// These valid observations were in flight when cancellation started.
emit(bridgeEvent{Type: "input_ready", SessionID: request.Resume})
emit(bridgeEvent{Type: "usage", ResultID: "native-result", SessionID: request.Resume, Usage: json.RawMessage(usageFixture)})
Expand All @@ -189,6 +192,8 @@ func runCancellationHelper(request startRequest, mode string, emit func(bridgeEv
}
_, _ = os.Stderr.WriteString(strings.Repeat("x", 2*1024*1024))
emit(bridgeEvent{Type: "delta", Delta: "drained"})
emit(bridgeEvent{Type: "input_closed", SessionID: request.Resume})
emit(bridgeEvent{Type: "error", Code: "cancelled"})
return
}
if mode != "cancellation-before-identity" {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
package claudesdk

import (
"bufio"
"context"
"encoding/json"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
Expand Down Expand Up @@ -119,8 +119,8 @@ func TestWorkspaceCommandCancellationAndBridgeFailuresCloseOnlyPendingCalls(t *t
t.Fatal(err)
}
} else if mode == "commands-cancel" || mode == "commands-drained" {
if err := s.Cancel(ctx); err != nil {
t.Fatal(err)
if err := s.Cancel(ctx); (err == nil) != (mode == "commands-drained") {
t.Fatalf("command cancellation confirmation: %v", err)
}
}
}
Expand Down Expand Up @@ -155,13 +155,7 @@ func TestWorkspaceCommandCancellationAndBridgeFailuresCloseOnlyPendingCalls(t *t
}
}

func runCommandsHelper(request startRequest, mode string, emit func(bridgeEvent)) {
var stopping chan os.Signal
if mode == "commands-cancel" || mode == "commands-drained" {
stopping = make(chan os.Signal, 1)
signal.Notify(stopping, syscall.SIGTERM)
defer signal.Stop(stopping)
}
func runCommandsHelper(request startRequest, mode string, scanner *bufio.Scanner, emit func(bridgeEvent)) {
if mode != "commands-before-ready" {
emit(bridgeEvent{Type: "input_ready", SessionID: request.Resume})
}
Expand All @@ -181,7 +175,9 @@ func runCommandsHelper(request startRequest, mode string, emit func(bridgeEvent)
}
switch mode {
case "commands-cancel", "commands-drained":
<-stopping
if !scanner.Scan() {
return
}
if mode == "commands-drained" {
interrupted := commandEvent("pending", "after", "incomplete", "sleep 30")
interrupted.Observation.Output = json.RawMessage(`"observed while draining"`)
Expand Down
Loading
Loading