carragent

package module
v0.0.0-...-aba0183 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 19, 2026 License: MIT Imports: 14 Imported by: 0

README

Carragent agent kit

Skills, MCP configuration and two small clients for looking up Carragent, a self-serve platform of AI agents for small businesses: what each agent does, what it leaves for the owner to approve, and what the plans cost.

Carragent's public API is read-only. It needs no account and no key. Nothing in this repository signs anyone up, starts a trial, runs an agent or reaches a business's data.

What is here

Path What
skills/ Four skills for coding agents, in the Agent Skills format
mcp.json Both MCP servers, ready to add to a client
plugin.json This repository as an Agent Plugin: the skills and the MCP servers together
AGENTS.md Instructions for coding agents that use Carragent or work in this repository
python/ The Python client: pip install carragent
this directory The Go client: go get carragent.com/go

In a terminal, the carragent npm package does the same reads: npx carragent search "late invoices". It has its own releases.

Skills

Skill An agent uses it when
carragent someone asks what Carragent does, which agent fits a task, or what it costs
find-carragent-agents a business owner describes a problem and you need to say which Carragent agents would help
compare-carragent-plans someone asks about plans, prices, the free trial or refunds
use-carragent-api you are calling Carragent's API from code or from an agent and must choose between REST, batch, jobs, MCP and A2A

Add them with the skills CLI:

npx skills add vicenccc/carragent-agent-kit

Or copy a folder from skills/ to wherever your agent reads skills. The site serves the same four files at https://carragent.com/.well-known/agent-skills/<name>/SKILL.md and lists them in https://carragent.com/.well-known/agent-skills/index.json.

MCP servers

mcp.json lists both servers. They speak Streamable HTTP and need no credentials.

Server Address Tools
carragent https://carragent.com/api/mcp list_agents, get_agent, list_plans, search_carragent
carragent-docs https://carragent.com/api/mcp/docs search_docs, get_doc

Python

pip install carragent
from carragent import Carragent

client = Carragent()
for hit in client.search("late invoices")["data"]:
    print(hit["kind"], hit["id"], hit["name"])

More in python/README.md.

Go

go get carragent.com/go
package main

import (
	"context"
	"fmt"
	"log"

	carragent "carragent.com/go"
)

func main() {
	client := carragent.NewClient()

	results, err := client.Search(context.Background(), "late invoices", "")
	if err != nil {
		log.Fatal(err)
	}
	for _, hit := range results.Data {
		fmt.Println(hit.Kind, hit.ID, hit.Name)
	}
}

The package documentation is on pkg.go.dev.

Both clients

  • They use only the standard library: Python 3.9 or newer, Go 1.21 or newer.
  • They read the agents, the plans and the search, run batches of up to 20 reads, and follow the REST pattern for jobs. A token is optional.
  • An error answer is an RFC 9457 problem: CarragentError in Python, *Problem in Go, with the API's stable code. A 429 carries how long to wait (Retry-After). Neither client retries by itself.
  • Requests identify themselves as carragent-python/<version> or carragent-go/<version>.

Limits and rules

  • 60 requests a minute per IP address without a token; 300 a minute per client with one.
  • Prices are US dollars, plus applicable taxes, as the API returns them. Savings are estimates.
  • Nothing here can change anything, so there is no sandbox: test against the live endpoints, and mind the limit.

Develop

cd python && python3 -m unittest -v       # Python 3.9 or newer
go vet ./... && go test ./...             # Go 1.21 or newer, from this directory

