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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,7 @@ The CLI stores local configuration in `~/.agentbox/config.json`; environment var
Contributor setup, spec synchronization, verification, KVM testing, versioning,
and publication are documented in [RELEASING.md](RELEASING.md).

The [Go command streaming design](docs/go-command-streaming.md) documents the
opt-in output policy for long-running processes.

AgentBox SDK is derived from upstream work described in [UPSTREAM.md](UPSTREAM.md). Licensing notices are in [LICENSE](LICENSE) and [NOTICE](NOTICE).
128 changes: 128 additions & 0 deletions docs/go-command-streaming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Go command streaming without output retention

## Contract and owner

The handwritten Go command transport in
[`commands.go`](../packages/go-sdk/commands.go) supports explicit streaming on
start and reattachment. [`pty.go`](../packages/go-sdk/pty.go) shares the delivery
implementation. No backend, wire or generated binding changes are required.
The minimum Go version remains 1.24.0.

A non-nil `CommandOptions.Streaming`, `CommandConnectOptions.Streaming` or
`PTYOptions.Streaming` selects streaming. Use `Commands.ConnectWithOptions` or
`PTY.ConnectWithOptions` for attachment by PID or tag. Existing `Connect` calls
and nil options retain collecting behavior.

| Concern | Collecting mode | Streaming mode |
| --- | --- | --- |
| Result | Full stdout/stderr and exit metadata | PID, status, exit code and errors; nil stdout/stderr |
| Callbacks | Existing callback and channel delivery | Synchronous callback only for that output |
| Channels | Unbounded queues with a preserved natural-completion tail | Explicitly enabled, unbuffered, at most 32 KiB per delivered slice |
| Unselected output | Captured and queued | Discarded; its public channel is closed |
| Callback plus channel | Existing fan-out | Invalid for the same output |
| `Wait` without reads | Supported | Supported only when no channels are enabled; callbacks must return |
| Local detach | `Close` or attachment context cancellation | Same; no signal or stdin EOF |

`CommandStreamingOptions` has `StdoutChannel`, `StderrChannel` and `PTYChannel`
flags. Existing start callbacks remain in `CommandOptions`/`PTYOptions`;
`CommandConnectOptions` supplies `OnStdout`, `OnStderr` and `OnPTY`.
`Run` continues to drain channels itself and collects output by default.

## Memory and delivery bounds

Streaming creates no queue goroutines or output history. An enabled channel uses
synchronous delivery, splitting events into independent slices of at most 32 KiB.
The SDK retains at most one pending delivery slice plus the current decoded
transport event. A streaming Connect client imposes a 4 MiB message-size limit;
this also bounds messages selected for discard. Decode buffers and HTTP transport
buffers add overhead. This is a bound independent of total emitted bytes, not a
promise of a 4 MiB total heap or RSS. Memory retained by the consumer is outside
the SDK bound.

A reader that stops blocks subsequent event processing, including exit events and
other outputs. Backpressure can propagate to the remote process. Read every
enabled channel concurrently, or select callbacks/discard for unused outputs.
There is no silent overflow, truncation, or goroutine per chunk. An oversized
transport message fails and closes the local attachment.

Callbacks execute serially on the receiver. Their byte slices are read-only and
valid only until the callback returns; copy bytes needed later. Channel receivers
own their independent slices. Channel chunk boundaries may differ from transport
boundaries, but byte order and content are preserved. Slow callbacks also apply
backpressure. Callbacks must not call `Wait` on their own handle.

## Completion, cancellation and errors

Natural streaming completion hands every enabled-channel byte to a reader before
closing the channels and `Done`; application processing may finish later.
Collecting-mode completion preserves its queued channel tail, even after `Wait`.

`CommandHandle.Close` cancels the local attachment and releases queued output.
Canceling the context supplied to start/connect does the same while the receiver
is active. Both close the HTTP response without relying on GC and without killing
the process or closing stdin. `Close` is idempotent and does not wait for arbitrary
callback code. `Done`/`Wait` complete when that callback returns and the receiver
exits. Canceling only `Wait`'s context does not detach.

