memcode docs
Halo

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/go

It 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.

KindMeaning
answera direct answer (Message)
resultsomething ran; Result holds what it returned, Authorization says how it was allowed
question / choiceHalo needs a detail, or the request had more than one reading
proposedplan mode: what Halo would do, with nothing run
denied / declinednot allowed by the session's settings, or refused by your program
unsupportedunderstood, but not something Halo does yet
cancelledthe 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, bypassPermissions included.
  • 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_available instead 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 too

If 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:

CodeMeaning
not_signed_inrun halo login
model_not_availablethe requested Halo model is not available
version_mismatchthe installed Halo is too old for this SDK; run halo upgrade
turn_in_progressone request at a time per session
rate_limit, capacity_unavailabletry again later (Retryable, RetryAfterMs)

On this page