Mango

Go SDK

A typed, standard-library-only client for Mango.

Edit on GitHub

The standalone module github.com/yanpgwang/mango/sdk/go requires Go 1.24+ and has no server or third-party runtime dependencies. See local installation.

Configure the client

The quickstart imports context, os, time, and the SDK as mango. This excerpt runs inside a function returning error:

baseURL := os.Getenv("MANGO_BASE_URL")
if baseURL == "" {
	baseURL = "http://localhost:8080"
}
client, err := mango.New(mango.Config{
	BaseURL: baseURL,
	APIKey:  os.Getenv("MANGO_API_KEY"),
})
if err != nil {
	return err
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()

Every operation accepts a context. Use a deadline or cancellation for long-lived streams. BaseURL supports a reverse-proxy prefix; do not append /v1.

Methods and inputs

Operations are grouped by resource: client.Sessions.New, client.Sessions.Events.Send, and client.MemoryStores.Memories.New. Path IDs are positional in HTTP hierarchy order, followed by generated request or query structs. ModelID, ModelSettings, AgentID, AgentVersion, Coordinator, RosterAgent, RosterAgentVersion, Self, Advisor, Text, and UserMessage construct common wire variants without pointer boilerplate.

  • The zero value of Optional[T] omits a field.
  • mango.Some(value) sends the value, including zero, false, or an empty list.
  • mango.SomePtr("text") sets a nullable string.
  • mango.Null[T]() sends explicit JSON null; the server checks whether it is allowed.
  • Set exactly one pointer in a wire union. Unknown response variants remain in Raw.

Streaming and pagination

Sessions.Events.Stream establishes the subscription before returning. Call it before sending input, iterate with Next(), decode Event(), check Err(), and close the stream. The stream is live-only and does not automatically reconnect.

Sessions.Events.ListAutoPaging follows pages; Sessions.Events.List fetches one page. Use the open-stream-then-list recovery procedure to recover durable events after disconnects.

Self-hosted Work polling

NewWorkPoller claims and acknowledges Work for one self_hosted Environment. It is a control-plane iterator, not a sandbox or tool runner. Advancing or closing it never stops Work. EnvironmentWorker composes it with the Session runner, lease heartbeat, and final Stop.

poller := mango.NewWorkPoller(ctx, client, mango.WorkPollerOptions{
    EnvironmentID: environmentID,
    Drain:         true,
})
defer poller.Close()

for poller.Next() {
    work := poller.Current()
    // Launch a per-Session runner for work.Data.ID.
}
if err := poller.Err(); err != nil {
    return err
}

Drain mode omits block_ms unless explicitly configured and returns normally when the queue is empty. A long-running poller defaults to the API's 999 ms long-poll and exits normally when its context is cancelled. If Ack has an ambiguous failure, the poller exits without calling Stop; Mango's Work TTL then makes the activation reclaimable.

Composed Environment worker

Use NewEnvironmentWorker for the complete provider-neutral Work lifecycle:

worker := mango.NewEnvironmentWorker(client, mango.EnvironmentWorkerOptions{
    EnvironmentID: environmentID,
    Workdir:       "/workspace",
    ToolsFunc: func(env mango.EnvironmentWorkerToolContext) ([]mango.SessionTool, error) {
        return agenttoolset.New(agenttoolset.Context{
            Workdir:       env.Workdir,
            AllowedRoots:  env.AllowedRoots,
            ReadOnlyRoots: env.ReadOnlyRoots,
        })
    },
})
if err := worker.Run(ctx); err != nil {
    return err
}

The trusted supervisor client is used only for Poll and Ack. Before executing a tool, the worker decodes the acknowledged Work secret, switches to its scoped Session token, and obtains a successful conditional heartbeat. That token then authorizes heartbeats, Fail, Session events, and final Stop. Missing or malformed secrets fail closed; there is no Workspace-key fallback.

For a launcher that Polls and Acks outside a Session sandbox, call HandleItem inside the sandbox with EnvironmentWorkerHandleItemOptions. Non-secret identity fields may be passed explicitly or through MANGO_WORK_ID, MANGO_ENVIRONMENT_ID, and MANGO_SESSION_ID. Pass WorkSecret explicitly from a protected launcher transport when the process runs untrusted code. The item process needs the Mango base URL but must not receive the Workspace key. An allowlisted child environment prevents ordinary inheritance, but on Linux the launcher must also keep secrets out of parent environment/command metadata and prevent same-identity children from inspecting the trusted runner. The first-party Docker launcher uses one-shot stdin and a non-dumpable item process.

After the first successful heartbeat, the worker fetches the frozen Session once. It prepares immutable custom Skill pins beneath Workdir, downloads attached Memory Stores at their frozen absolute mount paths, and only then calls ToolsFunc with the exact allowed and read-only roots. It verifies Skill and Memory integrity, confines filesystem access, and applies whole-Session limits. Permanent invalid input is committed through Work Fail and terminates the Session; temporary retrieval failure remains reclaimable. Self-hosted Skill paths exposed to the model are relative to Workdir; Memory paths remain the absolute /mnt/memory/... paths recorded on the Session.

The worker checks Memory synchronization after each dispatched tool, at a 15-second default cadence with a five-second minimum configurable interval. Remote Store content wins a simultaneous edit, writable local changes use the existing Memory API with SHA-256 preconditions, and read-only Stores never push. Clean shutdown performs a final sync; cancellation and error exits still receive a separately bounded push-only flush. Set MemorySyncInterval negative only when the operator explicitly wants to disable Memory download and synchronization. The worker does not create compute or prepare File/Git resources; those remain launcher responsibilities. On lease loss it cancels the runner, prevents later result submission, returns ErrEnvironmentWorkLeaseLost, and does not Stop a possibly newer owner's Work.

Cancel the context passed to EnvironmentWorker.Run or HandleItem on shutdown. Tool execution is cancelled first; heartbeat renewal continues through Memory teardown, and Work Stop follows the flush. A full Memory reconciliation and rescue upload each have a 30-second budget. Allow additional time for tool/result cleanup and final Stop; the Docker reference grants 120 seconds. Hard kills and upload failures are not durable local Memory commits.

For launchers that need Mango's core shell and file executors, the independent tools/agenttoolset package returns bash, read, write, edit, glob, and grep as SessionTool implementations:

tools, err := agenttoolset.New(agenttoolset.Context{Workdir: "/workspace"})
if err != nil {
    return err
}
defer agenttoolset.CloseAll(tools)

This package is not a sandbox. The caller must establish the isolation boundary first. File operations remain beneath Workdir and any explicitly configured AllowedRoots; write and edit reject ReadOnlyRoots. Reads and outputs are bounded, and Mango credentials are removed from the Bash environment. Bash uses one persistent PTY session per toolset, so cwd, exported variables, and background jobs survive calls. Its input accepts command, restart, and timeout_ms; the runner-wide tool deadline remains the hard upper bound. A timeout or cancellation replaces the shell before the next call. Bash is not path-confined inside the host process, so the caller's sandbox is the security boundary. A direct caller owns CloseAll because SessionToolRunner borrows tools and never closes them. EnvironmentWorker is the exception: it closes per-Session tools returned by ToolsFunc, while a static Tools slice remains caller-owned.

Self-hosted Session tools

NewSessionToolRunner handles one Session's provider-neutral tool loop. It opens SSE before listing durable history, reconciles again on reconnect, and maps owned agent.tool_use and agent.custom_tool_use events to their matching result events. MCP calls stay server-side, and unregistered tool names remain pending for another owner.

runner := mango.NewSessionToolRunner(ctx, sessionClient, sessionID,
    mango.SessionToolRunnerOptions{Tools: tools})
defer runner.Close()

for runner.Next() {
    dispatch := runner.Current()
    // Observe dispatch.Owned, dispatch.IsError, and dispatch.Posted.
}
if err := runner.Err(); err != nil &&
    !errors.Is(err, mango.ErrSessionTerminated) &&
    !errors.Is(err, mango.ErrIdleTimeout) {
    return err
}

sessionClient should use the acknowledged Work item's sessions_token. The runner fails closed on approval-gated or unknown permission values, and returns ErrSessionLeaseLost when that credential loses authority. It does not heartbeat or Stop Work; use EnvironmentWorker for that composition. Tools must honor cancellation and use the stable SessionToolCall.ToolUseID to make external side effects idempotent.

Errors and retry safety

Use errors.As with *mango.APIError to access StatusCode, Type, and RequestID. Avoid logging full error bodies, which may contain application data. Generated one-shot methods do not automatically retry writes after an ambiguous failure. SessionToolRunner is the narrow exception: it checks durable result history before retrying its own transient result submission.

On this page