Both suites run against local fake servers: no network, no key. Set CARRAGENT_LIVE_URL (for example http://localhost:3100) to run the opt-in tests against a real server as well.

skills/*/SKILL.md, mcp.json and python/LICENSE are written from Carragent's own sources. A change made to them here is overwritten. AGENTS.md has the rest.

Contact: admin@carragent.com. MIT licensed.

Documentation

Overview

Package carragent is a client for Carragent's public, read-only API: its AI agents for small businesses, and its plans and prices (https://carragent.com/developers).

The API needs no account and no key. This package reads it. It does not sign anyone up, start a trial, change anything or reach a business's data. It uses only the standard library.

The module path is carragent.com/go and the package is named carragent:

import carragent "carragent.com/go"

Reading

Make a Client and call its methods. Every one takes a context first, and returns typed values:

client := carragent.NewClient()

results, err := client.Search(ctx, "late invoices", "")
if err != nil {
	return err
}
for _, hit := range results.Data {
	fmt.Println(hit.Kind, hit.ID, hit.Name)
}

agent, err := client.GetAgent(ctx, "collections", carragent.English)
// agent.Summary, agent.WaitsForOwner, agent.Never

plans, err := client.ListPlans(ctx, "")
// Prices are whole US cents, before taxes: plan.MonthlyCents, plan.YearlyCents.

Text comes in English, Spanish or French (Lang); the empty Lang is English. Client.IterAgents walks every agent, Client.Batch runs up to 20 reads in one request, and Client.CreateJob, Client.GetJob and Client.WaitJob are the REST pattern for slow operations, which these fast reads follow so that code written for one works unchanged.

Errors

An error answer from the API is a *Problem (an RFC 9457 problem document). Branch on its Code, which is stable:

var problem *carragent.Problem
if errors.As(err, &problem) {
	if problem.RateLimited() {
		time.Sleep(problem.RetryAfter)
	}
}

The client never retries by itself. A request that gets no answer returns the transport's error, which wraps the standard ones: errors.Is(err, context.DeadlineExceeded) is true when the context ran out.

Tokens

A token is optional. It raises the rate limit from 60 requests a minute per IP address to 300 a minute per client, and reads the same public catalog. Client.UseAnonymousIdentity registers the agent anonymously and gets one; Client.UseClientCredentials does it for an OAuth client. Both set Client.Token. A token lasts an hour.

Concurrency

A Client is safe for concurrent use as long as its fields are not changed meanwhile. UseAnonymousIdentity and UseClientCredentials change Token, so call them before sharing the client between goroutines.

Example
package main

import (
	"context"
	"fmt"
	"log"

	carragent "carragent.com/go"
)

func main() {
	ctx := context.Background()
	client := carragent.NewClient()

	// Which agent follows up late invoices?
	results, err := client.Search(ctx, "late invoices", "")
	if err != nil {
		log.Fatal(err)
	}
	for _, hit := range results.Data {
		fmt.Println(hit.Kind, hit.ID, hit.Name)
	}

	// What does it do, and what does it leave to the owner?
	agent, err := client.GetAgent(ctx, "collections", carragent.English)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(agent.Summary)
	fmt.Println("Waits for the owner:", agent.WaitsForOwner)
	fmt.Println("Never:", agent.Never)

	// What does it cost? Prices are whole US cents, before taxes.
	plans, err := client.ListPlans(ctx, "")
	if err != nil {
		log.Fatal(err)
	}
	for _, plan := range plans.Data {
		if plan.SelfServe {
			fmt.Printf("%s: $%.2f a month\n", plan.Name, float64(plan.MonthlyCents)/100)
		}
	}
	fmt.Println("Free trial:", plans.Terms.TrialDays, "days")
}

Index

Examples

Constants

View Source
const (
	// DefaultBaseURL is where the API is.
	DefaultBaseURL = "https://carragent.com"

	// Version is this library's version. The client names it in its User-Agent header.
	Version = "1.0.0"
)

Variables

This section is empty.

Functions

This section is empty.

Types

type Agent

type Agent struct {
	// ID is the agent's stable id, such as "collections".
	ID string `json:"id"`
	// Name is its display name.
	Name string `json:"name"`
	// Area is the business area: "money", "customers", "sales" or "operations".
	Area string `json:"area"`
	// Status is "live" when the agent works today and "soon" when it is being built.
	Status string `json:"status"`
	// Summary says what it does, in one line.
	Summary string `json:"summary"`
	// Runs says when it runs.
	Runs string `json:"runs"`
	// Steps is what it does, in order.
	Steps []string `json:"steps"`
	// WaitsForOwner is what waits for the owner's approval.
	WaitsForOwner string `json:"waitsForOwner"`
	// Never is what the agent never does.
	Never string `json:"never"`
	// Tools are the tools it connects to. It is nil when there is nothing to connect.
	Tools []string `json:"tools"`
}

Agent is one AI agent.

type AgentIterator

type AgentIterator struct {
	// contains filtered or unexported fields
}

AgentIterator walks every agent, page by page. Make one with Client.IterAgents.

it := client.IterAgents(nil)
for it.Next(ctx) {
	fmt.Println(it.Agent().Name)
}
if err := it.Err(); err != nil {
	// ...
}

It is not safe for concurrent use.

func (*AgentIterator) Agent

func (it *AgentIterator) Agent() Agent

Agent returns the agent Next moved to.

func (*AgentIterator) Err

func (it *AgentIterator) Err() error

Err returns the error that stopped the iteration, or nil if it reached the last agent.

func (*AgentIterator) Next

func (it *AgentIterator) Next(ctx context.Context) bool

Next moves to the next agent, asking for the next page when it must. It returns false at the end, and on an error; Err tells which.

type AgentList

type AgentList struct {
	Object string  `json:"object"`
	Data   []Agent `json:"data"`
	// NextCursor goes in ListAgentsParams.Cursor to get the next page. It is empty on the last page.
	NextCursor string `json:"nextCursor"`
	HasMore    bool   `json:"hasMore"`
}

AgentList is a page of agents.

type BatchItem

type BatchItem struct {
	// ID names the item's answer: 1 to 64 of A-Za-z0-9_.:- and unique in the batch. The item's position when empty.
	ID string `json:"id,omitempty"`
	// Method is only "GET", which is also the default.
	Method string `json:"method,omitempty"`
	// Path is a read of the API relative to /api/v1: "/agents?limit=5", "/agents/collections", "/plans" or
	// "/search?q=late%20invoices".
	Path string `json:"path"`
}

BatchItem is one read in a batch.

type BatchResponse

type BatchResponse struct {
	Object    string        `json:"object"`
	Responses []BatchResult `json:"responses"`
}

BatchResponse is the answers of a batch, in the order of the requests.

type BatchResult

type BatchResult struct {
	ID string `json:"id"`
	// Status is the HTTP status the single endpoint would have answered.
	Status int `json:"status"`
	// Body is what the single endpoint would have returned: its JSON, or a problem when Status is 400 or more.
	Body json.RawMessage `json:"body"`
}

BatchResult is the answer to one item of a batch.

func (BatchResult) Decode

func (r BatchResult) Decode(v any) error

Decode reads the item's body into v, which is a pointer. It returns the item's problem instead when Status is 400 or more, so an item that failed is an error and one that worked is data.

type Client

type Client struct {
	// BaseURL is the API's address, without a path or with the prefix of a proxy. It must be https; http is
	// accepted only for a server on this machine. Empty means DefaultBaseURL.
	BaseURL string

	// HTTPClient sends the requests. Nil means a client with a 10 second timeout that does not follow redirects:
	// a redirect would send your token wherever it points, so it is returned as an error (code "unexpected_redirect").
	// If you set your own, you choose its timeout and its redirect policy.
	HTTPClient *http.Client

	// Token is an access token to send as Authorization: Bearer. Empty sends none. UseAnonymousIdentity and
	// UseClientCredentials get one and set it. A token lasts an hour; after that the API answers 401 invalid_token,
	// even though it is public, until you get a new token or clear this one.
	Token string

	// UserAgent is the User-Agent header. Empty means "carragent-go/" and the Version. To name your application,
	// start it with your own product token: "my-app/1.2 carragent-go/1.0.0".
	UserAgent string
}

Client reads Carragent's public, read-only API: its AI agents for small businesses, and its plans and prices.

Everything it reads is public and needs no account or key. A token is optional: it only raises the rate limit (60 requests a minute per IP address without one, 300 a minute per client with one) and reads the same public catalog. Nothing here creates an account, starts a trial or touches a business's data.

The zero Client works, with the defaults NewClient sets. A Client keeps no connection of its own and never retries a request. It is safe for concurrent use as long as its fields are not changed meanwhile; UseAnonymousIdentity and UseClientCredentials change Token, so call them before sharing the client between goroutines.

func NewClient

func NewClient() *Client

NewClient returns a Client with the defaults: DefaultBaseURL, a 10 second timeout and no token.

func (*Client) Batch

func (c *Client) Batch(ctx context.Context, items []BatchItem) (*BatchResponse, error)

Batch runs up to 20 reads in one request: POST /api/v1/batch.

The answers come back in order. An item that failed carries its problem in its Body and its own Status: Batch returns an error only when the whole request is refused. BatchResult.Decode reads an item's body, or returns its problem. Every item counts as one request against the rate limit.

Each item's Path is relative to /api/v1: "/agents", "/agents/collections", "/plans" or "/search?q=late%20invoices".

Example
package main

import (
	"context"
	"fmt"
	"log"

	carragent "carragent.com/go"
)

func main() {
	ctx := context.Background()
	client := carragent.NewClient()

	answers, err := client.Batch(ctx, []carragent.BatchItem{
		{ID: "plans", Path: "/plans"},
		{ID: "one", Path: "/agents/collections"},
		{Path: "/search?q=appointment%20reminders"},
	})
	if err != nil {
		log.Fatal(err) // the whole request was refused
	}
	for _, answer := range answers.Responses {
		fmt.Println(answer.ID, answer.Status)
	}

	// An item that failed carries its problem; Decode returns it as an error.
	var agent carragent.Agent
	if err := answers.Responses[1].Decode(&agent); err != nil {
		log.Fatal(err)
	}
	fmt.Println(agent.Name)
}

func (*Client) CreateJob

func (c *Client) CreateJob(ctx context.Context, operation JobOperation, params map[string]any) (*JobAccepted, error)

CreateJob starts a job: POST /api/v1/jobs, which answers 202 Accepted.

This is the REST pattern for slow operations, so that code written for one works here unchanged. The operations are fast reads: nothing is queued, and a job's result is worked out when you poll it, from the catalog as it is then. Poll with GetJob or WaitJob.

params are the operation's query parameters as strings ("lang", "limit", "cursor", "id", "q"), or map[string]any{"requests": []BatchItem{...}} for OperationBatch. They may be nil.

Example
package main

import (
	"context"
	"fmt"
	"time"

	carragent "carragent.com/go"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()
	client := carragent.NewClient()

	accepted, err := client.CreateJob(ctx, carragent.OperationSearchCatalog, map[string]any{"q": "late invoices"})
	if err != nil {
		fmt.Println(err)
		return
	}
	job, err := client.WaitJob(ctx, accepted.ID, time.Second)
	if err != nil {
		fmt.Println(err)
		return
	}
	var results carragent.SearchResults
	if err := job.Result.Decode(&results); err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(len(results.Data), "matches")
}

func (*Client) GetAgent

func (c *Client) GetAgent(ctx context.Context, id string, lang Lang) (*Agent, error)

GetAgent returns one agent by its id, such as "collections": GET /api/v1/agents/{id}. lang may be empty.

The error is a *Problem with the code "agent_not_found" (404) when there is no agent with that id.

func (*Client) GetJob

func (c *Client) GetJob(ctx context.Context, id string) (*Job, error)

GetJob returns a job, with its result once it has run: GET /api/v1/jobs/{id}.

The error is a *Problem with the code "job_not_found" (404) for an id the API does not recognise, and "job_expired" (410) once the job is older than 24 hours.

func (*Client) GoString

func (c *Client) GoString() string

GoString is String, so that %#v does not print the token either.

func (*Client) IterAgents

func (c *Client) IterAgents(params *ListAgentsParams) *AgentIterator

IterAgents returns an iterator over every agent. It follows NextCursor from page to page and asks for a page only when Next needs it. params may be nil; its Cursor is where to start, and its Limit is the page size (50 by default, which is the largest).

Example
package main

import (
	"context"
	"fmt"
	"log"

	carragent "carragent.com/go"
)

func main() {
	ctx := context.Background()
	client := carragent.NewClient()

	it := client.IterAgents(&carragent.ListAgentsParams{Lang: carragent.Spanish})
	for it.Next(ctx) {
		fmt.Println(it.Agent().ID, "-", it.Agent().Summary)
	}
	if err := it.Err(); err != nil {
		log.Fatal(err)
	}
}

func (*Client) ListAgents

func (c *Client) ListAgents(ctx context.Context, params *ListAgentsParams) (*AgentList, error)

ListAgents returns one page of the agents: GET /api/v1/agents. params may be nil.

func (*Client) ListPlans

func (c *Client) ListPlans(ctx context.Context, lang Lang) (*PlanList, error)

ListPlans returns the plans, with their prices in US cents (before taxes), their limits and the trial and refund terms: GET /api/v1/plans. There is one page: every plan. lang may be empty.

func (*Client) RegisterAnonymous

func (c *Client) RegisterAnonymous(ctx context.Context) (*Registration, error)

RegisterAnonymous registers this agent anonymously: POST /agent/identity.

The answer holds an IdentityAssertion that is good for 24 hours. There is nothing to claim and no account behind it: it only gives your agent an identity of its own, so that its rate limit is its own. Keep the answer and pass it to UseAnonymousIdentity to get a token. Registrations are limited to 10 a minute per IP address.

Token is not sent to the registration.

func (*Client) Search

func (c *Client) Search(ctx context.Context, q string, lang Lang) (*SearchResults, error)

Search is a keyword search over the agents and the plans, best match first: GET /api/v1/search.

It matches the words of q (those of three letters or more) against the text of the agents and plans in the language lang, ignoring case and accents. It is not a semantic search, so use the words the agents use, such as "late invoices" or "appointment reminders". It returns at most 10 matches. q is 1 to 200 characters; lang may be empty.

func (*Client) String

func (c *Client) String() string

String describes the client without its token.

func (*Client) UseAnonymousIdentity

func (c *Client) UseAnonymousIdentity(ctx context.Context, registration *Registration) (*Token, error)

UseAnonymousIdentity gets a token for an anonymous agent identity, and uses it from now on.

It registers first (RegisterAnonymous), unless you pass the registration you kept from an earlier call. It then exchanges the assertion for an access token at /api/oauth2/token (RFC 7523, the jwt-bearer grant) and sets Token. The token lasts an hour; call this again, with the same registration while it is valid, to get a new one. The assertion goes to the client's BaseURL, whatever address the registration names.

The error is a *Problem with the code "invalid_grant" (400) when the assertion is expired or not Carragent's. Token is left alone when it fails.

Example
package main

import (
	"context"
	"log"

	carragent "carragent.com/go"
)

func main() {
	ctx := context.Background()
	client := carragent.NewClient()

	// Registers this agent anonymously, gets a token for it and uses it from now on. The token lasts an hour. Keep the
	// registration (good for 24 hours) to get a new token without registering again.
	registration, err := client.RegisterAnonymous(ctx)
	if err != nil {
		log.Fatal(err)
	}
	if _, err := client.UseAnonymousIdentity(ctx, registration); err != nil {
		log.Fatal(err)
	}
}

func (*Client) UseClientCredentials

func (c *Client) UseClientCredentials(ctx context.Context, clientID, secret string, options ...TokenOption) (*Token, error)

UseClientCredentials gets a token with an OAuth client's id and secret, and uses it from now on (RFC 6749, client credentials).

The id and secret go to /api/oauth2/token as HTTP Basic credentials, and nowhere else. They are not kept. The token lasts an hour; call this again for a new one. Register a client at POST /api/oauth2/register (https://carragent.com/auth.md has the steps).

The error is a *Problem with the code "invalid_client" (401) for an unknown client or a wrong secret, and "invalid_scope" (400) for a scope beyond the client's. Token is left alone when it fails.

Example
package main

import (
	"context"
	"log"

	carragent "carragent.com/go"
)

func main() {
	ctx := context.Background()
	client := carragent.NewClient()

	// An OAuth client from POST /api/oauth2/register (https://carragent.com/auth.md).
	_, err := client.UseClientCredentials(ctx, "client id", "client secret", carragent.WithScope("agents:read plans:read"))
	if err != nil {
		log.Fatal(err)
	}
}

func (*Client) WaitJob

func (c *Client) WaitJob(ctx context.Context, id string, interval time.Duration) (*Job, error)

WaitJob polls a job until its status is no longer "pending", and returns it. It gives up with ctx's error when ctx is done, so give ctx a deadline.

Today a job has its result on the first poll, so this returns at once. Every poll runs the job again and costs what the job costs against the rate limit: one request, or one per item of a batch. interval is the time between polls; zero means one second.

type Job

type Job struct {
	Object string `json:"object"`
	ID     string `json:"id"`
	// Status is "succeeded" once the job has run. Result says what the operation answered.
	Status      string    `json:"status"`
	Operation   string    `json:"operation"`
	CreatedAt   time.Time `json:"createdAt"`
	CompletedAt time.Time `json:"completedAt"`
	ExpiresAt   time.Time `json:"expiresAt"`
	Result      JobResult `json:"result"`
}

Job is a job as polled: the answer to GetJob.

type JobAccepted

type JobAccepted struct {
	Object string `json:"object"`
	ID     string `json:"id"`
	// Status is "pending".
	Status    string    `json:"status"`
	Operation string    `json:"operation"`
	CreatedAt time.Time `json:"createdAt"`
	// ExpiresAt is when the job stops being available: 24 hours after it was created.
	ExpiresAt time.Time `json:"expiresAt"`
	Links     JobLinks  `json:"links"`
}

JobAccepted is a job that has just been created: the answer to CreateJob, a 202 Accepted.

type JobLinks struct {
	// Self is the job's address, which GetJob polls.
	Self string `json:"self"`
}

JobLinks says where a job is.

type JobOperation

type JobOperation string

JobOperation is what a job runs.

const (
	OperationListAgents    JobOperation = "listAgents"
	OperationGetAgent      JobOperation = "getAgent"
	OperationListPlans     JobOperation = "listPlans"
	OperationSearchCatalog JobOperation = "searchCatalog"
	OperationBatch         JobOperation = "batch"
)

The operations a job can run.

type JobResult

type JobResult struct {
	// Status is the HTTP status the operation answered. It can be a 4xx: the job still succeeded.
	Status int `json:"status"`
	// Body is the operation's JSON, or a problem.
	Body json.RawMessage `json:"body"`
}

JobResult is what a job's operation returned.

func (JobResult) Decode

func (r JobResult) Decode(v any) error

Decode reads the operation's body into v, which is a pointer. It returns the operation's problem instead when its Status is 400 or more.

type Lang

type Lang string

Lang is the language of the text the API returns. The empty Lang means English.

const (
	English Lang = "en"
	Spanish Lang = "es"
	French  Lang = "fr"
)

The languages of the API's text fields.

type ListAgentsParams

type ListAgentsParams struct {
	// Lang is the language of the text. Empty means English.
	Lang Lang
	// Limit is the page size, 1 to 50. Zero means the API's default, 20.
	Limit int
	// Cursor is the NextCursor of the previous page. Empty means the first page.
	Cursor string
}

ListAgentsParams are the optional parameters of ListAgents. The zero value asks for the first page in English.

type Plan

type Plan struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	Summary     string `json:"summary"`
	ForWho      string `json:"forWho"`
	Recommended bool   `json:"recommended"`
	// Currency is always "USD".
	Currency string `json:"currency"`
	// MonthlyCents is the price a month in cents. For a plan that is quoted, it is the starting price.
	MonthlyCents int `json:"monthlyCents"`
	// YearlyCents is the price a year in cents. It is nil when the plan is quoted.
	YearlyCents *int `json:"yearlyCents"`
	// SelfServe is false when the plan is quoted by email rather than bought on the site.
	SelfServe bool       `json:"selfServe"`
	Limits    PlanLimits `json:"limits"`
}

Plan is one plan. Prices are whole US cents, before applicable taxes.

type PlanLimits

type PlanLimits struct {
	Agents           *int `json:"agents"`
	Connections      *int `json:"connections"`
	MessagesPerMonth *int `json:"messagesPerMonth"`
	Seats            *int `json:"seats"`
}

PlanLimits is what a plan includes. A nil limit means unlimited, or quoted.

type PlanList

type PlanList struct {
	Object string    `json:"object"`
	Data   []Plan    `json:"data"`
	Terms  PlanTerms `json:"terms"`
	// NextCursor is always empty.
	NextCursor string `json:"nextCursor"`
	HasMore    bool   `json:"hasMore"`
}

PlanList is every plan and the terms. There is only one page.

type PlanTerms

type PlanTerms struct {
	TrialDays        int    `json:"trialDays"`
	AnnualRefundDays int    `json:"annualRefundDays"`
	SetupFee         int    `json:"setupFee"`
	Contract         bool   `json:"contract"`
	Currency         string `json:"currency"`
}

PlanTerms are the terms that apply to every plan.

type Problem

type Problem struct {
	// Type is the problem's type URI, "about:blank" for the API's own.
	Type string `json:"type,omitempty"`
	// Status is the HTTP status.
	Status int `json:"status"`
	// Code is the API's stable, machine-readable error code, such as "agent_not_found", "invalid_parameter" or
	// "rate_limited"; for the OAuth endpoints, the RFC 6749 error ("invalid_client", "invalid_grant").
	// The client adds four of its own: "http_error" (an error answer that is not a problem document, such as a
	// gateway's HTML page), "invalid_response" (a success answer that is not the JSON it should be),
	// "unexpected_redirect" and "response_too_large".
	Code string `json:"code"`
	// Title is a short summary of the problem.
	Title string `json:"title"`
	// Detail says what went wrong this time, when the API says.
	Detail string `json:"detail,omitempty"`
	// RetryAfter is how long to wait before trying again, from the Retry-After header of a 429 answer. It is zero
	// when there was no such header, or when it said to retry at once.
	RetryAfter time.Duration `json:"-"`
}

Problem is an error answer from the API: an RFC 9457 problem document (application/problem+json), or an OAuth error (RFC 6749) from the token endpoint. Every method of Client returns it as a *Problem, so use errors.As:

var problem *carragent.Problem
if errors.As(err, &problem) && problem.Code == "agent_not_found" {
	// ...
}

Branch on Code, which is stable, rather than on Title or Detail, which are for people.

A request that gets no answer at all (no network, a refused connection, a timeout) is not a Problem: the method returns the transport's error, which wraps the standard ones, so errors.Is(err, context.DeadlineExceeded) works.

Example
package main

import (
	"context"
	"errors"
	"fmt"
	"time"

	carragent "carragent.com/go"
)

func main() {
	client := carragent.NewClient()

	_, err := client.GetAgent(context.Background(), "no-such-agent", "")
	var problem *carragent.Problem
	if errors.As(err, &problem) {
		switch {
		case problem.RateLimited():
			time.Sleep(problem.RetryAfter) // the client never retries by itself
		case problem.Code == "agent_not_found":
			fmt.Println("no such agent:", problem.Detail)
		default:
			fmt.Println(problem.Status, problem.Code, problem.Title)
		}
	}
}

func (*Problem) Error

func (p *Problem) Error() string

Error describes the problem on one line.

func (*Problem) RateLimited

func (p *Problem) RateLimited() bool

RateLimited reports whether the API answered 429, too many requests. Wait RetryAfter and try again: the client never retries by itself.

type Registration

type Registration struct {
	RegistrationID   string `json:"registration_id"`
	RegistrationType string `json:"registration_type"`
	// IdentityAssertion is a signed assertion, good for 24 hours, that UseAnonymousIdentity trades for a token.
	IdentityAssertion string    `json:"identity_assertion"`
	AssertionExpires  time.Time `json:"assertion_expires"`
	PreClaimScopes    []string  `json:"pre_claim_scopes"`
	TokenEndpoint     string    `json:"token_endpoint"`
	GrantType         string    `json:"grant_type"`
}

Registration is an anonymous agent registration, the answer to RegisterAnonymous.

type SearchHit

type SearchHit struct {
	// Kind is "agent" or "plan".
	Kind        string `json:"kind"`
	ID          string `json:"id"`
	Name        string `json:"name"`
	Description string `json:"description"`
	// Score is higher for a better match. Its scale is not part of the API.
	Score float64 `json:"score"`
}

SearchHit is one match of a search: an agent or a plan.

type SearchResults

type SearchResults struct {
	Object string      `json:"object"`
	Query  string      `json:"query"`
	Data   []SearchHit `json:"data"`
	// NextCursor is always empty.
	NextCursor string `json:"nextCursor"`
	HasMore    bool   `json:"hasMore"`
}

SearchResults are the matches of a search, best first. There is only one page.

type Token

type Token struct {
	AccessToken string `json:"access_token"`
	TokenType   string `json:"token_type"`
	// ExpiresIn is the seconds until it expires: an hour.
	ExpiresIn int    `json:"expires_in"`
	Scope     string `json:"scope"`
}

Token is an access token for Authorization: Bearer.

type TokenOption

type TokenOption func(*tokenRequest)

A TokenOption changes a token request.

func WithScope

func WithScope(scope string) TokenOption

WithScope asks for these space-separated scopes ("agents:read plans:read search:read"), within the client's own. Without it, the token gets all of the client's scopes.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL