Skip to content

Commit 1993d8e

Browse files
committed
feat(cli): run the agents on a chosen provider's models
`aidd setup --ide … --provider openai|anthropic|mistral` gives each framework agent the model its role's tier maps to in agent-models.json (light for explorer, executor and validator, standard for the reviewer), per IDE and for the whole machine; `--provider none` gives the agents back the session's model. Every install build applies the recorded provider, the provider is part of what keeps a built tree fresh, and setting it re-installs the plugins already installed, the machine-wide ones too with --global. The session's own model stays the one the IDE picks.
1 parent 2fed9d3 commit 1993d8e

27 files changed

Lines changed: 915 additions & 37 deletions

File tree

‎aidd_docs/memory/cli.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ The `aidd` binary, built from `cli/` and published to npm as `@ai-driven-dev/cli
66
77
## Commands
88

9+
- `setup --provider` chooses the provider each IDE's agents run on, machine-wide: [agent guide](../../docs/SUBAGENTS.md#-models).
910
- `telemetry` counts the tokens of Claude Code and Codex sessions locally: [usage contract](../product/usage-contract.md).
1011
- No list here. The surface moves; `aidd --help` is the only reading that stays true.
1112

‎aidd_docs/tasks/2026_10/2026_10_14_sub_agents/verification.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ Behavior and sources are in [the agent guide](../../../../docs/SUBAGENTS.md).
66

77
- **Agents:** explorer, executor, validator and reviewer ship in `plugins/aidd-orchestrator/agents/`, installed with the plugin. No tool list is declared: every host gives an agent all tools by default.
88
- **Translation:** each host receives only the keys its documentation lists: name or file path as the ID, description, model, and Claude's `effort` under the host's own key. Each host has one agent frontmatter function, shared by the plugin install and its built distributions.
9-
- **Out of scope:** per-provider models and a model for the main session.
9+
- **Models:** `aidd setup --provider` records a provider per IDE; every build that installs agents gives each role its tier's model from `agent-models.json`.
10+
- **Out of scope:** plugin installs by `aidd setup --global`, so `make reload` applies no model.
1011

1112
## Checks
1213

1314
- **Claude Code:** the four agents load and run from a project-scoped plugin install.
1415
- **OpenCode:** the four agents run from the flat distribution through `opencode run --standalone --agent <id>` with an OpenAI model, each answering after a `read` call.
1516
- **Codex:** the four agents load from the flat distribution in a trusted project; `codex exec` spawned each, and its session records the agent's role and instructions.
17+
- **Provider models:** a built OpenCode agent with `openai` loaded as `{id: gpt-6-luna, providerID: openai, variant: medium}` (`opencode debug agents`), and a Codex session spawned the explorer on `gpt-6-luna` while its parent ran `gpt-6.1-sol`.
18+
- **Setter:** in a scratch project, `aidd setup --provider mistral` ran the OpenCode reviewer on `mistral-medium-latest`, and `--provider anthropic` the Claude reviewer on `claude-sonnet-5-5`; `--provider none` removed every model.
1619
- **Every host:** `cli/tests/e2e/agent-install.e2e.test.ts` builds the repository flat and checks each shipped agent's path, frontmatter and body against the host's documented format.
1720
- **Copilot, Cursor, Kilo:** no host binary in the test environment, so loading by the host is unverified. The Copilot plugin lists agent files where its documentation describes agent directories.

‎cli/README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,13 @@ Check: `aidd --version`.
3333

3434
`setup` writes the manifest, the default marketplace, the tool configs and the plugins.
3535
`--global` installs for the machine instead, and writes nothing under the project.
36+
`--provider` runs the framework's agents on that provider's models, and `--provider none` undoes it.
3637

3738
| Command | Lands in |
3839
| --- | --- |
3940
| `aidd setup --ide claude` | `.claude/` and `.aidd/` in this project |
4041
| `aidd setup --global --ide opencode --plugins all` | `~/.config/opencode/` and `~/.config/aidd/`, nothing in this project |
42+
| `aidd setup --ide opencode --provider mistral` | `.opencode/agents/` with Mistral models; the provider in `~/.config/aidd/agent-models.json`, for every project |
4143

4244
## Commands
4345

@@ -138,6 +140,8 @@ aidd sync --global # rebuild the copied files and refresh the hosts from the
138140
aidd clean --global # remove what --global installed and purge the shared source
139141
```
140142

