Skip to content

Commit 048c427

Browse files
committed
fix: address review feedback on model roles
1 parent 0b04283 commit 048c427

6 files changed

Lines changed: 35 additions & 8 deletions

File tree

apps/pythinker-code/src/tui/commands/config.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,7 @@ function resolveWorkspaceConfigPath(input: string, workDir: string): string {
482482

483483
export async function handleModelCommand(host: SlashCommandHost, args: string): Promise<void> {
484484
const requestedAlias = args.trim();
485-
const tokens = requestedAlias.split(/\s+/).filter(Boolean);
485+
const tokens = requestedAlias.split(/\s+/u).filter(Boolean);
486486
const config = await host.harness.getConfig({ reload: true });
487487
const roles = [...new Set([...BUILT_IN_MODEL_ROLES, ...Object.keys(config.modelRoles ?? {})])]
488488
.filter((role) => role.length > 0 && role !== 'default');
@@ -505,7 +505,12 @@ export async function handleModelCommand(host: SlashCommandHost, args: string):
505505
return;
506506
}
507507
if (tokens.length === 1) {
508-
showModelPicker(host, config.modelRoles?.[role], undefined, { assignToRole: role });
508+
const picker = showModelPicker(host, config.modelRoles?.[role], undefined, {
509+
assignToRole: role,
510+
});
511+
if (picker !== undefined) {
512+
void refreshModelsForOpenPicker(host, picker, config.modelRoles?.[role]);
513+
}
509514
return;
510515
}
511516
}

apps/pythinker-code/test/tui/commands/model-roles.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ describe('/model roles', () => {
8181
const { host, session, setConfig } = makeHost();
8282

8383
await handleModelCommand(host, 'small');
84+
expect(host.authFlow.refreshProviderModels).toHaveBeenCalledOnce();
8485
mountedPicker(host).handleInput(ENTER);
8586

8687
await vi.waitFor(() => {

docs/configuration/config-files.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ You can also switch models temporarily without touching the config file — by s
158158

159159
## `model_roles`
160160

161-
Each entry in the `model_roles` table locks a model alias to a named role. The built-in roles are `small`, `implementer`, and `advisor`; any other key defines a custom role. Values must be aliases defined in `models`; an empty string clears the role.
161+
Each entry in the `model_roles` table locks a model alias to a named role. The built-in roles are `small`, `implementer`, and `advisor`; any other key except the reserved `default` defines a custom role. Values must be aliases defined in `models`; an empty string clears the role.
162162

163163
```toml
164164
[model_roles]
@@ -169,7 +169,7 @@ advisor = "reviewer-model"
169169

170170
Roles take effect in two places:
171171

172-
- Wherever a subagent model alias is accepted (the `Agent` and `DynamicWorkflow` tool `model` arguments, and agent profile frontmatter), a `@<role>` reference such as `@small` resolves to the locked alias. An unassigned or unresolvable role falls back to the normal model precedence.
172+
- Wherever a subagent model alias is accepted (the `Agent` and `DynamicWorkflow` tool `model` arguments, and agent profile frontmatter), a `@<role>` reference such as `@small` resolves to the locked alias. An unassigned or unresolvable role falls back to the parent agent's model.
173173
- When `implementer` is assigned, it becomes the default model for subagents that do not set an explicit or profile model. Subagents of those subagents inherit the same default.
174174

175175
Inside the TUI, `/model <role>` assigns a role from the model picker, `/model <role> clear` removes it, and `/model roles` lists the current assignments. See [Slash commands](../reference/slash-commands.md).

packages/agent-core/src/config/schema.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,11 +270,16 @@ export const McpServerConfigSchema = z.preprocess((raw) => {
270270

271271
export type McpServerConfig = z.infer<typeof McpServerConfigSchema>;
272272

273+
const ModelRolesSchema = z.record(z.string(), z.string()).refine(
274+
(roles) => !Object.hasOwn(roles, 'default'),
275+
{ message: '"default" is a reserved model role name' },
276+
);
277+
273278
export const PythinkerConfigSchema = z.object({
274279
providers: z.record(z.string(), ProviderConfigSchema).default({}),
275280
defaultProvider: z.string().optional(),
276281
defaultModel: z.string().optional(),
277-
modelRoles: z.record(z.string(), z.string()).optional(),
282+
modelRoles: ModelRolesSchema.optional(),
278283
outputStyle: z.string().trim().min(1).optional(),
279284
models: z.record(z.string(), ModelAliasSchema).optional(),
280285
thinking: ThinkingConfigSchema.optional(),
@@ -320,7 +325,7 @@ export const PythinkerConfigPatchSchema = z
320325
providers: z.record(z.string(), ProviderConfigPatchSchema).optional(),
321326
defaultProvider: z.string().optional(),
322327
defaultModel: z.string().optional(),
323-
modelRoles: z.record(z.string(), z.string()).optional(),
328+
modelRoles: ModelRolesSchema.optional(),
324329
outputStyle: z.string().trim().min(1).optional(),
325330
models: z.record(z.string(), ModelAliasPatchSchema).optional(),
326331
thinking: ThinkingConfigPatchSchema.optional(),

packages/agent-core/src/tools/builtin/collaboration/dynamic-workflow.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export const DynamicWorkflowToolInputSchema = z
102102
.min(1)
103103
.optional()
104104
.describe(
105-
'Model alias for every subagent in this workflow, so the orchestrator can run on one model while the workers run on a cheaper or faster one. References such as @small, @implementer, @advisor, and @<custom-role> resolve through configured model roles and fall back to normal precedence when unassigned. Defaults to the subagent type profile model, then this agent model.',
105+
'Model alias for every subagent in this workflow, so the orchestrator can run on one model while the workers run on a cheaper or faster one. References such as @small, @implementer, @advisor, and @<custom-role> resolve through configured model roles and fall back to normal precedence when unassigned. Defaults to the subagent type profile model, then the configured implementer model role, then this agent model.',
106106
),
107107
effort: z
108108
.string()

packages/agent-core/test/config/model-roles.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
11
import { describe, expect, it } from 'vitest';
22

3-
import { expandModelRef, resolveModelRoleAlias } from '../../src/config';
3+
import {
4+
expandModelRef,
5+
PythinkerConfigPatchSchema,
6+
PythinkerConfigSchema,
7+
resolveModelRoleAlias,
8+
} from '../../src/config';
49

510
describe('model roles', () => {
11+
it('rejects the reserved default role in full and patch configs', () => {
12+
expect(
13+
PythinkerConfigSchema.safeParse({ modelRoles: { default: 'x' } }).success,
14+
).toBe(false);
15+
expect(
16+
PythinkerConfigPatchSchema.safeParse({ modelRoles: { default: 'x' } }).success,
17+
).toBe(false);
18+
expect(PythinkerConfigSchema.safeParse({ modelRoles: { custom: 'x' } }).success).toBe(true);
19+
expect(PythinkerConfigPatchSchema.safeParse({ modelRoles: { custom: 'x' } }).success).toBe(true);
20+
});
21+
622
it('resolves assigned roles and treats empty assignments as cleared', () => {
723
const config = {
824
modelRoles: {

0 commit comments

Comments
 (0)