Local cancellation reports `context.Canceled` or `context.DeadlineExceeded`.
A confirmed process end retains its result, including `CommandExitError` for a
nonzero exit. In streaming mode that error contains no stdout/stderr or server
error message. Transport failures preserve Connect error classification but use
generic diagnostic text, avoiding output-bearing server error details.

Reattachment adds no stdout replay, retry, restart or exactly-once promise.
Consumers must select the streaming policy on both launch and reconnect.

## Verification

Hermetic regressions in
[`command_streaming_test.go`](../packages/go-sdk/command_streaming_test.go) cover:

- Multiple MiB of ordered stdout/stderr through callbacks and channels, including
inspection of a still-live handle at a synchronized callback barrier.
- No capture or hidden channel queue; unused stderr discard and closed channels.
- Channel blocks larger than 32 KiB, byte ownership and ordered splitting.
- Unread output preventing completion, context cancellation, repeated detach,
response-body closure, and no signal/EOF side effect.
- Start, attach by PID/tag, nonzero exit, transport failure and oversized events.
- Cancellation while a callback is blocked, collecting tail release on `Close`,
PTY callback/channel delivery and PTY detach.

Existing collecting-mode no-drain, queued-tail, command/PTY and response-closure
regressions remain required. The supported Go matrix is 1.24–1.27; run
`make go-check` and generation/artifact gates described in
[`RELEASING.md`](../RELEASING.md) using the pinned containers.

Validation passed: `make go-check` (91.1% handwritten statement coverage), builds
and hermetic tests on Go 1.24.13, 1.25.14, 1.26.8 and 1.27.1, race checks on
1.24.13 and 1.27.1, format/lint/type checks, workspace tests, release-artifact
builds and clean installation checks. `make generate` and reference-contract
checks passed; a second generation produced identical tracked files. This does
not replace the full multi-language KVM release suite required before publication.

`BenchmarkCommandOutputRetention` compares callback consumers at fixed 1 KiB
chunks, retaining the handle until observation and leaving channels unread.
A Go 1.24.13 linux/arm64 sample (`-benchtime=1x -benchmem`) measured:

| Emitted stdout | Collecting retained output storage | Streaming retained output storage | Streaming allocated bytes |
| --- | --- | --- | --- |
| 1 MiB | 2,161,664 bytes | 0 bytes | 1,904 bytes |
| 16 MiB | 37,428,224 bytes | 0 bytes | 1,904 bytes |

Retained storage counts capture capacity and queued slices, excluding queue
metadata and any collecting slice already in flight. The benchmark isolates SDK
delivery from transport decoding. Allocation counts are supporting evidence;
structural live-retention assertions are the deterministic gate.

[`TestCommandStreamingAttachDetachKVM`](../packages/go-sdk/integration/command_streaming_test.go)
starts `cat`, exchanges bytes, detaches/reconnects three times using PID and tag,
exchanges further bytes, then explicitly closes stdin and checks exit metadata.
It creates and deletes only its own sandbox. This runtime smoke has passed.

## Delivery

