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
7 changes: 4 additions & 3 deletions docs/agent-profile-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ model can choose the right profile.

### `provider`

Required built-in provider id:
Required provider instance id. The default instance ids are:

```yaml
provider: codex
Expand All @@ -76,8 +76,9 @@ provider: copilot
provider: grok
```

Unsupported or custom providers are rejected. DevSpace maps providers to their
native integration:
Profiles may also reference named provider instances configured under
`subagents.providers`, such as `provider: codex-work`. Provider instances map
to one of these built-in driver integrations:

- `codex`: the host-installed `codex app-server` command
- `claude`: Claude Code SDK
Expand Down
20 changes: 20 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,26 @@ Subagent providers are explicit. Omitted providers are disabled:
}
```

`id` is the provider instance id used by profiles and `devspace agents run`.
For the default built-in instance ids, `driver` is optional and defaults to the
same value. Named instances declare the driver explicitly, which allows
multiple independently configured instances of one integration:

```jsonc
{
"id": "codex-work",
"driver": "codex",
"enabled": true,
"env": {
"CODEX_HOME": "/home/alice/.codex-work",
},
}
```

Profiles can then select that instance with `provider: codex-work`. The driver
is the implementation (`codex`, `claude`, `opencode`, `pi`, `cursor`,
`copilot`, or `grok`); provider-native session ids remain internal to DevSpace.

`subagents.instructions` controls when ChatGPT receives the managed workflow:

| Value | Behavior |
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"drizzle-orm": "^0.45.2",
"express": "^5.2.1",
"jsonc-parser": "^3.3.1",
"koffi": "^3.1.2",
"koffi": "3.2.1",
"lucide": "^1.24.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
Expand Down
2 changes: 1 addition & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

128 changes: 46 additions & 82 deletions schema/v1/devspace.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,92 +177,56 @@
"providers": {
"type": "array",
"items": {
"oneOf": [
{
"type": "object",
"properties": {
"id": {
"type": "string",
"enum": [
"codex",
"claude",
"cursor",
"copilot",
"grok"
]
},
"enabled": {
"type": "boolean"
},
"model": {
"type": "string",
"minLength": 1
},
"effort": {
"type": "string",
"minLength": 1
},
"env": {
"type": "object",
"propertyNames": {
"type": "string",
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
},
"additionalProperties": {
"type": "string"
}
},
"command": {
"type": "string",
"minLength": 1,
"pattern": "\\S"
}
},
"required": [
"id",
"enabled"
],
"additionalProperties": false
"type": "object",
"properties": {
"id": {
"type": "string",
"minLength": 1
},
"driver": {
"type": "string",
"enum": [
"codex",
"claude",
"opencode",
"pi",
"cursor",
"copilot",
"grok"
]
},
"enabled": {
"type": "boolean"
},
{
"model": {
"type": "string",
"minLength": 1
},
"effort": {
"type": "string",
"minLength": 1
},
"env": {
"type": "object",
"properties": {
"id": {
"type": "string",
"enum": [
"opencode",
"pi"
]
},
"enabled": {
"type": "boolean"
},
"model": {
"type": "string",
"minLength": 1
},
"effort": {
"type": "string",
"minLength": 1
},
"env": {
"type": "object",
"propertyNames": {
"type": "string",
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
},
"additionalProperties": {
"type": "string"
}
}
"propertyNames": {
"type": "string",
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
},
"required": [
"id",
"enabled"
],
"additionalProperties": false
"additionalProperties": {
"type": "string"
}
},
"command": {
"type": "string",
"minLength": 1,
"pattern": "\\S"
}
]
},
"required": [
"id",
"enabled"
],
"additionalProperties": false
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
Waishnav marked this conversation as resolved.
}
}
},
Expand Down
6 changes: 4 additions & 2 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ try {
workspaceId: "ws_current",
workspaceRoot: projectRoot,
profileName: "reviewer",
provider: "codex",
providerInstanceId: "codex",
driver: "codex",
model: "gpt-5.4",
effort: "high",
}).id,
Expand All @@ -79,7 +80,8 @@ try {
workspaceId: "ws_other",
workspaceRoot: projectRoot,
profileName: "reviewer",
provider: "codex",
providerInstanceId: "codex",
driver: "codex",
}).id,
{ status: "running" },
);
Expand Down
6 changes: 3 additions & 3 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
formatLocalAgentProviderStatusSummary,
} from "./local-agent-catalog.js";
import { loadLocalAgentProfiles } from "./local-agent-profiles.js";
import type { LocalAgentProvider } from "./local-agent-profiles.js";
import { resolveSubagentsConfig } from "./local-agent-config.js";
import {
parseLocalAgentContinueArgs,
parseLocalAgentRunArgs,
Expand Down Expand Up @@ -218,7 +218,7 @@ async function runInit({ force }: { force: boolean }): Promise<void> {
}));
}

const currentSubagents = files.config.subagents;
const currentSubagents = resolveSubagentsConfig(files.config.subagents);
const availability = getLocalAgentProviderAvailabilitySnapshot(
process.env,
currentSubagents,
Expand Down Expand Up @@ -247,7 +247,7 @@ async function runInit({ force }: { force: boolean }): Promise<void> {
required: true,
});
if (prompts.isCancel(providerAnswer)) throw new SetupCancelledError();
const selectedProviders = providerAnswer as LocalAgentProvider[];
const selectedProviders = providerAnswer as string[];
const subagents = updateOnboardingSubagentsConfig(
currentSubagents,
selectedProviders,
Expand Down
4 changes: 2 additions & 2 deletions src/config-migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
type DevspaceConfig,
} from "./config-schema.js";
import { storedSubagentsConfigSchema } from "./local-agent-config.js";
import { LOCAL_AGENT_PROVIDERS } from "./local-agent-profiles.js";
import { LOCAL_AGENT_DRIVER_KINDS } from "./local-agent-provider.js";

const legacyConfigSchema = z.object({
host: z.string().optional(),
Expand Down Expand Up @@ -92,7 +92,7 @@ function migrateLegacySubagents(
return {
enabled: value,
providers: value
? LOCAL_AGENT_PROVIDERS.map((id) => ({ id, enabled: true }))
? LOCAL_AGENT_DRIVER_KINDS.map((id) => ({ id, enabled: true }))
: [],
};
}
4 changes: 2 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { expandHomePath } from "./roots.js";
import type { LoggingConfig } from "./logger.js";
import type { OAuthConfig } from "./oauth-provider.js";
import { devspaceAgentsDir, devspaceSkillsDir, loadDevspaceFiles } from "./user-config.js";
import type { SubagentsConfig } from "./local-agent-config.js";
import { resolveSubagentsConfig, type SubagentsConfig } from "./local-agent-config.js";

export type { ToolMode } from "./config-schema.js";

Expand Down Expand Up @@ -75,7 +75,7 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): ServerConfig {
skillPaths: stored.skills.paths,
devspaceSkillsDir: devspaceSkillsDir(env),
devspaceAgentsDir: devspaceAgentsDir(env),
subagents: stored.subagents,
subagents: resolveSubagentsConfig(stored.subagents),
agentDir: normalizePath(stored.skills.agentDir),
logging: {
...stored.logging,
Expand Down
21 changes: 21 additions & 0 deletions src/db/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ const migrations: Migration[] = [
name: "local-agent-turns",
up: migrateLocalAgentTurns,
},
{
version: 9,
name: "local-agent-provider-instances",
up: migrateLocalAgentProviderInstances,
},
];

export function migrateDatabase(sqlite: Database.Database): void {
Expand Down Expand Up @@ -289,6 +294,22 @@ function migrateLocalAgentTurns(sqlite: Database.Database): void {
`);
}

