Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e2cde17
feat(agentstudio): add API client foundation for Agent Studio
cmarguta-alg May 6, 2026
fc16b49
feat(agents): add `algolia agents list/get` for Agent Studio
cmarguta-alg May 6, 2026
d6a5b6f
feat(agents): add create/update/delete/publish/unpublish/duplicate
cmarguta-alg May 6, 2026
97bfae7
feat(agents): add test/run streaming commands
cmarguta-alg May 6, 2026
77557d4
docs(agents): cross-cutting — AGENTS.md, e2e testscripts, dry-run val…
cmarguta-alg May 6, 2026
678c593
refactor(agents): rename `test` → `try`, drop --dry-run from it
cmarguta-alg May 6, 2026
6eda6da
feat(agents): completion runtime knobs + cache invalidate (Phase 5)
cmarguta-alg May 6, 2026
662aff6
feat(agents): providers + configuration sub-groups (Phase 6)
cmarguta-alg May 6, 2026
f20fb09
chore(agents): align files with API tag boundaries
cmarguta-alg May 6, 2026
cd1dc5b
feat(agents): conversations sub-group (Phase 7)
cmarguta-alg May 6, 2026
975fcd0
fix(agents): conversations purge requires at least one date filter
cmarguta-alg May 6, 2026
72c0178
feat(agents): allowed-domains sub-group (Phase 8a)
cmarguta-alg May 6, 2026
d3c3733
feat(agents): secret-keys sub-group + lift MaskInput to shared (Phase…
cmarguta-alg May 6, 2026
9669787
feat(agents): feedback + user-data sub-groups (Phase 8c)
cmarguta-alg May 6, 2026
da322ea
feat(agents): hidden internal endpoints + providers defaults (Phase 8d)
cmarguta-alg May 6, 2026
7fbc7e1
fix(agents): refuse user-tokens containing "/" with a clear error (An…
cmarguta-alg May 6, 2026
2d17e79
feat(agents): rich TTY streaming + bulk-delete count + mask coverage
cmarguta-alg May 7, 2026
fc250d1
docs(agents): consolidate Agent Studio reference + strip verbose comm…
cmarguta-alg May 7, 2026
f050530
refactor(agents): lift duplicated test/runtime helpers into shared/
cmarguta-alg May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@ ALGOLIA_API_URL=
ALGOLIA_SEARCH_HOSTS=
ALGOLIA_OAUTH_CLIENT_ID=
ALGOLIA_OAUTH_SCOPE=
# Default Agent Studio base URL baked into the binary at build time.
# Leave empty for production builds (the CLI falls back to the
# cluster-proxy URL https://<appID>.algolia.net/agent-studio).
# Internal beta builds usually set this to:
# ALGOLIA_AGENT_STUDIO_URL=https://agent-studio.staging.eu.algolia.com
ALGOLIA_AGENT_STUDIO_URL=
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ Use narrower verification for small edits.
- Put shared command logic in focused helper packages, usually `pkg/cmdutil`.
- Keep docs-generation logic in `internal/docs` and `cmd/docs`.

## Agent Studio (`pkg/cmd/agents/...`, `api/agentstudio/`)

See [`docs/agents.md`](docs/agents.md) for the command surface, file layout, auth/host resolution, streaming protocols, dry-run semantics, secret masking, telemetry, and the live-backend gotchas. Don't duplicate that content here or in code comments.

## Code Style

### Imports
Expand Down
3 changes: 2 additions & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ tasks:
-X github.com/algolia/cli/api/dashboard.DefaultAPIURL=$ALGOLIA_API_URL
-X github.com/algolia/cli/pkg/config.DefaultSearchHosts=$ALGOLIA_SEARCH_HOSTS
-X github.com/algolia/cli/pkg/auth.DefaultOAuthClientID=$ALGOLIA_OAUTH_CLIENT_ID
-X 'github.com/algolia/cli/api/dashboard.DefaultOAuthScope=$ALGOLIA_OAUTH_SCOPE'"
-X 'github.com/algolia/cli/api/dashboard.DefaultOAuthScope=$ALGOLIA_OAUTH_SCOPE'
-X github.com/algolia/cli/api/agentstudio.DefaultBaseURL=$ALGOLIA_AGENT_STUDIO_URL"
-o algolia cmd/algolia/main.go
vars:
VERSION: '{{ .VERSION | default "main" }}'
Expand Down
217 changes: 217 additions & 0 deletions api/agentstudio/agents.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package agentstudio

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
)

// ListAgents calls GET /1/agents.
func (c *Client) ListAgents(ctx context.Context, params ListAgentsParams) (*PaginatedAgentsResponse, error) {
q := url.Values{}
if params.Page > 0 {
q.Set("page", strconv.Itoa(params.Page))
}
if params.Limit > 0 {
q.Set("limit", strconv.Itoa(params.Limit))
}
if params.ProviderID != "" {
q.Set("providerId", params.ProviderID)
}

endpoint := c.cfg.BaseURL + "/1/agents"
if encoded := q.Encode(); encoded != "" {
endpoint += "?" + encoded
}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
c.setHeaders(req)

resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("agent studio: list agents: %w", err)
}
defer resp.Body.Close()

if err := checkResponse(resp); err != nil {
return nil, err
}

var out PaginatedAgentsResponse
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return nil, fmt.Errorf("agent studio: decode list agents response: %w", err)
}
return &out, nil
}

