Agent SDK
Drive Halo from your own programs and agents, with the same safety checks as the shell.
The Agent SDK lets a program use Halo the way a person does at the prompt. It sends a request in words and gets back a typed outcome.
The SDK drives the Halo you already have installed. It needs no separate runtime, daemon or service, and no extra sign-in: it uses your Halo sign-in.
Everything that decides what is allowed still happens inside Halo, the same way as in the shell:
- previews;
- the permission checks;
- sandboxing;
- the approvals.
Your program only answers the questions Halo asks.
Install
The Go binding is the first SDK.
go get github.com/memcode-ai/halo-client/sdk/goIt needs the halo command on your PATH (Installation), signed in with
halo login.
The SDK needs a Halo client with machine mode, which ships in the next client release. With v0.1.5 and
earlier, halo.Start fails with halo_exited; run halo upgrade once the new release is out.
Run a request
s, err := halo.Start(ctx, halo.Options{Cwd: "/path/to/project"})
if err != nil {
log.Fatal(err)
}
defer s.Close()
out, err := s.Run(ctx, "what is using port 5432?")
if err != nil {
log.Fatal(err)
}
fmt.Println(out.Kind, out.Message)A session is one conversation: follow-ups like "kill it" or "only the PDFs" refer to what came before, just as in
the shell. s.Reset(ctx) starts over.
Outcomes
Every request ends with exactly one outcome or a typed error.
Kind | Meaning |
|---|---|
answer | a direct answer (Message) |
result | something ran; Result holds what it returned, Authorization says how it was allowed |
question / choice | Halo needs a detail, or the request had more than one reading |
proposed | plan mode: what Halo would do, with nothing run |
denied / declined | not allowed by the session's settings, or refused by your program |
unsupported | understood, but not something Halo does yet |
cancelled | the request was cancelled |
Permissions
A session starts with no permissions inherited from your interactive Halo. You choose them in Options:
opts := halo.Options{
Cwd: "/path/to/project",
PermissionMode: halo.ModeAcceptEdits, // default | dontAsk | acceptEdits | plan | bypassPermissions
AllowedOperations: []string{"git.*"}, // pre-approved
DisallowedOperations: []string{"files.delete"}, // always denied, in every mode
}
s, err := halo.Start(ctx, opts)
if err != nil {
log.Fatal(err)
}
defer s.Close()- Default. Anything that needs approval is denied unless your program answers approvals.
- Disallowed operations always win. This holds in every mode,
bypassPermissionsincluded. - Some protections are never bypassed. Running without a sandbox and catastrophic operations each need their own explicit setting.
Approvals, choices and offers
To have Halo ask instead of deny, give the session an Interactor:
type reviewer struct{}
func (reviewer) Approve(ctx context.Context, r halo.ApprovalRequest) halo.Approval {
fmt.Println("Halo wants to:", r.Operations, r.Reason)
if r.Effect == "READ" {
return halo.Approval{Decision: halo.DecisionAllow}
}
return halo.Approval{Decision: halo.DecisionDeny, Message: "only reads in this job"}
}
func (reviewer) Choose(ctx context.Context, r halo.ChoiceRequest) (int, bool) { return 0, false } // cancel
func (reviewer) Offer(ctx context.Context, r halo.OfferRequest) bool { return false }
func startReviewed(ctx context.Context) (*halo.Session, error) {
return halo.Start(ctx, halo.Options{Cwd: ".", Interactor: reviewer{}})
}- What it can decide. An approval can allow, allow for the rest of the session (
DecisionAllowSession), or deny. - What it cannot change. It cannot alter the operation it approves. To do something different, deny it and ask again in words.
Pinning the Halo model
A Halo model identifies a stable interpretation behavior. Pin a model when reproducibility matters.
- IDs are stable. A model ID always behaves the same way.
- Halo never retargets a pinned session, and a model that isn't available fails with
model_not_availableinstead of falling back to another. - Updates don't change it. Updating Halo or the SDK never changes which model you selected.
s, err := halo.Start(ctx, halo.Options{Cwd: ".", Model: "halo-orbit-preview"})
if err != nil {
log.Fatal(err) // model_not_available if that model is not offered
}
defer s.Close()
fmt.Println("pinned to", s.Model()) // every outcome names its model tooIf you leave Model empty, the session uses the default model and pins it when the session starts.
See Models.
Errors
Failures that are not outcomes are *halo.Error values with a Code:
Code | Meaning |
|---|---|
not_signed_in | run halo login |
model_not_available | the requested Halo model is not available |
version_mismatch | the installed Halo is too old for this SDK; run halo upgrade |
turn_in_progress | one request at a time per session |
rate_limit, capacity_unavailable | try again later (Retryable, RetryAfterMs) |