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
17 changes: 4 additions & 13 deletions .codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,10 @@ coverage:
ignore:
- "**/*_test.go"
- "**/testdata/**"
- "e2e/behaviour/suite_test.go"
- "e2e/behaviour/features/**"
- "e2e/behaviour/fixtures/**"
# Integration-only; covered by behaviour/e2e CI jobs (not unit coverage upload).
- "pkg/behaviourtest/steps/dispatch.go"
- "pkg/behaviourtest/steps/dispatch_count.go"
- "pkg/behaviourtest/steps/dummy_agent.go"
- "pkg/behaviourtest/steps/fork.go"
- "pkg/behaviourtest/steps/jirapoll.go"
- "pkg/behaviourtest/steps/registry.go"
- "pkg/behaviourtest/steps/triage.go"
- "pkg/behaviourtest/suite/init.go"
- "pkg/behaviourtest/drivers/install/perrepo_github.go"
# Behaviour-test harness and scenarios: integration-only, exercised by the
# behaviour/e2e CI jobs against real repos (not by the unit coverage upload).
- "e2e/behaviour/**"
Comment thread
waynesun09 marked this conversation as resolved.
Comment thread
waynesun09 marked this conversation as resolved.
Comment thread
waynesun09 marked this conversation as resolved.
- "pkg/behaviourtest/**"
- "pkg/e2etest/cleanup.go"
- "pkg/e2etest/build.go"
- "docs/**"
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/reusable-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -457,9 +457,11 @@ jobs:
echo "::error::config.yaml: disabled agent entry without a name field — add 'name:' to identify which agent to disable"
exit 1
fi
BAD_SOURCE=$(yq '.agents[] | select(type == "!!map" and .enabled != false and (.source == null or .source == "")) | line' .fullsend/config.yaml 2>/dev/null || echo "")
# An enabled entry may omit source when it only tunes a built-in
# agent by name (runtime/model/effort; ADR 0091).
BAD_SOURCE=$(yq '.agents[] | select(type == "!!map" and .enabled != false and (.source == null or .source == "") and (.runtime == null and .model == null and .effort == null)) | line' .fullsend/config.yaml 2>/dev/null || echo "")
if [[ -n "$BAD_SOURCE" ]]; then
echo "::error::config.yaml: enabled agent entry without a source field"
echo "::error::config.yaml: enabled agent entry without a source field (an entry with only a name must set runtime, model or effort to tune a built-in agent)"
exit 1
fi
DUPES_ENABLED=$(yq '.agents[] | select(type == "!!map" and .enabled != false) | (.name // "") | select(. != "") | downcase' .fullsend/config.yaml 2>/dev/null | sort | uniq -d)
Expand Down
11 changes: 10 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,17 @@ test: lint-all go-test script-test lint-eval-cases
e2e-test:
go test -tags e2e -v -count=1 -timeout 30m ./e2e/admin/

# Capabilities the runner declares for @requires:capability:<name> scenarios.
# Declared here rather than in the e2e workflow so a PR that adds a gated
# scenario exercises it on its own CI run (E2E Tests runs on
# pull_request_target, whose workflow file comes from main). runtime-pi:
# fullsend-sandbox:latest ships pi since v0.37.0; each pi scenario costs one
# small haiku run on the pool repo's Vertex WIF. Override to skip them:
# BEHAVIOUR_CAPABILITIES= make behaviour-test
BEHAVIOUR_CAPABILITIES ?= runtime-pi

behaviour-test:
go test -tags behaviour -race -v -count=1 -timeout 45m ./e2e/behaviour/
BEHAVIOUR_CAPABILITIES="$(BEHAVIOUR_CAPABILITIES)" go test -tags behaviour -race -v -count=1 -timeout 45m ./e2e/behaviour/