// GetAgent calls GET /1/agents/{id}.
func (c *Client) GetAgent(ctx context.Context, id string) (*Agent, error) {
if strings.TrimSpace(id) == "" {
return nil, fmt.Errorf("agent studio: agent id is required")
}

endpoint := c.cfg.BaseURL + "/1/agents/" + url.PathEscape(id)

req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
c.setHeaders(req)

resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("agent studio: get agent: %w", err)
}
defer resp.Body.Close()

if err := checkResponse(resp); err != nil {
return nil, err
}

var out Agent
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return nil, fmt.Errorf("agent studio: decode get agent response: %w", err)
}
return &out, nil
}

// CreateAgent calls POST /1/agents. Body is opaque JSON; see docs/agents.md.
func (c *Client) CreateAgent(ctx context.Context, body json.RawMessage) (*Agent, error) {
if len(body) == 0 {
return nil, fmt.Errorf("agent studio: create agent: body is required")
}
return c.doAgentMutation(ctx, http.MethodPost, c.cfg.BaseURL+"/1/agents", body, "create agent")
}

// UpdateAgent calls PATCH /1/agents/{id}.
func (c *Client) UpdateAgent(ctx context.Context, id string, body json.RawMessage) (*Agent, error) {
if strings.TrimSpace(id) == "" {
return nil, fmt.Errorf("agent studio: agent id is required")
}
if len(body) == 0 {
return nil, fmt.Errorf("agent studio: update agent: body is required")
}
endpoint := c.cfg.BaseURL + "/1/agents/" + url.PathEscape(id)
return c.doAgentMutation(ctx, http.MethodPatch, endpoint, body, "update agent")
}

// DeleteAgent calls DELETE /1/agents/{id}. Backend soft-deletes;
// recovery is a backend ops concern.
func (c *Client) DeleteAgent(ctx context.Context, id string) error {
if strings.TrimSpace(id) == "" {
return fmt.Errorf("agent studio: agent id is required")
}
endpoint := c.cfg.BaseURL + "/1/agents/" + url.PathEscape(id)

req, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil)
if err != nil {
return err
}
c.setHeaders(req)

resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("agent studio: delete agent: %w", err)
}
defer resp.Body.Close()

return checkResponse(resp)
}

// PublishAgent calls POST /1/agents/{id}/publish.
func (c *Client) PublishAgent(ctx context.Context, id string) (*Agent, error) {
return c.doAgentLifecycle(ctx, id, "publish")
}

// UnpublishAgent calls POST /1/agents/{id}/unpublish.
func (c *Client) UnpublishAgent(ctx context.Context, id string) (*Agent, error) {
return c.doAgentLifecycle(ctx, id, "unpublish")
}

// DuplicateAgent calls POST /1/agents/{id}/duplicate.
func (c *Client) DuplicateAgent(ctx context.Context, id string) (*Agent, error) {
return c.doAgentLifecycle(ctx, id, "duplicate")
}

// InvalidateAgentCache calls DELETE /1/agents/{id}/cache?before=YYYY-MM-DD.
// Empty before = wipe all cache entries. Date format is validated
// server-side (see docs/agents.md gotchas).
func (c *Client) InvalidateAgentCache(ctx context.Context, id, before string) error {
if strings.TrimSpace(id) == "" {
return fmt.Errorf("agent studio: agent id is required")
}

endpoint := c.cfg.BaseURL + "/1/agents/" + url.PathEscape(id) + "/cache"
if before != "" {
q := url.Values{}
q.Set("before", before)
endpoint += "?" + q.Encode()
}

req, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil)
if err != nil {
return err
}
c.setHeaders(req)

resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("agent studio: invalidate agent cache: %w", err)
}
defer resp.Body.Close()

return checkResponse(resp)
}

func (c *Client) doAgentLifecycle(ctx context.Context, id, verb string) (*Agent, error) {
if strings.TrimSpace(id) == "" {
return nil, fmt.Errorf("agent studio: agent id is required")
}
endpoint := c.cfg.BaseURL + "/1/agents/" + url.PathEscape(id) + "/" + verb
return c.doAgentMutation(ctx, http.MethodPost, endpoint, nil, verb+" agent")
}

func (c *Client) doAgentMutation(
ctx context.Context,
method, endpoint string,
body json.RawMessage,
errLabel string,
) (*Agent, error) {
var reqBody io.Reader
if body != nil {
reqBody = strings.NewReader(string(body))
}

req, err := http.NewRequestWithContext(ctx, method, endpoint, reqBody)
if err != nil {
return nil, err
}
c.setHeaders(req)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}

resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("agent studio: %s: %w", errLabel, err)
}
defer resp.Body.Close()

if err := checkResponse(resp); err != nil {
return nil, err
}

var out Agent
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return nil, fmt.Errorf("agent studio: decode %s response: %w", errLabel, err)
}
return &out, nil
}
Loading
Loading