function migrateLocalAgentProviderInstances(sqlite: Database.Database): void {
const columns = sqlite.prepare("pragma table_info(local_agent_sessions)").all() as Array<{
name: string;
}>;
const names = new Set(columns.map((column) => column.name));
if (names.has("provider") && !names.has("provider_instance_id")) {
sqlite.exec("alter table local_agent_sessions rename column provider to provider_instance_id");
}
addColumnIfMissing(sqlite, "local_agent_sessions", "driver", "text not null default ''");
sqlite.exec(`
update local_agent_sessions
set driver = provider_instance_id
where driver is null or driver = ''
`);
}

function addColumnIfMissing(
sqlite: Database.Database,
table: "workspace_sessions" | "local_agent_sessions",
Expand Down
3 changes: 2 additions & 1 deletion src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ export const localAgentSessions = sqliteTable(
workspaceId: text("workspace_id"),
workspaceRoot: text("workspace_root").notNull(),
profileName: text("profile_name").notNull(),
provider: text("provider").notNull(),
providerInstanceId: text("provider_instance_id").notNull(),
driver: text("driver").notNull(),
model: text("model"),
effort: text("effort"),
providerSessionId: text("provider_session_id"),
Expand Down
10 changes: 9 additions & 1 deletion src/local-agent-acp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ await overlapRuntime.close();

const cachedContext = {
agentId: "agt_acp",
providerInstanceId: "cursor",
provider: "cursor" as const,
workspaceRoot: "/tmp/project",
writeMode: "allowed" as const,
Expand All @@ -266,11 +267,13 @@ assert.deepEqual(acpCommandArgs("cursor", cachedContext), [
]);
assert.deepEqual(acpCommandArgs("grok", {
...cachedContext,
providerInstanceId: "grok",
provider: "grok",
effort: "low",
}), ["agent", "--reasoning-effort", "low", "stdio"]);
assert.deepEqual(acpCommandArgs("grok", {
...cachedContext,
providerInstanceId: "grok",
provider: "grok",
effort: "low",
}, { GROK_AGENT_PROFILE: " /tmp/grok-coding-only.md " }), [
Expand Down Expand Up @@ -314,7 +317,12 @@ if (process.platform === "win32") {
);
await writeFile(command, `@ECHO OFF\r\n"${process.execPath}" "${recorder}" %*\r\n`);
const shimDriver = new AcpLocalAgentDriver("copilot", process.env, () => command);
const shimStartup = await shimDriver.createRuntime({ ...cachedContext, provider: "copilot", workspaceRoot });
const shimStartup = await shimDriver.createRuntime({
...cachedContext,
providerInstanceId: "copilot",
provider: "copilot",
workspaceRoot,
});
assert.equal(shimStartup.isErr(), true);
if (shimStartup.isErr()) assert.equal(shimStartup.error.code, "PROVIDER_PROTOCOL_ERROR");
const forwarded = JSON.parse(await readFile(marker, "utf8")) as string[];
Expand Down
2 changes: 2 additions & 0 deletions src/local-agent-acp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ export class AcpRuntime implements LocalAgentRuntime {

export class AcpLocalAgentDriver implements LocalAgentDriver {
readonly provider: AcpProvider;
readonly providerInstanceId: string;
// Keep ACP warm briefly, then let the generic pool close the process so the
// daemon can reach its own idle shutdown state.
readonly idleTimeoutMs = 5 * 60_000;
Expand All @@ -422,6 +423,7 @@ export class AcpLocalAgentDriver implements LocalAgentDriver {
private readonly commandResolver: AcpCommandResolver = resolveAcpCommand,
) {
this.provider = provider;
this.providerInstanceId = provider;
}

runtimeKey(context: LocalAgentRuntimeContext): string {
Expand Down
Loading
Loading