# Go SDK

Record agent actions, push facts from systems of record, read verdicts and draft contracts. Standard library
only; Go 1.22+.

```bash
go get github.com/kobikwelu/stateprf-svcs/sdk/go
export COMMITLAYER_API_KEY=spk_...          # the agent runtime's key: a *recorder* key (records actions, nothing else)
export COMMITLAYER_EVIDENCE_KEY=spk_...     # the pipeline's key: an *evidence* key bound to the sources it pushes for
# COMMITLAYER_API_URL defaults to https://api.commitlayer.ai. One credential per side of the line: the key that
# records an action can never also supply the facts that verify it.
```

Every method takes a `context.Context` and returns the API payload as `map[string]any`. The response shapes are
documented in `docs/API.md` and still evolve, so the SDK keeps them untyped; decode into your own struct when you
want static fields.

## Record an action where it happens

```go
import "github.com/kobikwelu/stateprf-svcs/sdk/go"

cl, err := commitlayer.New(commitlayer.WithAgentID("support-bot"))
if err != nil {
	log.Fatal(err)
}

func refund(ctx context.Context, orderID string, amount float64) (map[string]any, *commitlayer.Recorded, error) {
	return commitlayer.Do(ctx, cl, commitlayer.Action{
		ContractID: "refund_workflow",
		SubjectID:  orderID,
		ActionOptions: commitlayer.ActionOptions{
			Params:      map[string]any{"amount": amount},
			SubjectKind: "order",
			ActionID:    "refund:" + orderID, // retries update one claim instead of recording two
		},
	}, func(ctx context.Context) (map[string]any, error) {
		return payments.Refund(ctx, orderID, amount) // the agent's own work, unchanged
	})
}

result, rec, err := refund(ctx, "order_10231", 180.0)
fmt.Println(rec.Response["verification"].(map[string]any)["summary"].(map[string]any)["headline"])
```

The action is recorded **before** the function runs, so its request time precedes every effect. The function's
return value is never changed and never consulted: verification reads the systems of record. If the API cannot
be reached the function still runs, a warning is logged through `log/slog`, and `rec.Err` carries the recording
error (fail-open, the default). Set `FailOpen` to a pointer to `false` to return the recording error without
running the function.

An action that corrects an earlier one names it in `Compensates`:

```go
commitlayer.ActionOptions{Compensates: "refund:order_10231"}
```

`RecordAction` is the same call without a function around it:

```go
resp, err := cl.RecordAction(ctx, "refund_workflow", "order_10231", commitlayer.ActionOptions{
	Params: map[string]any{"amount": 180.0},
})
```

## Push facts, with a watermark

Use an **evidence** key here, bound to the sources it pushes for (Settings → API keys). The key your agent runtime records actions with is a **recorder** key and cannot push facts: the credential that records an action never also supplies the facts that verify it.

```go
evidence, err := commitlayer.New(commitlayer.WithAPIKey(os.Getenv("COMMITLAYER_EVIDENCE_KEY"))) // the pipeline's own client
if err != nil {
	log.Fatal(err)
}
_, err = evidence.PushFacts(ctx, []map[string]any{
	{
		"source":      "crm",
		"subject_id":  "order_10231",
		"fact_type":   "note_added",
		"occurred_at": "2026-10-13T10:42:09Z",
	},
}, commitlayer.FactsOptions{
	Watermarks: map[string]string{"crm": "2026-10-13T10:45:00Z"}, // everything up to here has been sent for crm
})
```

A verdict that is not yet a PASS settles only when its window has closed **and** every source it reads has a
watermark past the end of the window. Declare one when your feed lags; a source that never declares one is taken
as current.

## Draft a contract from what was recorded

```go
d, err := cl.DraftContract(ctx, "refund_order", commitlayer.DraftOptions{LookbackDays: 30})
fmt.Println(d["yaml"]) // expected effects, window, preconditions, limits, from the sample
fmt.Println(d["stats"].(map[string]any)["effects"])
yaml := d["yaml"].(string)
v, err := cl.ValidateContract(ctx, yaml) // errors, and weakening warnings against the version it would replace
_, err = cl.SaveContract(ctx, yaml)      // the response carries the same warnings
```

Every value in a draft is what the sample showed, not what the policy says. Review it before saving.

## Read a verdict

```go
v, err := cl.GetAction(ctx, "refund:order_10231")
v["status"]                                        // pending | settled
v["settlement"].(map[string]any)["waiting_on"]     // sources still behind the window, if any
v["reversal"]                                      // the later record that undid the action, if any
v["sources"]                                       // trust level and watermark per source the contract read
```

## Errors

API rejections come back as `*commitlayer.Error` with `Status`, `Code`, `Message` and `Details`, taken from the
API's `{"error": {...}}` body:

```go
var apiErr *commitlayer.Error
if errors.As(err, &apiErr) && apiErr.Code == "entitlement_limit" {
	// apiErr.Details names the plan to upgrade to
}
```

## Testing without the network

`WithTransport` replaces the HTTP layer with a `func(ctx, method, path, body) (status, payload, err)`, which is
how this package's own tests run offline. With a transport present no API key is needed.