# Functional agent evals — run agents against ephemeral GitHub repos and judge results.
# Required env: EVAL_ORG (GitHub org for ephemeral repos), plus GCP creds for Vertex AI.
Expand Down
190 changes: 190 additions & 0 deletions docs/ADRs/0091-per-agent-runtime-model-effort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
---
title: "91. Per-agent runtime, model and effort on agents: entries"
status: Accepted
relates_to:
- agent-architecture
topics:
- config
- harness
- runtime
---

# 91. Per-agent runtime, model and effort on agents: entries

Date: 2026-08-25

## Status

Accepted

## Context

`.fullsend/config.yaml` could not express per-agent `model`, `runtime`, or
`effort`. A repo that needed different agents on different models or
runtimes had to set GitHub repository variables (`TRIAGE_FULLSEND_MODEL`,
`CODE_FULLSEND_RUNTIME`, ...), which live outside the repository: no review,
no git history, no diff, and nothing in the repo records the intent.

The per-role `model:` field in the harness is per-agent by construction,
but repos consuming remote harnesses cannot reach it. The `validModelName`
regex (`^[a-zA-Z0-9_.@-]+$`) also prevented provider-qualified model
identifiers (e.g. `xai-vertex/xai/grok-4.6`) in harness files, forcing
repos on pi to use repository variables as the only path.

`runtime:` was a single repo-wide key (ADR 0033), yet the case that
motivates this record is precisely a repo whose agents run on different
runtimes: fullsend-ai/pi-xai-vertex runs triage and review on pi (Grok)
and code, fix and retro on Claude Code.

Related issues: #6529 (per-role model + effort, scoped out runtime),
#6570 (validModelName `/` restriction), #6577 (alias resolution bug,
independent). #6529 and #6570 are folded into this record deliberately:
a per-agent model field that cannot name `provider/id` would not serve
the pi case that motivates it, so the schema and the validation rule are
one decision, not two.

## Options

