Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@

## Checklist

- [ ] I have run `make lint` and `make test` locally
- [ ] I have run `make check`
- [ ] I have run `task examples:all`
- [ ] I have run `make tidy` if I added or removed dependencies
- [ ] Commit messages follow [conventional commits](https://www.conventionalcommits.org) (e.g. `feat:`, `fix:`, `docs:`)
- [ ] I have added/updated tests for my changes
Expand Down
6 changes: 1 addition & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# CI: lint, test, coverage, build
# - Push to branches (not main): full checks + coverage (no Codecov upload)
# - PR to main: same + Codecov upload
# - PR to main: full checks + Codecov upload
# - workflow_dispatch: same + Codecov upload (on-demand coverage report)
# - Push / merge to main: no workflow (already validated on the PR)
# (Secret scanning: run `make secrets-scan` locally.)
Expand All @@ -9,9 +8,6 @@ name: CI
on:
pull_request:
branches: [main]
push:
branches-ignore:
- main
workflow_dispatch:

jobs:
Expand Down
44 changes: 35 additions & 9 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,27 +56,43 @@ git checkout -b <branch-name>

Keep your branch short and descriptive. Sync with `main` before opening a PR: `git pull upstream main` (or rebase if you prefer). Push your branch to your fork and open a PR against `main`.

### 3. Run tests
### 3. Run checks before a PR

```bash
make test
make check
```

**CI runs automatically** on pull requests and on pushes to branches other than `main`. Pushes or merges to `main` do not trigger CI automatically; use **workflow_dispatch** in GitHub Actions when you need an on-demand run. Lint and test must pass before merge — fix any CI failures in your PR.
Runs `fmt-check`, spell check, `make lint`, `make test`, `make build`, and `make secrets-scan` — same core gates as CI (coverage is CI-only; use `make test-coverage` locally if you want a report).

Or run tests for a specific package:
Also run the full example suite on any code change to catch regressions unit tests may miss:

```bash
task examples:all
```

Requires Task, Docker, and LLM credentials — see [examples/README.md](examples/README.md).

**CI runs automatically** on pull requests to `main` (open a PR or push updates to an existing PR to re-run checks). Pushes or merges to `main` do not trigger CI; use **workflow_dispatch** in GitHub Actions for an on-demand run. Run `make check` locally before opening a PR; CI must pass on the PR before merge.

To run only tests (e.g. while iterating):

```bash
make test
```

Or a specific package:

```bash
go test ./pkg/agent/... -count=1 -v
```

### 4. Run linters
### 4. Run linters (included in `make check`)

```bash
make lint
```

This runs `go vet` and `golangci-lint`. All contributions must pass lint with zero errors.
This runs `gofmt -s` check, `misspell`, `go vet`, and `golangci-lint`. Use when debugging a lint failure without re-running the full `make check`.

**golangci-lint vs Go version:** If you see `the Go language version used to build golangci-lint is lower than the targeted Go version`, your `golangci-lint` binary is too old for this module (Go 1.26+ requires **golangci-lint v2**). Reinstall: `go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest`, ensure `$(go env GOPATH)/bin` is on `PATH` ahead of any older install, then run `golangci-lint version` — it should report **v2.x** and a Go build **≥ 1.26**.

Expand All @@ -87,7 +103,7 @@ make test-coverage
# Open coverage.html in a browser
```

### 6. Run examples (optional)
### 6. Run examples

Examples load **`examples/.env.defaults`** automatically. Set LLM credentials via environment or an optional override file:

Expand All @@ -98,7 +114,15 @@ export LLM_MODEL=your-model
# LLM_PROVIDER: openai, anthropic, or gemini. Or append the same keys to examples/.env
```

Then run any example:
Run the full example suite before a PR (local + temporal, with reports):

```bash
task examples:all
```

When you add a new example, register it in **`taskfiles/examples.yml`** if it can run non-interactively via Task (one-shot `go run`, no REPL, no split worker process). See existing lists (`EXAMPLES`, `EXAMPLES_WITH_PROMPTS`, `EXAMPLES_TEMPORAL`) and commented TODOs for patterns.

Or run a single example:

```bash
go run ./examples/simple_agent "Hello"
Expand Down Expand Up @@ -140,7 +164,9 @@ Using the SDK and ran into issues, unclear docs, or confusing behavior? **Raise
## What Contributors Must Follow

1. **Code quality**
- Run `make lint` and `make test` before submitting a PR. PRs must pass both.
- Run `make check` before submitting a PR (format, spell, lint, test, build, secrets scan). PRs must pass.
- Run `task examples:all` before submitting a PR to verify nothing in the example suite breaks (any code change — not only example edits). Requires Task, Docker, and LLM credentials — see [examples/README.md](examples/README.md).
- New examples that support batch runs must be added to **`taskfiles/examples.yml`** (see §6).
- Run `make tidy` before committing if you add or remove dependencies.

2. **Tests**
Expand Down
36 changes: 19 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ a, _ := agent.NewAgent(
)
defer a.Close()

result, err := a.Run(ctx, "Hello", "")
result, err := a.Run(ctx, "Hello", nil)
// result.Content, result.AgentName, result.Model
```

Expand All @@ -205,7 +205,7 @@ a, _ := agent.NewAgent(
)
defer a.Close()

result, err := a.Run(ctx, "Hello", "")
result, err := a.Run(ctx, "Hello", nil)
```

[examples/simple_agent](examples/simple_agent)
Expand Down Expand Up @@ -297,7 +297,7 @@ a, _ := agent.NewAgent(
)
defer a.Close()

eventCh, err := a.Stream(ctx, "What's 17 * 23?", "")
eventCh, err := a.Stream(ctx, "What's 17 * 23?", nil)
for ev := range eventCh {
if ev == nil {
continue
Expand Down Expand Up @@ -363,7 +363,7 @@ a, _ := agent.NewAgent(
)
defer a.Close()

result, _ := a.Run(ctx, "What's the weather in Tokyo?", "")
result, _ := a.Run(ctx, "What's the weather in Tokyo?", nil)
```

[examples/agent_with_tools](examples/agent_with_tools)
Expand Down Expand Up @@ -694,7 +694,7 @@ mainAgent, _ := agent.NewAgent(
)
defer mainAgent.Close()

result, _ := mainAgent.Run(ctx, "What is 144 divided by 12?", "")
result, _ := mainAgent.Run(ctx, "What is 144 divided by 12?", nil)
```

[examples/agent_with_subagents](examples/agent_with_subagents)
Expand Down Expand Up @@ -755,7 +755,7 @@ a, _ := agent.NewAgent(
}),
// ...
)
a.Run(ctx, prompt, "")
a.Run(ctx, prompt, nil)
```

**Stream** — approval and delegation requests are `**CUSTOM`** events (`[AgentEventTypeCustom](pkg/agent/event.go)`). Parse with `[ParseCustomEventApproval](pkg/agent/event.go)` / `[ParseCustomEventDelegation](pkg/agent/event.go)`, then call `[OnApproval](pkg/agent/approval.go)` with the token from the value field (see [examples/durable_agent/agent/main.go](examples/durable_agent/agent/main.go)):
Expand All @@ -780,7 +780,7 @@ for ev := range eventCh {
**RunAsync** — channel-based completion without streaming. Do not set `WithApprovalHandler` for this path (it is replaced for the duration of the run). Receive each pending approval on `approvalCh` and call `req.Respond` (same idea as `WithApprovalHandler`):

```go
resultCh, approvalCh, err := a.RunAsync(ctx, prompt, "")
resultCh, approvalCh, err := a.RunAsync(ctx, prompt, nil)
if err != nil { /* validation error before goroutine started */ }

go func() {
Expand Down Expand Up @@ -825,7 +825,7 @@ a, _ := agent.NewAgent(
agent.WithTimeout(5 * time.Minute),
// ...
)
result, err := a.Run(context.Background(), "Hello", "")
result, err := a.Run(context.Background(), "Hello", nil)
```

**Notes:**
Expand Down Expand Up @@ -933,7 +933,7 @@ a, _ := agent.NewAgent(
agent.WithLLMClient(...),
agent.DisableLocalWorker(),
)
result, _ := a.Run(ctx, "Hello", "")
result, _ := a.Run(ctx, "Hello", nil)
```

[examples/agent_with_worker](examples/agent_with_worker) · [examples/durable_agent](examples/durable_agent)
Expand All @@ -947,7 +947,7 @@ result, _ := a.Run(ctx, "Hello", "")

Pass `agent.WithConversation(conv)` to persist message history for multi-turn context. Use `agent.WithConversationSize(n)` to limit how many messages are fetched for LLM context (default 20).

**Conversation ID:** When the agent is configured with a conversation, pass the same `conversationID` to both `Run(ctx, prompt, conversationID)` and `Stream(ctx, prompt, conversationID)` for the same session—so history is shared across turns.
**Conversation ID:** When the agent is configured with a conversation, pass an `*agent.AgentRunOptions` with `ConversationOptions.ID` set to the same session ID on every call to `Run`, `RunAsync`, and `Stream`—so history is shared across turns.

Choose implementation by deployment:

Expand All @@ -973,7 +973,8 @@ a, _ := agent.NewAgent(
agent.WithConversation(conv),
agent.WithConversationSize(20), // optional; default 20
)
result, _ := a.Run(ctx, "Hello", "session-1")
opts := &agent.AgentRunOptions{ConversationOptions: &agent.ConversationOptions{ID: "session-1"}}
result, _ := a.Run(ctx, "Hello", opts)

// Worker process
convW, _ := redis.NewRedisConversation(redis.WithAddr("localhost:6379"))
Expand All @@ -994,7 +995,8 @@ a, _ := agent.NewAgent(
agent.DisableLocalWorker(),
agent.WithConversation(convA),
)
result, _ := a.Run(ctx, "Hello", "session-1")
opts := &agent.AgentRunOptions{ConversationOptions: &agent.ConversationOptions{ID: "session-1"}}
result, _ := a.Run(ctx, "Hello", opts)
```

**Lifecycle:** You own the conversation. Call `Clear` when ending a session or when you no longer need the history. The agent never calls `Clear`.
Expand All @@ -1016,9 +1018,9 @@ a, _ := agent.NewAgent(
)
defer a.Close()

convID := "session-1"
a.Run(ctx, "I'm Alice. Remember that.", convID)
a.Run(ctx, "What's my name?", convID) // agent uses history: "Alice"
opts := &agent.AgentRunOptions{ConversationOptions: &agent.ConversationOptions{ID: "session-1"}}
a.Run(ctx, "I'm Alice. Remember that.", opts)
a.Run(ctx, "What's my name?", opts) // agent uses history: "Alice"
```

[examples/agent_with_conversation](examples/agent_with_conversation)
Expand All @@ -1032,7 +1034,7 @@ Events like `RUN_STARTED`, `TEXT_MESSAGE_CONTENT`, `TOOL_CALL_START`, and `REASO
For a complete server + UI reference, see [examples/agent_with_agui](examples/agent_with_agui) (Go SSE server in `server/main.go`, Next.js + CopilotKit bridge in `ui/app/api/copilotkit/route.ts`).

```go
ch, err := a.Stream(ctx, prompt, conversationID)
ch, err := a.Stream(ctx, prompt, nil) // pass &agent.AgentRunOptions{ConversationOptions: &agent.ConversationOptions{ID: id}} when using conversation
if err != nil {
return err
}
Expand Down Expand Up @@ -1173,7 +1175,7 @@ A Temporal connection (`WithTemporalConfig` or `WithTemporalClient`) is **option
- **WithTemporalClient**: Pre-configured Temporal client. Use for TLS, API key auth, Temporal Cloud. Requires `WithTaskQueue`. Agent does not close the client.
- **WithTaskQueue**: Task queue name. Required when using `WithTemporalClient`. Ignored when using `WithTemporalConfig`.
- **WithResponseFormat**: LLM response format. Omit for text-only. Use `&interfaces.ResponseFormat{Type, Name, Schema}` for JSON with schema. See [Response format](#response-format).
- **WithConversation**: Message history store. Use `inmem` for single process; `redis` for remote workers. Pass same `conversationID` to `Run` and `Stream` for a session. See [Conversation](#conversation-message-history).
- **WithConversation**: Message history store. Use `inmem` for single process; `redis` for remote workers. Pass the conversation ID via `AgentRunOptions` to `Run`, `RunAsync`, and `Stream` to share history across turns. See [Conversation](#conversation-message-history).
- **WithConversationSize**: Max messages to fetch for LLM context (default 20). Only applies when `WithConversation` is set.
- **EnableRemoteWorkers**: Pass `EnableRemoteWorkers()` when using `DisableLocalWorker` with approval or streaming (starts the event worker/workflow path).
- **WithSubAgents**: Attach specialist agents the main agent can delegate to. Each needs its own task queue and worker. See [Sub-agents](#sub-agents).
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func runBenchmark(ctx context.Context, cfg *setup.Config, llm *setup.MockLLMClie

func executeRun(ctx context.Context, a *agent.Agent, rng *rand.Rand) runOutcome {
start := time.Now()
_, err := a.Run(ctx, setup.RandomUserPrompt(rng), "")
_, err := a.Run(ctx, setup.RandomUserPrompt(rng), nil)
return runOutcome{
latencyMs: float64(time.Since(start).Milliseconds()),
success: err == nil,
Expand Down
7 changes: 6 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,12 @@ func main() {
break
}

eventCh, err := a.Stream(context.Background(), line, convID)
opts := &agent.AgentRunOptions{
ConversationOptions: &agent.ConversationOptions{
ID: convID,
},
}
eventCh, err := a.Stream(context.Background(), line, opts)
if err != nil {
log.Printf("agent error: %v", err)
continue
Expand Down
2 changes: 1 addition & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ EOF

Override **`LLM_PROVIDER`** / **`LLM_MODEL`**, **`MCP_TRANSPORT=streamable_http`** + **`MCP_STREAMABLE_HTTP_URL`**, a remote **`A2A_URL`**, or retriever vars when not using the default local stack. Process environment (export / root **`Taskfile.yml`** `dotenv`) wins over both files. See [env vars](#env-vars) and **`examples/.env.defaults`**.

**Task** — not installed by default; install via **[Task installation](https://taskfile.dev/installation/)** (platform-specific). Not needed for **`go run ./<example>`** when the overview table has no infra. Compose infra also needs **Docker**. From **`examples/`**: **`task infra:status`**, **`infra:deps:up`** / **`down`**, **`infra:*:up`** / **`down`**. From **repo root**: **`task examples:local`**, **`task examples:temporal`**. **`task --dry`** only prints commands (no report file). To preview the report layout without running examples or infra, use **`task examples:local:plan`**, **`task examples:temporal:plan`**, or **`task examples:all:plan`**.
**Task** — not installed by default; install via **[Task installation](https://taskfile.dev/installation/)** (platform-specific). Not needed for **`go run ./<example>`** when the overview table has no infra. Compose infra also needs **Docker**. From **`examples/`**: **`task infra:status`**, **`infra:deps:up`** / **`down`**, **`infra:*:up`** / **`down`**. From **repo root**: **`task examples:local`**, **`task examples:temporal`**, **`task examples:all`**. Contributors: run **`task examples:all`** before any PR to catch regressions across local and Temporal runtimes. New examples that can run non-interactively (one-shot, no REPL, no separate worker) should be listed in **`taskfiles/examples.yml`** (`EXAMPLES`, `EXAMPLES_WITH_PROMPTS`, or `EXAMPLES_TEMPORAL` as appropriate). **`task --dry`** only prints commands (no report file). To preview the report layout without running examples or infra, use **`task examples:local:plan`**, **`task examples:temporal:plan`**, or **`task examples:all:plan`**.

## Run examples

Expand Down
2 changes: 1 addition & 1 deletion examples/agent_with_a2a_client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func main() {
}

fmt.Println("user:", prompt)
result, err := a.Run(context.Background(), prompt, "")
result, err := a.Run(context.Background(), prompt, nil)
if err != nil {
log.Printf("run failed: %v", err)
return
Expand Down
2 changes: 1 addition & 1 deletion examples/agent_with_a2a_config/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func main() {
}

fmt.Println("user:", prompt)
result, err := a.Run(context.Background(), prompt, "")
result, err := a.Run(context.Background(), prompt, nil)
if err != nil {
log.Printf("run failed: %v", err)
return
Expand Down
2 changes: 1 addition & 1 deletion examples/agent_with_agui/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func streamHandler(a *agent.Agent) http.HandlerFunc {
w.Header().Set("Connection", "keep-alive")

ctx := r.Context()
ch, err := a.Stream(ctx, prompt, "")
ch, err := a.Stream(ctx, prompt, nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
Expand Down
14 changes: 12 additions & 2 deletions examples/agent_with_conversation/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,12 @@ func main() {

func runSingleTurn(ctx context.Context, a *agent.Agent, prompt, convID string) {
fmt.Println("user:", prompt)
result, err := a.Run(ctx, prompt, convID)
opts := &agent.AgentRunOptions{
ConversationOptions: &agent.ConversationOptions{
ID: convID,
},
}
result, err := a.Run(ctx, prompt, opts)
if err != nil {
log.Printf("run failed: %v", err)
return
Expand All @@ -91,7 +96,12 @@ func runInteractive(ctx context.Context, a *agent.Agent, convID string) {
if prompt == "exit" || prompt == "quit" || prompt == "bye" {
break
}
result, err := a.Run(ctx, prompt, convID)
opts := &agent.AgentRunOptions{
ConversationOptions: &agent.ConversationOptions{
ID: convID,
},
}
result, err := a.Run(ctx, prompt, opts)
if err != nil {
log.Printf("run failed: %v", err)
continue
Expand Down
2 changes: 1 addition & 1 deletion examples/agent_with_json_response/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func main() {
}

fmt.Println("user:", prompt)
result, err := a.Run(context.Background(), prompt, "")
result, err := a.Run(context.Background(), prompt, nil)
if err != nil {
log.Fatalf("run failed: %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion examples/agent_with_mcp_client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func main() {
}

fmt.Println("user:", prompt)
result, err := a.Run(context.Background(), prompt, "")
result, err := a.Run(context.Background(), prompt, nil)
if err != nil {
log.Printf("run failed: %v", err)
return
Expand Down
2 changes: 1 addition & 1 deletion examples/agent_with_mcp_config/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func main() {
}

fmt.Println("user:", prompt)
result, err := a.Run(context.Background(), prompt, "")
result, err := a.Run(context.Background(), prompt, nil)
if err != nil {
log.Printf("run failed: %v", err)
return
Expand Down
2 changes: 1 addition & 1 deletion examples/agent_with_observability/config/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func main() {

prompt := setup.UserPrompt()
fmt.Printf("entry=config (WithObservabilityConfig: OTLP traces, metrics, logs)\nuser: %s\n", prompt)
result, err := a.Run(context.Background(), prompt, "")
result, err := a.Run(context.Background(), prompt, nil)
if err != nil {
log.Printf("run failed: %v", err)
return
Expand Down
2 changes: 1 addition & 1 deletion examples/agent_with_observability/objects/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func main() {

prompt := setup.UserPrompt()
fmt.Printf("entry=objects (WithTracer / WithMetrics / WithLogs; default logger bridged to OTLP)\nuser: %s\n", prompt)
result, err := a.Run(context.Background(), prompt, "")
result, err := a.Run(context.Background(), prompt, nil)
if err != nil {
log.Printf("run failed: %v", err)
return
Expand Down
2 changes: 1 addition & 1 deletion examples/agent_with_reasoning/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func main() {
fmt.Println("user:", prompt)
fmt.Println("--- stream (REASONING_MESSAGE_CONTENT may appear before assistant text) ---")

eventCh, err := a.Stream(context.Background(), prompt, "")
eventCh, err := a.Stream(context.Background(), prompt, nil)
if err != nil {
log.Fatalf("Stream: %v", err)
}
Expand Down
Loading
Loading