README
¶
Agently
Agently is a full-featured AI agent platform built on agently-core. It provides an HTTP server, embedded web UI, CLI, and workspace management for creating and interacting with AI agents powered by LLMs.
Why Agently
- Secure MCP hosting — authority matching, HTTPS-only header reuse, and origin/audience allowlists prevent credential leakage while supporting bearer-first and BFF cookie reuse.
- Conversation-scoped orchestration — MCP clients, elicitation, and tool calls are associated to a conversation, preserving auth/session boundaries across multi-step flows.
- Workspace-driven operations — agents, models, MCP clients, and policies live in the workspace; reproducible, reviewable, and environment-overridable.
- OAuth / JWT authentication — BFF, SPA, bearer, mixed, and local auth modes; RSA/HMAC JWT; distributed token refresh across pods.
- A2A protocol — agent-to-agent communication via
/.well-known/agent.jsonand/v1/api/a2a/*. - MCP tool exposure — optionally expose workspace tools as an MCP HTTP server for external agents.
- Parallel tool calls — enabled by default; agents invoke multiple tools concurrently within a single reasoning step.
- Embedded web UI — Forge-based React UI served directly from the binary.
Features
- Multi-LLM support: OpenAI, Vertex AI (Gemini + Claude), Bedrock Claude, Grok, InceptionLabs, Ollama
- MCP integration with per-user BFF auth round-tripper
- Conversation management with SQLite (default) or MySQL
- Scheduler: cron/interval/adhoc with distributed lease coordination
- JWT (RSA/HMAC) and OAuth BFF/SPA/bearer authentication
- CLI:
query,list-tools,chatgpt-login,serve - Embedded Forge web UI with navigation and window metadata
Installation
# Prerequisites: Go 1.25+, Node.js (for UI builds)
git clone https://github.com/viant/agently.git
cd agently/agently
export OPENAI_API_KEY=your_key
go build -o agently .
Quick Start
# Start the server (default :8080)
./agently serve
# Start on a custom port
./agently serve -a :9595
# Start with a specific workspace
./agently serve -w /path/to/workspace
# Query an agent (auto-detects local server)
./agently query
# Query with a prompt
./agently query -q "How many tables in my database?"
# List available tools
./agently list-tools
# Login to ChatGPT OAuth
./agently chatgpt-login --clientURL "scy://..."
Server Options
./agently serve [flags]
-a, --addr Listen address (default :8080)
-w, --workspace Workspace root path (overrides AGENTLY_WORKSPACE)
-p, --policy Tool policy: auto|ask|deny (default: auto)
--expose-mcp Expose tools as MCP HTTP server
--ui-dist Optional local UI dist directory
-d, --debug Enable debug logging
Tool Policy
Agently has two layers of tool control:
- coarse runtime tool policy
- per-bundle approval rules
The coarse runtime policy is set by --policy on agently serve.
Available modes:
auto— normal operation; tools run when allowed by the selected bundle and agentask— interactive approval-oriented mode for risky operationsdeny— deny tool execution
Approval rules are separate from the coarse runtime policy and live on tool bundle match rules.
Supported approval modes:
none— no approvalprompt— block the active turn and ask inlinequeue— create a queued approval item for the user
Approval is configured at the bundle-rule level, not on agent tool items.
match:
- name: "system/os:*"
approval:
mode: queue
Execution order:
- coarse runtime policy is checked first
- matching bundle approval config is resolved
- approval mode is applied (
none,prompt, orqueue)
That means:
denystill denies before approval is considered- approval only applies after the tool is otherwise allowed
- queue/prompt approval is a finer-grained control than the top-level policy
Configuration
Agently uses a workspace directory ($AGENTLY_WORKSPACE, default ~/.agently) with YAML files:
~/.agently/
config.yaml # defaults, auth, internalMCP
agents/ # agent definitions (*.yaml)
models/ # LLM/embedder configs (*.yaml)
embedders/ # embedder configs (*.yaml)
mcp/ # MCP client definitions (*.yaml)
tools/
bundles/ # tool bundle definitions
config.yaml defaults
default:
agent: chatter
model: openai_gpt-5.4
embedder: openai_text
auth:
enabled: true
cookieName: agently_session
defaultUsername: devuser # auto-login for local dev; remove for production
ipHashKey: your-hmac-salt
local:
enabled: true
# OAuth IDP (BFF mode — uncomment to enable SSO)
# oauth:
# mode: bff
# name: my-idp
# label: My IDP
# client:
# configURL: "" # scy resource URL for OAuth client config
# redirectURI: "" # https://your-host/v1/api/auth/oauth/callback
# scopes: [openid, profile, email]
Environment Variables
| Variable | Default | Purpose |
|---|---|---|
AGENTLY_WORKSPACE |
~/.agently |
Workspace root |
AGENTLY_ADDR |
:8080 |
Listen address |
AGENTLY_DB_DRIVER |
sqlite |
Database driver |
AGENTLY_DB_DSN |
(workspace SQLite) | Database connection string |
AGENTLY_UI_DIST |
(embedded) | Optional local UI dist path |
AGENTLY_DEBUG |
false |
Enable verbose logging |
AGENTLY_SCHEDULER_RUNNER |
false |
Enable scheduler watchdog in-process (scheduled runs only) |
AGENTLY_SCHEDULER_API |
true |
Mount scheduler HTTP endpoints |
AGENTLY_SCHEDULER_RUN_NOW |
true |
Enable run-now endpoint |
AGENTLY_SCHEDULER_MAX_CONCURRENT_RUNS |
0 |
Cap on in-flight scheduler runs; 0 = unbounded |
AGENTLY_CHATGPT_CALLBACK_PORT |
1455 |
Local OAuth callback port for agently chatgpt-login. Integer or auto (OS-picked). Must match the OAuth redirect allowlist for OpenAI; auto only works with issuers accepting arbitrary localhost ports. Overridden by --port. |
Authentication
Local (development)
Default config.yaml enables local auth with a dev user. Remove defaultUsername and set local.enabled: false for production.
JWT
Add to config.yaml:
auth:
enabled: true
ipHashKey: your-hmac-salt
cookieName: agently_session
jwt:
enabled: true
rsa:
- /path/to/public.pem
rsaPrivateKey: /path/to/private.pem
OAuth BFF
auth:
enabled: true
ipHashKey: your-hmac-salt
cookieName: agently_session
oauth:
mode: bff
label: My IDP
client:
configURL: "scy://..." # encrypted OAuth client config
redirectURI: "https://your-host/v1/api/auth/oauth/callback"
scopes: [openid, profile, email]
Token Refresh
Tokens are proactively refreshed before expiry (default: 15 min lead time). Configurable:
auth:
tokenRefreshLeadMinutes: 15 # refresh tokens this many minutes before expiry
Agent Configuration
Agents are YAML files under $AGENTLY_WORKSPACE/agents/:
# my-agent.yaml
id: my-agent
name: My Agent
modelRef: openai_gpt-5.4
temperature: 0
parallelToolCalls: true # enable parallel tool calls (default when omitted)
tool:
- pattern: system/exec # internal tools
- pattern: sqlkit # MCP tool patterns
knowledge:
- url: knowledge/
profile:
enabled: true
name: My Agent
description: "What this agent does"
tags: [code, data]
MCP Server Setup
# $AGENTLY_WORKSPACE/mcp/sqlkit.yaml
name: sqlkit
transport:
type: sse
url: http://localhost:5000
# Start an MCP server (example: mcp-sqlkit)
git clone https://github.com/viant/mcp-sqlkit
cd mcp-sqlkit && go run ./cmd/mcp-sqlkit -a :5000
MCP Tool Exposure
Expose workspace tools as an MCP HTTP server for external agents:
./agently serve --expose-mcp
Configure in config.yaml:
mcpServer:
port: 9090
toolItems:
- "system/*"
- "resources"
CLI Reference
agently serve
Start the HTTP server.
./agently serve -a :8080 -w /path/to/workspace
agently query
Query an agent interactively or one-shot. Auto-detects a running local server.
./agently query
./agently query -q "What is the schema of my database?"
./agently query --api http://server:8080 --token $TOKEN
./agently query --oob "/path/to/user_cred.enc|blowfish://default"
Flags:
-q, --query— prompt text-a, --agent-id— agent identifier-c, --conv— conversation ID to continue--api— server URL (skip auto-detect)--token/AGENTLY_TOKEN— Bearer token--oob/AGENTLY_OOB_SECRETS— OOB credentials for BFF auth
agently list-tools
List available tools from the running server.
./agently list-tools
./agently list-tools --api http://server:8080
./agently list-tools -s system/os
./agently list-tools -n system/exec.execute --json
agently mcp list
List tools in an MCP-oriented format.
./agently mcp list --api http://server:8080 --token $TOKEN
./agently mcp list --api http://server:8080 --session $SESSION_ID
./agently mcp list -s forecasting --api http://server:8080 --token $TOKEN
./agently mcp list -n forecasting/Total --example --schema --json
agently mcp run
Run a tool by exact name with JSON arguments.
./agently mcp run -n forecasting/Total -a '{"viewId":"TOTAL"}' --api http://server:8080 --token $TOKEN
./agently mcp run -n forecasting/Total -a '{"viewId":"TOTAL"}' --api http://server:8080 --session $SESSION_ID
./agently mcp run -n resources/read -a @args.json --api http://server:8080 --token $TOKEN --json
agently chatgpt-login
Login via ChatGPT/OpenAI OAuth and persist tokens.
./agently chatgpt-login --clientURL "scy://..."
Project Structure
agently/
agently/ # Binary entry point (package main)
main.go # Imports cmd/agently, wires cloud storage
build.yaml # Endly build pipeline
cmd/agently/ # CLI commands: serve, query, list-tools, chatgpt-login
main.go # Serve() and server orchestration (package agently)
server/ # HTTP auth, OAuth endpoints, speech, JWT keygen
runtime/ # Model/embedder finders, tool plugins, scheduler options
bootstrap/ # Workspace default seeding and config loading
defaults/ # Default agent, model, embedder YAML files
metadata/ # Forge UI navigation/window metadata (embed)
deployment/ui/ # Built UI bundle (embed)
ui/ # React/Vite UI source
e2e/ # End-to-end tests (endly + Go)
e2e/build-ui-embed.sh # UI build script
Multi-Platform Architecture
Agently is a multi-platform app:
web— embedded Forge/React UI served by theagentlybinaryios— SwiftUI app using localAgentlySDKandForgeIOSPackageandroid— Compose app using localagently-core-sdkandforge-sdk
Shared Target Context
Platform targeting should use one shared shape across metadata requests, query context, and runtime resolution:
{
"platform": "web|android|ios",
"formFactor": "desktop|tablet|phone",
"surface": "browser|app",
"capabilities": ["markdown", "chart", "upload", "code", "diff"]
}
Rules:
- Forge should own the canonical target-context contract for metadata-driven UI targeting
- Agently should reuse that same shape for metadata calls and
context.clientinstead of inventing an app-specific variant - server-side metadata resolution should consume the same shape
- client-side fallback resolution should consume the same shape
Metadata Branching
Metadata should be separated by explicit platform and form-factor branches instead of letting mobile changes mutate shared web windows.
Recommended structure:
metadata/window/<window-key>/
shared/
web/
android/
phone/
tablet/
ios/
phone/
tablet/
Resolution order should be:
- exact platform + form factor
- platform
- shared
- legacy fallback only during migration
Important Constraint
Mobile work must not remove metadata that web still depends on.
Local Multi-Repo Development
This repo now expects local multi-repo refactors to use the workspace file at:
/Users/awitas/go/src/github.com/viant/go.work
That workspace ties together:
agentlyagently-coreforge
Use go.work for local cross-repo development instead of committing module-level
replace directives in go.mod.
Request-Scoped SDK Debug Logging
Agently reuses the agently-core request-scoped SDK debug contract. For HTTP
SDK sessions, callers can enable debug logging without turning on global process
debug:
- Go HTTP SDK:
sdk.WithSessionDebug("trace", "conversation", "reactor") - TypeScript SDK:
sessionDebug: { level: "trace", components: ["conversation", "reactor"] } - iOS SDK:
SessionDebugOptions(level: "trace", components: ["conversation", "reactor"]) - Android SDK:
SessionDebugOptions(level = "trace", components = listOf("conversation", "reactor"))
These emit:
X-Agently-DebugX-Agently-Debug-LevelX-Agently-Debug-Components
If a surface needs mobile-specific behavior:
- create
android/phone,android/tablet,ios/phone, orios/tabletbranches - keep
web/as a first-class target - keep
shared/minimal and stable
This is especially important for top-level Forge windows such as:
chat/newchat/conversationsscheduleschedule/historyagentmodeloauthmcppreferencestoolworkflow
Current Migration Direction
The current migration work is tracked in:
/Users/awitas/go/src/github.com/viant/agently/multi-platform.md
That document tracks:
- Forge backend loader work for server-side target-aware metadata selection
- target-aware
$import(...)resolution - explicit web / iOS / Android metadata branch migration
- final three-platform verification
UI Development
# Build the embedded UI bundle safely
cd ui && npm run build:embed
# Alternative wrapper (also safe)
./e2e/build-ui-embed.sh
# Rebuild binary with updated UI
cd agently && go build -o agently .
# Dev mode (proxies to local server at localhost:9393)
cd ui && npm run dev
Notes:
- Do not copy
ui/distintodeployment/uiwith a rawrsync --deleteunless you preservedeployment/ui/init.go. npm run build:embedand./e2e/build-ui-embed.shalready handle the safe sync path for the embedded bundle.
Approval Editors And Callbacks
Tool approvals can now expose generic, selector-driven editors. The same selector is used both to extract editable data from the original tool request and to write the user-edited value back into that request before the tool executes.
Supported built-in editor kinds:
checkbox_list— keep/remove items from a collectionradio_list— choose exactly one record from a collection
These editors are supported in:
- prompt approval in the web UI
- prompt approval in the CLI
- queue approval in the web UI
Workspace Bundle Example
The example below turns system/os:getEnv approval into a checkbox list. The
editor reads from input.names and writes the filtered list back to the same
path.
# $AGENTLY_WORKSPACE/tools/bundles/system_os.yaml
id: system/os
title: System OS
description: OS helpers (e.g. environment variables)
iconRef: builtin:system-os
priority: 60
match:
- name: "system/os:*"
approval:
mode: queue
prompt:
acceptLabel: "Allow"
rejectLabel: "Deny"
cancelLabel: "Cancel"
ui:
editable:
- name: names
selector: input.names
kind: checkbox_list
label: Environment variables
description: Choose which environment variables this tool may access.
forge:
windowRef: chat/new
containerRef: approvalEnvPicker
dataSource: approvalEditor
Record Collection Example
For record collections, use relative selectors for item fields. These selectors are evaluated against each collection item.
approval:
mode: queue
ui:
editable:
- name: records
selector: input.records
kind: radio_list
label: Records
itemValueSelector: id
itemLabelSelector: label
itemDescriptionSelector: description
In this example:
selector: input.recordsextracts the collection from the tool requestitemValueSelector: idresolvesrecord.iditemLabelSelector: labelresolvesrecord.label- the selected record is written back to
records
Approval Callback Payload
Approval callbacks are optional. They run inside the active Forge window
context, using the same lookupHandler(...) mechanism as other Forge actions.
Callback input shape:
type ApprovalCallbackPayload = {
approval?: {
type?: string
toolName?: string
title?: string
message?: string
acceptLabel?: string
rejectLabel?: string
cancelLabel?: string
editors?: Array<{
name: string
kind: string
path?: string
label?: string
description?: string
options?: Array<{
id: string
label: string
description?: string
selected: boolean
}>
}>
}
editedFields?: Record<string, unknown>
originalArgs?: Record<string, unknown>
event?: string
}
Callback return shape:
type ApprovalCallbackResult = {
editedFields?: Record<string, unknown>
action?: string
}
Callback lifecycle:
- callbacks run in the order declared under
approval.ui.forge.callbacks - a callback runs only for its matching event, or for all events when
eventis omitted editedFieldsare shallow-merged; later callbacks win on conflictsactionoverrides are also last-wins- missing handlers are skipped by the SDK; handler resolution is supplied by the host UI
Example Forge Handler
The example below keeps callback logic generic and deterministic: it receives the current edited selection and original request, and returns the normalized edited field payload that will be written back into the tool request.
// Example Forge action handler
export async function filterEnvNames({ editedFields = {}, originalArgs = {} }) {
const requested = Array.isArray(originalArgs.names) ? originalArgs.names : [];
const selected = new Set(
Array.isArray(editedFields.names) ? editedFields.names : requested
);
return {
editedFields: {
names: requested.filter((name) => selected.has(name))
}
};
}
To use that handler end to end:
- register it in the active Forge window context so
lookupHandler(...)resolves your handler name - reference it from
approval.ui.forge.callbacks - the built-in approval UI renders the editor
- the callback can normalize or rewrite
editedFields - Agently writes the final edited value back to the same selector path before tool execution
Current Behavior
What works today:
- built-in approval dialogs render
checkbox_listandradio_list - queue approvals and prompt approvals both support
editedFields - the same selector path is used for extract and write-back
- Forge callbacks can post-process
editedFieldsbefore the approval decision is submitted
What is not implemented yet:
- mounting a fully custom Forge approval container from
windowRef/containerRef/dataSource
The current runtime uses the built-in approval editor UI and optional Forge callbacks together.
Custom Forge Approval Container
When you need a fully custom approval experience, you can point approval UI at an existing Forge window/container/data source.
approval:
mode: queue
ui:
editable:
- name: names
selector: input.names
kind: checkbox_list
forge:
windowRef: chat/new
containerRef: approvalEnvPicker
dataSource: approvalEditor
callbacks:
- event: approve
handler: myApproval.normalizeSelection
Canonical metadata example included in this repo:
Expected Forge data source shape:
{
"approval": {
"type": "tool_approval",
"toolName": "system/os/getEnv",
"title": "OS Env Access",
"message": "The agent wants access to your HOME, SHELL, and PATH environment variables.",
"editors": [
{
"name": "names",
"kind": "checkbox_list",
"path": "names",
"options": [
{ "id": "HOME", "label": "HOME", "selected": true },
{ "id": "SHELL", "label": "SHELL", "selected": true },
{ "id": "PATH", "label": "PATH", "selected": true }
]
}
]
},
"editedFields": {
"names": ["HOME", "PATH"]
},
"originalArgs": {
"names": ["HOME", "SHELL", "PATH"]
}
}
The approval container is expected to edit editedFields. On approve, Agently:
- reads the current Forge data source form values
- takes
editedFields - runs any configured approval callbacks
- writes the final edited value back to the original tool request using the same selector path
- executes the tool with the rewritten request
Example Forge handler:
export async function normalizeSelection({ editedFields = {}, originalArgs = {} }) {
const requested = Array.isArray(originalArgs.names) ? originalArgs.names : [];
const selected = new Set(
Array.isArray(editedFields.names) ? editedFields.names : requested
);
return {
editedFields: {
names: requested.filter((name) => selected.has(name))
}
};
}
Strict Behavior
If approval.ui.forge.containerRef is configured, Agently treats that as an
explicit instruction to use the Forge approval renderer.
There is no silent fallback to the built-in approval editor.
If the Forge window/context/container/data source cannot be resolved:
- the approval dialog shows an explicit Forge error
- approve is disabled
- the user must fix the Forge configuration before continuing
Example Outcome
For a request like:
What are my HOME, SHELL, and PATH environment variables?
if the user deselects SHELL in the approval editor, Agently rewrites the tool
request before execution so the final result contains only HOME and PATH.
E2E Testing
cd e2e
# Build binary
endly -t=build
# Run full regression suite (requires MySQL)
endly
# Run just the server regression
endly -t=test
Knowledge / RAG
# agent yaml
knowledge:
- url: knowledge/ # local files
- embedius:
config: ~/embedius/config.yaml
role: user
Resources Tools
The resources internal tool provides filesystem and MCP resource discovery:
resources:roots— discover configured rootsresources:list— list files under given locationsresources:match— semantic search via embedder
Configure in workspace config.yaml:
default:
resources:
locations:
- /path/to/docs
indexPath: "${runtimeRoot}/index/${user}"
snapshotPath: "${runtimeRoot}/snapshots"
Scheduler
Run scheduled agent tasks on cron, interval, or ad-hoc basis.
Serverless (no scheduler):
AGENTLY_SCHEDULER_API=false ./agently serve
Dedicated scheduler pod:
AGENTLY_SCHEDULER_RUNNER=true AGENTLY_SCHEDULER_API=false ./agently serve
Related Projects
- agently-core — Embeddable Go runtime (this project's backbone)
- mcp-sqlkit — MCP server for database operations
- forge — React UI framework for the web interface
- datly — Data access layer for persistence
License
Apache License 2.0 — see LICENSE and NOTICE.
This product includes software developed at Viant (http://viantinc.com/).
Documentation
¶
Index ¶
Constants ¶
This section is empty.
Variables ¶
var Version string
Functions ¶
func RunScheduler ¶ added in v0.3.4
func RunScheduler(options SchedulerRunOptions) error
func Serve ¶ added in v0.3.0
func Serve(options ServeOptions) error