Skip to content
156 changes: 132 additions & 24 deletions cmd/agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package cmd

import (
"context"
"encoding/json"
"fmt"
"io"

glean "github.com/gleanwork/api-client-go"
Expand All @@ -11,10 +13,21 @@ import (
"github.com/spf13/cobra"
)

// agentIDRequest is a CLI-only request struct for commands that take an agent
// ID as their only input. Using camelCase JSON tag for CLI consistency.
// agentIDRequest is a CLI-only request struct for commands whose only input
// is an agent ID. The snake_case tag matches the platform input shape;
// cmdutil transparently normalizes camelCase agentId.
type agentIDRequest struct {
AgentID string `json:"agentId"`
AgentID string `json:"agent_id"`
}

// agentRunRequest is the CLI request for `agents run`. The platform route
// takes the agent ID as a path parameter (POST /api/agents/{agent_id}/runs),
// so the CLI folds it into the --json body alongside the run inputs.
type agentRunRequest struct {
AgentID string `json:"agent_id"`
Input map[string]any `json:"input,omitempty"`
Messages []components.PlatformMessage `json:"messages,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
}

func NewCmdAgents() *cobra.Command {
Expand All @@ -25,11 +38,19 @@ func NewCmdAgents() *cobra.Command {

Agents are AI-powered workflows that can search, reason, and act on your company's knowledge.

Agent commands are served by the platform API (/api/agents/...) and responses
use its snake_case shape. list/get/schemas fall back to the classic API with a
warning when the platform API is not enabled; run does not fall back
automatically because its request body differs between the two surfaces. Set
GLEAN_LEGACY_APIS=1 to use the classic API directly (run then expects the
classic messages/fragments body).

Example:
glean agents list
glean agents get --json '{"agentId":"<id>"}'
glean agents schemas --json '{"agentId":"<id>"}'
glean agents run --json '{"agentId":"<id>","messages":[{"author":"USER","fragments":[{"text":"summarize Q1 results"}]}]}'`,
glean agents get --json '{"agent_id":"<id>"}'
glean agents schemas --json '{"agent_id":"<id>"}'
glean agents run --json '{"agent_id":"<id>","input":{"query":"summarize Q1 results"}}'
glean agents run --json '{"agent_id":"<id>","messages":[{"role":"user","content":[{"type":"text","text":"summarize Q1 results"}]}]}'`,
}
cmd.AddCommand(
newAgentsListCmd(),
Expand All @@ -40,26 +61,56 @@ Example:
return cmd
}

// agentsListTextFn renders whichever agents list shape the request produced:
// the platform response (default) or the classic response (legacy fallback).
func agentsListTextFn(w io.Writer, v any) error {
header := []string{"ID", "NAME", "DESCRIPTION"}
switch resp := v.(type) {
case *components.PlatformAgentsSearchResponse:
rows := make([][]string, len(resp.Agents))
for i, a := range resp.Agents {
desc := ""
if a.Description != nil {
desc = output.Truncate(*a.Description, 60)
}
rows[i] = []string{a.AgentID, a.Name, desc}
}
return output.WriteTable(w, header, rows)
case *components.SearchAgentsResponse:
rows := make([][]string, len(resp.Agents))
for i, a := range resp.Agents {
desc := ""
if a.Description != nil {
desc = output.Truncate(*a.Description, 60)
}
rows[i] = []string{a.AgentID, a.Name, desc}
}
return output.WriteTable(w, header, rows)
default:
return output.WriteJSON(w, v)
}
}

func newAgentsListCmd() *cobra.Command {
return cmdutil.Build(cmdutil.Spec[components.SearchAgentsRequest]{
Use: "list",
Short: "List available agents",
TextFn: func(w io.Writer, v any) error {
resp, ok := v.(*components.SearchAgentsResponse)
if !ok {
return output.WriteJSON(w, v)
return cmdutil.Build(cmdutil.Spec[components.PlatformAgentsSearchRequest]{
Use: "list",
Short: "List available agents",
Endpoint: "/api/agents/search",
TextFn: agentsListTextFn,
Run: func(ctx context.Context, sdk *glean.Glean, req components.PlatformAgentsSearchRequest) (any, error) {
resp, err := sdk.Agents.Search(ctx, req)
if err != nil {
return nil, err
}
rows := make([][]string, len(resp.Agents))
for i, a := range resp.Agents {
desc := ""
if a.Description != nil {
desc = output.Truncate(*a.Description, 60)
return resp.PlatformAgentsSearchResponse, nil
},
LegacyRun: func(ctx context.Context, sdk *glean.Glean, rawJSON []byte) (any, error) {
var req components.SearchAgentsRequest
if len(rawJSON) > 0 {
if err := json.Unmarshal(rawJSON, &req); err != nil {
return nil, fmt.Errorf("invalid --json: %w", err)
}
rows[i] = []string{a.AgentID, a.Name, desc}
}
return output.WriteTable(w, []string{"ID", "NAME", "DESCRIPTION"}, rows)
},
Run: func(ctx context.Context, sdk *glean.Glean, req components.SearchAgentsRequest) (any, error) {
resp, err := sdk.Client.Agents.List(ctx, req)
if err != nil {
return nil, err
Expand All @@ -69,12 +120,34 @@ func newAgentsListCmd() *cobra.Command {
})
}

// parseAgentIDRequest is the shared LegacyRun payload parse for get/schemas:
// the input is a bare agent ID, identical on both surfaces.
func parseAgentIDRequest(rawJSON []byte) (agentIDRequest, error) {
var req agentIDRequest
if err := json.Unmarshal(rawJSON, &req); err != nil {
return req, fmt.Errorf("invalid --json: %w", err)
}
return req, nil
}

func newAgentsGetCmd() *cobra.Command {
return cmdutil.Build(cmdutil.Spec[agentIDRequest]{
Use: "get",
Short: "Get an agent by ID",
JSONRequired: true,
Endpoint: "/api/agents/{agent_id}",
Run: func(ctx context.Context, sdk *glean.Glean, req agentIDRequest) (any, error) {
resp, err := sdk.Agents.Get(ctx, req.AgentID)
if err != nil {
return nil, err
}
return resp.PlatformAgentGetResponse, nil
},
LegacyRun: func(ctx context.Context, sdk *glean.Glean, rawJSON []byte) (any, error) {
req, err := parseAgentIDRequest(rawJSON)
if err != nil {
return nil, err
}
resp, err := sdk.Client.Agents.Retrieve(ctx, req.AgentID, nil, nil)
if err != nil {
return nil, err
Expand All @@ -89,7 +162,19 @@ func newAgentsSchemasCmd() *cobra.Command {
Use: "schemas",
Short: "Get the schemas for an agent",
JSONRequired: true,
Endpoint: "/api/agents/{agent_id}/schemas",
Run: func(ctx context.Context, sdk *glean.Glean, req agentIDRequest) (any, error) {
resp, err := sdk.Agents.GetSchemas(ctx, req.AgentID, nil)
if err != nil {
return nil, err
}
return resp.PlatformAgentSchemasResponse, nil
},
LegacyRun: func(ctx context.Context, sdk *glean.Glean, rawJSON []byte) (any, error) {
req, err := parseAgentIDRequest(rawJSON)
if err != nil {
return nil, err
}
resp, err := sdk.Client.Agents.RetrieveSchemas(ctx, req.AgentID, nil, nil)
if err != nil {
return nil, err
Expand All @@ -100,11 +185,34 @@ func newAgentsSchemasCmd() *cobra.Command {
}

func newAgentsRunCmd() *cobra.Command {
return cmdutil.Build(cmdutil.Spec[components.AgentRunCreate]{
return cmdutil.Build(cmdutil.Spec[agentRunRequest]{
Use: "run",
Short: "Run an agent (synchronous)",
JSONRequired: true,
Run: func(ctx context.Context, sdk *glean.Glean, req components.AgentRunCreate) (any, error) {
Endpoint: "/api/agents/{agent_id}/runs",
// The run body is user-authored and shaped differently on the two
// surfaces (platform input/messages vs classic messages/fragments),
// so gate-closed errors instead of silently replaying the payload.
FallbackMode: cmdutil.FallbackEnvOnly,
Run: func(ctx context.Context, sdk *glean.Glean, req agentRunRequest) (any, error) {
if req.AgentID == "" {
return nil, fmt.Errorf("agent_id is required in the --json payload")
}
resp, err := sdk.Agents.CreateRun(ctx, req.AgentID, components.PlatformAgentRunCreateRequest{
Input: req.Input,
Messages: req.Messages,
Metadata: req.Metadata,
})
if err != nil {
return nil, err
}
return resp.PlatformAgentRunWaitResponse, nil
},
LegacyRun: func(ctx context.Context, sdk *glean.Glean, rawJSON []byte) (any, error) {
var req components.AgentRunCreate
if err := json.Unmarshal(rawJSON, &req); err != nil {
return nil, fmt.Errorf("invalid --json: %w", err)
}
resp, err := sdk.Client.Agents.Run(ctx, req)
if err != nil {
return nil, err
Expand Down
Loading