143+
With `--provider`, the copied agents and enabled plugins carry that provider's models.
144+
141145
## Authentication
142146

143147
Needed for a private marketplace only.

‎cli/aidd_docs/memory/codebase-map.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ src/
1313
│ │ └── infrastructure/
1414
│ ├── framework/ # the installation record and everything done to a project
1515
│ │ ├── application/
16+
│ │ │ ├── agents/ # which model each host's agents run on
1617
│ │ │ ├── clean/
1718
│ │ │ ├── doctor/
1819
│ │ │ ├── flows/

‎cli/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"node": ">=22.12"
3737
},
3838
"packageManager": "pnpm@12.16.1",
39-
"bundleBudgetKB": 734,
39+
"bundleBudgetKB": 741,
4040
"scripts": {
4141
"build": "tsup && node scripts/check-bundle-size.mjs",
4242
"build:check-size": "node scripts/check-bundle-size.mjs",

‎cli/scripts/check-bundle-size.mjs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ const pkg = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8"));
5151
// 0.2% measured headroom.
5252
// 734 KB: 718.8 KB, the machine-wide install of `setup --global` (+8.0 KB over the 710.8 KB
5353
// above); 2.1% measured headroom.
54+
// 741 KB: 725.6 KB, the agents' models: a provider per IDE that every install build applies
55+
// (+6.8 KB over the 718.8 KB above); 2.1% measured headroom.
5456
const budgetKB = pkg.bundleBudgetKB ?? 500;
5557
const budgetBytes = budgetKB * 1024;
5658

‎cli/scripts/smoke-tools.sh‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ project_tree_hash() {
6464
' )
6565
}
6666

67-
PARENTS=" plugin marketplace auth framework telemetry "
67+
PARENTS=" plugin marketplace auth framework telemetry agents "
6868
# A parent one level deeper than PARENTS, so a covered key needs three words there, not two.
6969
GRANDPARENTS=" "
7070
derive_key() {
@@ -225,6 +225,13 @@ run "telemetry forget (preview)" 0 "Nothing was removed" "$P_TEL" -- telemetry f
225225
run "telemetry forget --yes" 0 "Removed" "$P_TEL" -- telemetry forget --yes
226226
run "telemetry off (after forget)" 0 "Measurement was not on" "$P_TEL" -- telemetry off
227227

228+
# The provider lands in the sandboxed $AIDD_USER_CONFIG_DIR, never the real profile's.
229+
section "setup --provider (sandboxed profile)"
230+
P_AGENTS=$(new_project)
231+
SETUP_OC=(setup --source local --path "$FRAMEWORK_FIXTURE" --ide opencode --plugins none --yes)
232+
run "setup --provider" 0 "run on mistral" "$P_AGENTS" -- "${SETUP_OC[@]}" --provider mistral
233+
run "setup --provider none" 0 "session's model" "$P_AGENTS" -- "${SETUP_OC[@]}" --provider none
234+
228235
section "update --check"
229236
out=$(cd "$ROOT" && node "$CLI" update --check 2>&1); rc=$?
230237
if [[ "$rc" -eq 0 || "$rc" -eq 1 ]]; then mark_covered "update"; ok "update --check (exit $rc)"; else bad "update crashed (exit $rc)" "$out"; fi
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import type { MarketplaceScope } from "../../../../kernel/scope.js";
2+
import {
3+
type AgentHost,
4+
type AgentProvider,
5+
supportedProvider,
6+
} from "../../../tools/domain/formats/agent-models.js";
7+
import type { AgentModelStore } from "../../domain/ports/agent-model-store.js";
8+
import type { ManifestRepository } from "../../domain/ports/manifest-repository.js";
9+
import type { MarketplaceSyncSettings } from "../flows/marketplace-sync-settings-use-case.js";
10+
import type { PluginUpdateUseCase } from "../plugin/plugin-update-use-case.js";
11+
12+
export interface SetAgentModelOptions {
13+
readonly projectRoot: string;
14+
readonly hosts: readonly AgentHost[];
15+
/** Absent, the hosts' agents go back to the session's model. */
16+
readonly provider?: string;
17+
/** `"user"` also refreshes what `setup --global` installed for the machine. */
18+
readonly scope: MarketplaceScope;
19+
}
20+
21+
export interface SetAgentModelResult {
22+
/** Plugins re-installed with the new models. */
23+
readonly reinstalled: readonly string[];
24+
}
25+
26+
/**
27+
* The setter for agent models. Records the provider each host's agents run on, which every
28+
* build of that host then applies, and re-installs the plugins already installed so they
29+
* carry it. The session's own model stays the IDE's choice.
30+
*/
31+
export class SetAgentModelUseCase {
32+
constructor(
33+
private readonly store: AgentModelStore,
34+
private readonly manifestRepo: ManifestRepository,
35+
private readonly userManifestRepo: ManifestRepository,
36+
private readonly pluginUpdate: Pick<PluginUpdateUseCase, "execute">,
37+
private readonly userPluginUpdate: Pick<PluginUpdateUseCase, "execute">,
38+
private readonly activation: MarketplaceSyncSettings
39+
) {}
40+
41+
async execute(options: SetAgentModelOptions): Promise<SetAgentModelResult> {
42+
const chosen = new Map<AgentHost, AgentProvider>();
43+
if (options.provider !== undefined) {
44+
for (const host of options.hosts) chosen.set(host, supportedProvider(host, options.provider));
45+
}
46+
const providers = await this.store.loadProviders();
47+
for (const host of options.hosts) {
48+
const provider = chosen.get(host);
49+
if (provider === undefined) delete providers[host];
50+
else providers[host] = provider;
51+
}
52+
await this.store.saveProviders(providers);
53+
return { reinstalled: await this.reinstall(options) };
54+
}
55+
56+
/** Files a plugin wrote are rewritten from the rebuilt tree, and a host that installs plugins
57+
* itself is driven again, which hands it that tree. The provider is machine-wide, so this
58+
* project is always refreshed, and a plugin installed in the user's home, as Cursor's always
59+
* is, too; `--global` adds the machine-scope registrations. */
60+
private async reinstall(options: SetAgentModelOptions): Promise<readonly string[]> {
61+
const rewritten = new Set<string>();
62+
const userManifest = await this.userManifestRepo.load();
63+
const userPlugins = options.hosts.filter((host) =>
64+
userManifest?.getPlugins(host).some((plugin) => plugin.scope === "user")
65+
);
66+
if (userPlugins.length > 0) {
67+
const names = await this.userPluginUpdate.execute({
68+
toolIds: userPlugins,
69+
projectRoot: options.projectRoot,
70+
scope: "user",
71+
rebuild: true,
72+
});
73+
for (const name of names) rewritten.add(name);
74+
}
75+
const manifest = await this.manifestRepo.load();
76+
const installed = options.hosts.filter((host) => manifest?.hasTool(host) === true);
77+
const projectPlugins = installed.filter((host) =>
78+
manifest?.getPlugins(host).some((plugin) => plugin.scope !== "user")
79+
);
80+
if (projectPlugins.length > 0) {
81+
const names = await this.pluginUpdate.execute({
82+
toolIds: projectPlugins,
83+
projectRoot: options.projectRoot,
84+
rebuild: true,
85+
});
86+
for (const name of names) rewritten.add(name);
87+
}
88+
if (installed.length > 0) await this.activate(options.projectRoot, installed);
89+
if (options.scope === "user") {
90+
const machine = options.hosts.filter((host) => userManifest?.hasTool(host) === true);
91+
if (machine.length > 0) await this.activate(options.projectRoot, machine, true);
92+
}
93+
return [...rewritten];
94+
}
95+
96+
private async activate(projectRoot: string, toolIds: AgentHost[], user = false): Promise<void> {
97+
const { errors } = await this.activation.execute({
98+
projectRoot,
99+
toolIds,
100+
...(user ? { scope: "user" as const, manifestRepo: this.userManifestRepo } : {}),
101+
});
102+
if (errors.length > 0) throw new Error(errors.map((error) => error.message).join("\n"));
103+
}
104+
}

‎cli/src/contexts/framework/application/plugin/plugin-update-use-case.ts‎

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ export interface PluginUpdateOptions {
3131
toolIds: AiToolId[] | "all";
3232
projectRoot: string;
3333
scope?: "project" | "user";
34-
/** Re-materialize at the same version: the source changed, not the plugin's version. Read by
35-
* the machine-scope update, which `sync --global` drives. */
34+
/** Re-materialize at the same version: the build changed, not the plugin. */
3635
rebuild?: boolean;
3736
}
3837

@@ -60,7 +59,8 @@ export class PluginUpdateUseCase {
6059
pluginNames,
6160
projectRoot,
6261
cacheDir,
63-
manifest
62+
manifest,
63+
options.rebuild === true
6464
);
6565
updated.push(...names);
6666
}
@@ -73,15 +73,23 @@ export class PluginUpdateUseCase {
7373
pluginNames: string[] | undefined,
7474
projectRoot: string,
7575
cacheDir: string,
76-
manifest: Manifest
76+
manifest: Manifest,
77+
rebuild: boolean
7778
): Promise<string[]> {
7879
const plugins = manifest.getPlugins(toolId);
7980
const targets = pluginNames
8081
? plugins.filter((p) => pluginNames.includes(p.name))
8182
: plugins.filter((plugin) => plugin.scope !== "user");
8283
const updated: string[] = [];
8384
for (const plugin of targets) {
84-
const didUpdate = await this.updateOnePlugin(plugin, toolId, projectRoot, cacheDir, manifest);
85+
const didUpdate = await this.updateOnePlugin(
86+
plugin,
87+
toolId,
88+
projectRoot,
89+
cacheDir,
90+
manifest,
91+
rebuild
92+
);
8593
if (didUpdate) updated.push(plugin.name);
8694
}
8795
return updated;
@@ -92,14 +100,15 @@ export class PluginUpdateUseCase {
92100
toolId: AiToolId,
93101
projectRoot: string,
94102
cacheDir: string,
95-
manifest: Manifest
103+
manifest: Manifest,
104+
rebuild: boolean
96105
): Promise<boolean> {
97106
if (plugin.scope === "user") throw new InvalidPluginScopeError(toolId, "project", "user");
98107
const dist = await this.distributionLoader.load(plugin.source, cacheDir, {
99108
forceRefresh: true,
100109
});
101110
await this.pluginRuntime?.ensurePluginRuntimeFiles(toolId, projectRoot, manifest);
102-
if (compareSemver(dist.manifest.version, plugin.version) <= 0) return false;
111+
if (!rebuild && compareSemver(dist.manifest.version, plugin.version) <= 0) return false;
103112
await this.replacePluginFiles(plugin, dist, toolId, projectRoot, manifest);
104113
return true;
105114
}

‎cli/src/contexts/framework/application/shared/ensure-built-marketplace-use-case.ts‎

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,18 @@ import type { FrameworkBuildMode } from "../../../tools/domain/registry.js";
1414
import type { FrameworkBuild } from "../../../translate/application/translate-source.js";
1515
import type { FrameworkBuildTarget } from "../../../translate/domain/build-target.js";
1616

17-
/** `undefined` when the target/mode pair has no build. */
17+
/** `undefined` when the target/mode pair has no build. With a provider, every agent gets its
18+
* role's model there. */
1819
export type FrameworkBuildFor = (
1920
target: FrameworkBuildTarget,
2021
mode: FrameworkBuildMode,
21-
outDir: string
22+
outDir: string,
23+
provider?: string
2224
) => FrameworkBuild | undefined;
2325

26+
/** The provider chosen for a target's agents, if any. */
27+
export type AgentProviderFor = (target: FrameworkBuildTarget) => Promise<string | undefined>;
28+
2429
export interface EnsureBuiltMarketplaceOptions {
2530
readonly projectRoot: string;
2631
readonly marketplace: Marketplace;
@@ -60,7 +65,9 @@ export class EnsureBuiltMarketplaceUseCase implements EnsureBuiltMarketplace {
6065
* happened to register it would tie a declaration meant for every project to the life of one
6166
* of them: delete that project and the global registration points at nothing.
6267
*/
63-
private readonly userCacheRoot: () => string
68+
private readonly userCacheRoot: () => string,
69+
/** A changed provider changes the built agents, so it is part of what keeps a cache fresh. */
70+
private readonly agentProviderFor: AgentProviderFor = async () => undefined
6471
) {}
6572

6673
async execute(options: EnsureBuiltMarketplaceOptions): Promise<EnsureBuiltMarketplaceResult> {
@@ -82,7 +89,8 @@ export class EnsureBuiltMarketplaceUseCase implements EnsureBuiltMarketplace {
8289
projectRoot: options.projectRoot,
8390
forceRefresh: options.forceRefresh,
8491
});
85-
const sentinel = this.sentinelValue(resolved.catalog?.version);
92+
const provider = await this.agentProviderFor(options.target);
93+
const sentinel = this.sentinelValue(resolved.catalog?.version, provider);
8694
const memoKey = `${options.marketplace.name}:${options.target}:${sentinel}`;
8795
const memoized = this.memo.get(memoKey);
8896
if (memoized !== undefined) return memoized;
@@ -91,14 +99,16 @@ export class EnsureBuiltMarketplaceUseCase implements EnsureBuiltMarketplace {
9199
builtDir,
92100
resolve(resolved.localPath),
93101
sentinel,
94-
this.versionIsTrustworthy(options)
102+
this.versionIsTrustworthy(options),
103+
provider
95104
);
96105
this.memo.set(memoKey, result);
97106
return result;
98107
}
99108

100-
private sentinelValue(catalogVersion: string | undefined): string {
101-
return `${this.version.get()}:${catalogVersion ?? UNVERSIONED}`;
109+
private sentinelValue(catalogVersion: string | undefined, provider: string | undefined): string {
110+
const base = `${this.version.get()}:${catalogVersion ?? UNVERSIONED}`;
111+
return provider === undefined ? base : `${base}:${provider}`;
102112
}
103113

104114
/**
@@ -120,13 +130,14 @@ export class EnsureBuiltMarketplaceUseCase implements EnsureBuiltMarketplace {
120130
builtDir: string,
121131
sourceDir: string,
122132
sentinel: string,
123-
versionIsTrustworthy: boolean
133+
versionIsTrustworthy: boolean,
134+
provider: string | undefined
124135
): Promise<EnsureBuiltMarketplaceResult> {
125136
const version = sentinel.split(":")[1];
126137
if (versionIsTrustworthy && (await this.isFresh(builtDir, sentinel))) {
127138
return { builtDir, version, rebuilt: false };
128139
}
129-
await this.build(options.target, options.mode, sourceDir, builtDir);
140+
await this.build(options.target, options.mode, sourceDir, builtDir, provider);
130141
await this.fs.writeFile(join(builtDir, SENTINEL_FILE), sentinel);
131142
return { builtDir, version, rebuilt: true };
132143
}
@@ -143,13 +154,14 @@ export class EnsureBuiltMarketplaceUseCase implements EnsureBuiltMarketplace {
143154
target: FrameworkBuildTarget,
144155
mode: FrameworkBuildMode,
145156
sourceDir: string,
146-
builtDir: string
157+
builtDir: string,
158+
provider: string | undefined
147159
): Promise<void> {
148160
if (this.nested(sourceDir, builtDir)) {
149-
await this.buildViaTemp(target, mode, sourceDir, builtDir);
161+
await this.buildViaTemp(target, mode, sourceDir, builtDir, provider);
150162
return;
151163
}
152-
await this.runBuild(target, mode, sourceDir, builtDir);
164+
await this.runBuild(target, mode, sourceDir, builtDir, provider);
153165
}
154166

155167
private nested(sourceDir: string, builtDir: string): boolean {
@@ -160,11 +172,12 @@ export class EnsureBuiltMarketplaceUseCase implements EnsureBuiltMarketplace {
160172
target: FrameworkBuildTarget,
161173
mode: FrameworkBuildMode,
162174
sourceDir: string,
163-
builtDir: string
175+
builtDir: string,
176+
provider: string | undefined
164177
): Promise<void> {
165178
const temp = join(tmpdir(), `aidd-built-${target}-${mode}`);
166179
await this.fs.deleteDirectory(temp);
167-
await this.runBuild(target, mode, sourceDir, temp);
180+
await this.runBuild(target, mode, sourceDir, temp, provider);
168181
await this.fs.deleteDirectory(builtDir);
169182
await this.copyDir(temp, builtDir);
170183
await this.fs.deleteDirectory(temp);
@@ -177,10 +190,11 @@ export class EnsureBuiltMarketplaceUseCase implements EnsureBuiltMarketplace {
177190
target: FrameworkBuildTarget,
178191
mode: FrameworkBuildMode,
179192
sourceDir: string,
180-
outDir: string
193+
outDir: string,
194+
provider: string | undefined
181195
): Promise<void> {
182196
await this.fs.createDirectory(outDir);
183-
const build = this.buildFor(target, mode, outDir);
197+
const build = this.buildFor(target, mode, outDir, provider);
184198
if (build === undefined) {
185199
throw new Error(`No framework build for target '${target}' mode '${mode}'.`);
186200
}

0 commit comments

Comments
 (0)