**A separate `role_overrides:` map** (the first cut of the implementing
PR, #6583) — keyed by agent name, with `runtime`/`model`/`effort` per key.
Rejected before merge: `config.yaml` already has a per-agent place — the
`agents:` list (ADR 0058) — keyed by the same names with the same layered
merge, so a second per-agent section would have split "what runs" from
"how it runs" and carried a misleading name (it was keyed by agent name,
not harness role, because `code` and `fix` share `role: coder`).

**Fields on the `agents:` entry** — chosen; see Decision.

## Decision

### 1. `runtime`, `model`, `effort` on `agents:` entries

An `agents:` entry may set the three fields. A built-in agent is tuned
with a **name-only entry** (no `source:`); a custom agent carries them on
its `source:` entry:

```yaml
runtime: pi # repo default for agents that set none
agents:
- name: triage
model: xai-vertex/xai/grok-4.6
- name: code
runtime: claude
model: sonnet
effort: high
- source: https://raw.githubusercontent.com/acme/agents/<sha>/harness/lint.yaml#sha256=<hash>
model: haiku # custom agent "lint" (name derived from the file, ADR 0058)
```

Names are **agent names** as passed to `fullsend run <agent>` (triage,
code, review, fix, retro, prioritize) or a custom entry's name, matched
case-insensitively. They are NOT harness `role:` values — `code` and
`fix` both carry `role: coder`, while the existing role-prefixed
repository variables (`CODE_FULLSEND_MODEL`, `FIX_FULLSEND_MODEL`) already
key on agent name.

An enabled entry without `source:` must name a built-in agent and set at
least one field; anything else is rejected (`coder` gets a "did you mean
`code`?" hint). Such an *override-only* entry registers no harness: the
built-in keeps resolving through the agents-repo fallback, and it is not
enumerated as a custom harness, locked, or listed as one.

This narrows ADR 0045's "config.yaml does not gain deep-merge
capabilities or per-agent override entries": that decision kept an
agent's *definition* out of `config.yaml`, and it still holds — these
fields tune three operational knobs of an already resolved harness; they
do not define or compose one. Anything beyond runtime/model/effort still
belongs in a harness (`base` composition).

`fullsend agent set <name> [--runtime] [--model] [--effort]` writes the
entry so the file need not be edited by hand.

### 2. Precedence

Config-layer addition, slotting in below per-run overrides:

```
--runtime/--model/--effort flag
> FULLSEND_* env (including role-prefixed repository variables)
> the agent's agents: entry
> repo-wide runtime: / harness model: effort:
> default
```

The repo-wide `runtime:` key is kept as the default for agents that set
none — deliberately not removed: existing configs, `fullsend github setup
--runtime` and the behaviour-test installs depend on it, and a default is
still the right shape for a repo where every agent runs the same way.
Per-agent is now the primary place to select a runtime; the docs steer
there.

Entries merge per field across the layered config (ADR 0069): the
overlay's non-empty value wins, an empty value inherits the base's; there
is no tombstone to unset a base value short of restating the entry.

Plan output, stderr `runtime: selected ...` lines, and `metrics.json`
(`runtime_source`, `override_source`) name the source as
`<config path> agents.<name>` (the effective config file).

`fullsend run` does not call `Validate()` on the config it loads, so it
validates the effective `agents:` list itself on every run — names,
runtime against `ValidRuntimes()` (a stub runtime cannot be activated
through an entry any more than through `runtime:`), effort against the
shared levels, model against the shared model-reference syntax — in the
overlay and in `config.base.yaml` alike, and fails the run with an error
naming the file and entry rather than silently running without the
settings. Runtime selection and the model/effort application read one
loaded config, so an entry's three fields always agree on their source.

The effective entries, with these fields, are exposed to overlay CEL
expressions as `config.agents` (ADR 0088).

### 3. Segment-based model validation

A shared `ValidModelRef` regex (`^[a-zA-Z0-9_.@-]+(/[a-zA-Z0-9_.@-]+)*$`)
replaces the harness-local `validModelName`. This is a superset of the
previous rule: existing single-segment model names continue to validate,
and `provider/id` forms are now accepted in both harness `model:` fields
and `agents:` entry `model:` values. Malformed forms (`/leading`,
`trailing/`, `a//b`) are rejected.

The effort level list moves to `config.ValidEffortLevels()` for the same
reason: `harness` imports `config`, so the shared lists live in `config`
and both validators read one source of truth.

### 4. Deferred: `models.aliases`

Provider-qualified per-agent models remove most of the need for the
`models.aliases` map proposed in #6529. Alias resolution is tracked
separately in #6577. This ADR does not introduce an alias system.

### 5. Scope: per-repo first

The fields are read wherever `agents:` entries are (org-mode configs
included, since the list is shared), but the CLI (`agent set`), the setup
PR text and the docs target per-repo installs; per-org installation mode
is deprecated (ADR 0044).

## Consequences

- Repos express per-agent runtime, model and effort in one reviewable,
version-controlled list that also says which harness runs — one place
per agent.
- Repository variables remain valid for one-off experiments and for
repos that prefer out-of-band configuration.
- The harness `model:` field now accepts `/` in model identifiers,
unblocking provider-qualified models in harness files.
- Because `config.yaml` now carries durable per-agent intent, a
`fullsend github setup` re-run keeps an existing per-repo `config.yaml`
untouched unless a flag targets a config key, and then changes only
that key; managed workflow files still refresh. The converge
full-rescaffold repair (missing workflow) and the GitLab setup path keep
regenerating the file as before.
- The reusable workflow's `config.yaml` guard allows an enabled entry
without `source:` when it sets a field. A repo must bump its pin to a
version that carries this change before adding such an entry: an older
pinned workflow rejects it for every agent, whereas a separate unknown
key would have been ignored. The docs say so.
- `review` and `retro` can be put on another runtime through their
entries but are documented to stay on Claude Code today; this is not
enforced in validation.
4 changes: 2 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ flowchart TB

**Decided (implementation):**

- The `fullsend run` runner delegates in-sandbox agent execution to a `runtime.Runtime` interface; production orgs default to Claude Code, with [pi](https://github.com/earendil-works/pi) available as an opt-in second runtime (`runtime: pi`, Claude-on-Vertex through the same WIF credential path). Runtime selection is configured in `defaults.runtime` on the org `config.yaml` and resolved via `runtime.ResolveFromConfig()`. A **dummy** runtime executes scripted operations in the real OpenShell sandbox for behaviour tests (inference removed). Bootstrap uses a portable `BootstrapInput` interface with optional extensions such as `SandboxHooksBootstrap` for the runtime-neutral sandbox tool hooks ([ADR 0090](ADRs/0090-runtime-neutral-sandbox-hooks-contract.md)); runtimes declare further capabilities through small optional interfaces (`DebugLogNamer`, `ContextBridger`) rather than `Name()` checks in the runner. Transcript and debug artifact handling use a separate `TranscriptHandler` interface. See [runtimes.md](runtimes.md) for the per-runtime security feature matrix required when adding a new backend.
- The `fullsend run` runner delegates in-sandbox agent execution to a `runtime.Runtime` interface; production orgs default to Claude Code, with [pi](https://github.com/earendil-works/pi) available as an opt-in second runtime (`runtime: pi`, Claude-on-Vertex through the same WIF credential path). Runtime selection is configured per repo with `runtime:` in `.fullsend/config.yaml` (per-agent `runtime`/`model`/`effort` on the agent's `agents:` entry sit above it and below the `--runtime`/`--model`/`--effort` flags and `FULLSEND_*` variables, [ADR 0091](ADRs/0091-per-agent-runtime-model-effort.md)) and resolved via `runtime.ResolveForAgent()`; org-mode configs use `defaults.runtime` and `runtime.ResolveFromConfig()`. A **dummy** runtime executes scripted operations in the real OpenShell sandbox for behaviour tests (inference removed). Bootstrap uses a portable `BootstrapInput` interface with optional extensions such as `SandboxHooksBootstrap` for the runtime-neutral sandbox tool hooks ([ADR 0090](ADRs/0090-runtime-neutral-sandbox-hooks-contract.md)); runtimes declare further capabilities through small optional interfaces (`DebugLogNamer`, `ContextBridger`) rather than `Name()` checks in the runner. Transcript and debug artifact handling use a separate `TranscriptHandler` interface. See [runtimes.md](runtimes.md) for the per-runtime security feature matrix required when adding a new backend.

### Behaviour testing

Expand Down Expand Up @@ -383,7 +383,7 @@ the inheritance model: fullsend defaults, then repo baseline (`config.base.yaml`
- Config-level agent registration: an `agents` list in both `OrgConfig` and `PerRepoConfig` declares agent harness sources as pinned URLs or local paths, replacing compiled-in agent discovery ([ADR 0058](ADRs/0058-agent-registration.md)).
- Runtime resolution: `fullsend run <name>` resolves agents in two tiers: (1) config entries from `OrgConfig.Agents` (highest priority), (2) runtime fallback to the `fullsend-ai/agents` repository for known first-party agents not in config. The agents-repo fallback is a transitional mechanism for the agent extraction; it will be removed once all users have migrated to config-driven registration (ADR 0058 Phase 5).
- Config lookup: config entries are looked up directly via `findConfigAgentEntry`; the agents-repo fallback operates independently when the agent is not found in config. Builds on [ADR 0045](ADRs/0045-forge-portable-harness-schema.md) harness identity model.
- CLI management: `fullsend agent add|list|update|remove` manages config entries and auto-pins URLs to a commit SHA with an integrity hash.
- CLI management: `fullsend agent add|list|set|update|remove` manages config entries and auto-pins URLs to a commit SHA with an integrity hash.

**Open questions:**

Expand Down
2 changes: 1 addition & 1 deletion docs/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Download the latest binary from [GitHub Releases](https://github.com/fullsend-ai

| Command group | Description |
|--------------|-------------|
| [`fullsend agent`](agent.md) | Manage agent registrations — add, list, update, remove |
| [`fullsend agent`](agent.md) | Manage agent registrations — add, list, set, update, remove |
| [`fullsend github`](github.md) | Configure GitHub orgs and repos — setup, enrollment, day-2 operations |
| [`fullsend inference`](inference.md) | Manage GCP Workload Identity Federation for Agent Platform access |
| [`fullsend mint`](mint.md) | Deploy and manage the OIDC token mint service |
Expand Down
28 changes: 27 additions & 1 deletion docs/cli/agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ sidebar_label: fullsend agent

# fullsend agent

Manage agent registrations in fullsend config. Add, list, update, and remove agents.
Manage agent registrations in fullsend config. Add, list, set (runtime, model, effort), update, and remove agents.

`agent add` and `agent update` fetch remote content and resolve GitHub URLs. Authentication is via `gh` CLI or `GH_TOKEN` environment variable.

Expand All @@ -15,6 +15,7 @@ Manage agent registrations in fullsend config. Add, list, update, and remove age
| `fullsend agent add <url-or-path>` | Register an agent in config |
| `fullsend agent list` | List registered agents |
| `fullsend agent update <name> [sha]` | Update a URL agent to a new commit SHA |
| `fullsend agent set <name>` | Set an agent's runtime, model or effort |
| `fullsend agent remove <name>` | Remove an agent from config |

## `agent add`
Expand Down Expand Up @@ -75,6 +76,31 @@ fullsend agent update triage a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2 --fullsend

Only URL agents can be updated — local path agents have nothing to pin. Non-GitHub URL agents require an explicit SHA argument. The integrity hash is recomputed by fetching the content at the new SHA.

## `agent set`

Sets `runtime`, `model` and/or `effort` for one agent in `.fullsend/config.yaml` (per-repo
configs). A built-in agent (`triage`, `code`, `review`, `fix`, `retro`, `prioritize`) without an
entry gets a name-only entry; a custom agent's settings land on its `source:` entry (or, for an
agent registered in `config.base.yaml`, on a name-only overlay entry that merges onto it). Only the
flags given change; pass an empty value (`--model ""`) to clear a setting. The result is validated
before it is written.

```bash
fullsend agent set code --fullsend-dir .fullsend --runtime claude --model sonnet --effort high
fullsend agent set triage --fullsend-dir .fullsend --model xai-vertex/xai/grok-4.6
```

### Flags

| Flag | Description |
|------|-------------|
| `--fullsend-dir` | Path to the `.fullsend` configuration directory (required) |
| `--runtime` | Agent runtime for this agent (`claude` or `pi`) |
| `--model` | Model for this agent — an alias, a model id, or `provider/id` on pi |
| `--effort` | Effort level for this agent (`low`, `medium`, `high`, `xhigh`, `max`) |

See [Runtimes — per-agent settings](../runtimes.md#per-agent-runtime-model-and-effort-in-configyaml) for precedence.

## `agent remove`

Remove an agent from config. If the removed agent was the last one using a given `allowed_remote_resources` prefix, that prefix is also cleaned up.
Expand Down
9 changes: 9 additions & 0 deletions docs/cli/github.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ fullsend github setup <owner/repo> \
--inference-wif-provider "<WIF_PROVIDER>"
```

**Re-running per-repo setup** (for example after a fullsend upgrade) refreshes the managed
workflow files but never rewrites an existing `.fullsend/config.yaml` on its own: `agents:` entries and
their per-agent settings, allowlists and hand-written comments stay as they are, the runtime prompt is skipped,
and the setup PR reports the runtime the file already selects. Passing a flag that targets a
config key — `--runtime`, `--agents`, `--mint-url`, `--inference-*` — changes that key on the
existing file and keeps the rest (the file is re-serialized, so comments are not preserved in
that case). `--config` rewrites `config.base.yaml` and keeps the existing overlay. A
`config.yaml` that no longer parses fails the re-run rather than being regenerated.

### Flags

| Flag | Default | Description |
Expand Down
Loading
Loading