The implementation must follow the coordinated SDK release procedure in
[`RELEASING.md`](../RELEASING.md). No ad-hoc fork or replacement module is needed.
The coordinated release version is 0.1.8. Consumers must upgrade to
`github.com/abox-dev/sdk/packages/go-sdk@v0.1.8` and explicitly enable streaming
on both launch and reconnect.
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@abox-dev/cli",
"version": "0.1.7",
"version": "0.1.8",
"description": "CLI for AgentBox sandboxes and templates",
"homepage": "https://docs.agentbox.ru/en/cli/",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/code-interpreter-js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@abox-dev/code-interpreter",
"version": "0.1.7",
"version": "0.1.8",
"packageManager": "pnpm@10.34.5",
"description": "AgentBox Code Interpreter - Stateful code execution",
"homepage": "https://docs.agentbox.ru/en/sdk/code-interpreter/",
Expand Down
2 changes: 1 addition & 1 deletion packages/code-interpreter-python/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@abox-dev/code-interpreter-python",
"private": true,
"version": "0.1.7",
"version": "0.1.8",
"scripts": {
"test": "uv run pytest -n 2 --verbose -x tests/test_sandbox_url.py",
"test:integration": "uv run pytest -n 2 --verbose -x",
Expand Down
2 changes: 1 addition & 1 deletion packages/code-interpreter-python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "abox-code-interpreter"
version = "0.1.7"
version = "0.1.8"
description = "AgentBox Code Interpreter - Stateful code execution"
authors = [{ name = "RetailDriver LLC" }]
license = "MIT"
Expand Down
4 changes: 2 additions & 2 deletions packages/code-interpreter-python/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/go-sdk/GO_PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ concurrency, and race coverage.
| Network rules, IAM payloads, metrics, structured logs | sandbox network/IAM/metrics/log tests | `sandbox_test.go` | `integration/sdk_test.go` |
| Forks, snapshots, signed upload/download URLs | sandbox fork/snapshot/signature tests | `sandbox_test.go` | core KVM lifecycle |
| Foreground/background commands, attach/list, stdin/EOF, signals, output streams, exit errors | command and command-handle tests | `envd_test.go` | `integration/sdk_test.go` |
| PTY create/attach/input/resize/kill | PTY tests | `envd_test.go` | KVM command transport |
| Opt-in bounded command delivery, callback/discard/channel policy, detach without EOF | Go-specific explicit memory policy; default behavior remains aligned | `command_streaming_test.go` | `integration/command_streaming_test.go` |
| PTY create/attach/input/resize/kill | PTY tests | `envd_test.go`, `command_streaming_test.go` | KVM command transport |
| Text/binary/stream reads and writes, batch writes, list/stat/metadata/exists/mkdir/move/remove/watch | filesystem and watch-handle tests | `envd_test.go` | `integration/sdk_test.go` |
| Base images/templates, private registries, Dockerfile parsing, copy, packages, env/user/workdir/start/ready/cache | template builder/parser tests | `template_test.go` | `integration/sdk_test.go` |
| Build request/upload/start/poll/log/status, visibility, tags, list/info/delete | template API/build tests | `template_test.go` | `integration/sdk_test.go` |
Expand Down
87 changes: 80 additions & 7 deletions packages/go-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,92 @@ func main() {
Code Interpreter is available from
`github.com/abox-dev/sdk/packages/go-sdk/codeinterpreter`.

API reference for this release: [core SDK on pkg.go.dev](https://pkg.go.dev/github.com/abox-dev/sdk/packages/go-sdk@v0.1.7) and
[Code Interpreter on pkg.go.dev](https://pkg.go.dev/github.com/abox-dev/sdk/packages/go-sdk/codeinterpreter@v0.1.7).
API reference for this release: [core SDK on pkg.go.dev](https://pkg.go.dev/github.com/abox-dev/sdk/packages/go-sdk@v0.1.8) and
[Code Interpreter on pkg.go.dev](https://pkg.go.dev/github.com/abox-dev/sdk/packages/go-sdk/codeinterpreter@v0.1.8).

Documentation: [core SDK](https://docs.agentbox.ru/en/sdk/),
[sandboxes](https://docs.agentbox.ru/en/sdk/sandboxes/),
[templates](https://docs.agentbox.ru/en/sdk/templates/), and
[Code Interpreter](https://docs.agentbox.ru/en/sdk/code-interpreter/).

`Sandbox.Kill` returns `false, nil` when the sandbox no longer exists. Command
handles can be waited on without draining their live output channels; `Wait`
always returns complete stdout and stderr collected by the SDK. Output channels
may finish draining and close after `Wait` returns. PTY callers can consume
`CommandHandle.PTY` or use `PTYOptions.OnPTY`.
`Sandbox.Kill` returns `false, nil` when the sandbox no longer exists.

## Command output

By default, command handles collect complete stdout/stderr for `Wait`, which can
be called without draining their output channels. Channel tails may finish
draining after `Wait` returns. Callbacks also receive output in this mode.

For long-lived processes, opt into streaming on both start and reconnect:

```go
policy := &agentbox.CommandStreamingOptions{}
handle, err := sandbox.Commands.Start(ctx, "cat", &agentbox.CommandOptions{
Stdin: true,
Streaming: policy,
OnStdout: func(chunk []byte) { consume(chunk) },
// No stderr callback or channel: discard stderr without retaining it.
})
if err != nil {
return err
}
defer handle.Close()
pid, err := handle.PID(ctx)
if err != nil {
return err
}
// Exchange messages, then detach locally. The remote process and stdin stay open.
_ = handle.Close()
_, _ = handle.Wait(ctx) // reports context.Canceled for an active attachment
attached, err := sandbox.Commands.ConnectWithOptions(ctx, pid, "", &agentbox.CommandConnectOptions{
Streaming: policy,
OnStdout: func(chunk []byte) { consume(chunk) },
})
if err != nil {
return err
}
defer attached.Close()
```

A non-nil `Streaming` selects exactly one delivery path per output: its callback,
its explicitly enabled channel (`StdoutChannel`, `StderrChannel`, `PTYChannel`),
or discard. Selecting both a callback and a channel for the same output returns
`InvalidArgumentError`. Unselected channels are already closed. Callbacks execute
serially on the receiver; their read-only slices are valid until the callback
returns. Copy data that must be retained and return promptly. Cancellation closes
the transport but cannot interrupt callback code; `Done`/`Wait` finish after it
returns. Do not call `Wait` from a callback.

Streaming channels are unbuffered. Consume every enabled channel concurrently
with `Wait`; a stalled reader applies backpressure and can eventually stall the
remote process. Byte order is preserved, but transport chunks may be split into
slices of at most **32 KiB**. The receiver owns those slices. There is no output
queue or captured result, only one pending channel slice plus one in-flight
transport event. Each encoded/decompressed Connect message is limited to **4 MiB**;
an oversized message fails the attachment with a resource-exhausted error. HTTP
buffers, decoding allocations and caller-retained slices are additional memory.
The limit is independent of total process output, not an exact heap/RSS cap.

`Wait` returns PID, status, exit code and errors, with nil stdout/stderr in streaming
mode. Successful completion means all enabled channel bytes were handed to their
readers; their processing may still be running. Nonzero exits return
`CommandExitError` with an empty output result and empty server error message.
Transport failures retain their error code but omit server-provided diagnostic
text/details that might contain output. Neither path silently truncates output
and reports success.

Cancel the context passed to `Start`/`ConnectWithOptions`, or call `Close`, to
release the local attachment without killing the process or sending EOF.
Cancellation returns `context.Canceled`/`context.DeadlineExceeded`, unless a process
end event was already confirmed. `Close` also discards an unread collecting-mode
channel tail; natural completion preserves that tail. Canceling only the context
passed to `Wait` stops waiting without detaching. Reconnecting by PID or tag adds
no stdout replay, delivery retry, process restart or exactly-once guarantee.

PTY callers can use `PTYOptions.Streaming` with `OnPTY` or `PTYChannel`, and
`PTY.ConnectWithOptions` to reattach with the same policy. Default PTY behavior
remains unchanged. See the [streaming contract](../../docs/go-command-streaming.md)
for bounds and validation, and `examples_test.go` for a complete example.

Streaming uploads have no SDK deadline by default. Set
`WriteFileOptions.RequestTimeout` to limit a complete upload. A client supplied
Expand Down
Loading
Loading