Skip to content

Commit 463b176

Browse files
authored
feat(dynamic-workflow): run subagents on a chosen model (#30)
## Related Issue No issue — problem described below. ## Problem Every subagent in a Dynamic Workflow ran on whatever model the calling agent was using. A workflow that fans 128 children out over mechanical work — reading files, applying a mechanical edit, running a check — paid the orchestrator's model for all of it, and there was no way to say "plan here, implement there" without editing an agent profile up front. The provider layer already supported this: a model alias resolves to its own provider per agent, and `configureChild` already honoured `profile.model` / `profile.effort` (kept across resume by #28). The routing decision just had no way to reach a single run. ## What changed `DynamicWorkflow` accepts `model` and `effort`. Both apply to every subagent in the call and travel on `QueuedSubagentTask` through `SubagentBatch` into `RunSubagentOptions`, where the existing option → profile → parent precedence resolves them, so the choice also survives resume and retry. An alias the provider cannot resolve falls back to the calling agent's model instead of failing at generate time. `/workflow model <alias>` stores the choice for the session; `/workflow model` reports it and `/workflow model off` clears it. The alias reaches the run as an instruction on the task prompt rather than a hard override, so the agent can still pick something else when the work plainly calls for it — the same shape as the existing Dynamic Workflow mode reminders. /workflow model deepseek-v4 /workflow audit every route handler under src/routes/ for missing auth → orchestrator stays on the session model, all 128 children run on deepseek-v4 ## Checklist - [x] I have read the CONTRIBUTING document. - [x] I have linked a related issue, or explained the problem above. - [x] I have added tests that prove my feature works. - [x] Ran `gen-changesets` skill, or this PR needs no changeset. - [x] Ran `gen-docs` skill, or this PR needs no doc update. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Configure a model and reasoning effort for all Dynamic Workflow subagents. * Use `/workflow model <alias>` to view, select, or clear the session’s subagent model. * Added autocomplete support for the new workflow model command. * **Documentation** * Updated Dynamic Workflow and slash-command documentation with model and effort configuration details. * **Bug Fixes** * Ensured selected model and reasoning settings apply consistently to spawned and resumed workflow tasks. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent cf5b6b1 commit 463b176

13 files changed

Lines changed: 203 additions & 6 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@pythoughts/pythinker-code": minor
3+
---
4+
5+
Let a Dynamic Workflow run its subagents on a different model than the agent orchestrating them. `DynamicWorkflow` accepts `model` and `effort` for every subagent in the call, and `/workflow model <alias>` sets that model for the session so an expensive orchestrator can hand mechanical work to a cheaper or faster one.

apps/pythinker-code/src/tui/commands/dynamic-workflow.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export async function handleDynamicWorkflowCommand(host: SlashCommandHost, args:
1919
}
2020

2121
const prompt = args.trim();
22+
if (handleModelSubcommand(host, prompt)) return;
23+
2224
const mode = dynamicWorkflowModeSubcommand(prompt);
2325
if (mode !== undefined) {
2426
await applyDynamicWorkflowMode(host, mode, `/workflow ${prompt}`);
@@ -93,7 +95,50 @@ async function startDynamicWorkflowTask(host: SlashCommandHost, prompt: string):
9395
return;
9496
}
9597
renderDynamicWorkflowModeMarker(host, 'active');
96-
host.sendNormalUserInput(prompt);
98+
host.sendNormalUserInput(withWorkerModelInstruction(prompt, host.state.appState.dynamicWorkflowModel));
99+
}
100+
101+
/**
102+
* `/workflow model <alias>` is a preference, not a hard override: it reaches the
103+
* subagents as an instruction to set DynamicWorkflow's `model` field, so the
104+
* agent can still pick something else when the task plainly calls for it.
105+
*/
106+
function withWorkerModelInstruction(prompt: string, model: string | undefined): string {
107+
return model === undefined
108+
? prompt
109+
: `${prompt}\n\nUse model "${model}" for the DynamicWorkflow subagents in this task.`;
110+
}
111+
112+
/** Returns true when the input was a `model` subcommand and has been handled. */
113+
function handleModelSubcommand(host: SlashCommandHost, input: string): boolean {
114+
const match = /^model(?:\s+(.*))?$/iu.exec(input);
115+
if (match === null) return false;
116+
117+
const value = match[1]?.trim() ?? '';
118+
const current = host.state.appState.dynamicWorkflowModel;
119+
if (value.length === 0) {
120+
host.showStatus(
121+
current === undefined
122+
? 'Dynamic Workflow subagents use this session model. Set another with /workflow model <alias>.'
123+
: `Dynamic Workflow subagents use ${current}. Clear it with /workflow model off.`,
124+
);
125+
return true;
126+
}
127+
if (value.toLowerCase() === 'off' || value.toLowerCase() === 'clear') {
128+
host.setAppState({ dynamicWorkflowModel: undefined });
129+
host.showStatus('Dynamic Workflow subagents now use this session model.');
130+
return true;
131+
}
132+
// An alias the engine cannot resolve falls back to the session model at spawn
133+
// time, so accepting one here would report a routing that never happens.
134+
const configured = host.state.appState.availableModels;
135+
if (Object.keys(configured).length > 0 && !Object.hasOwn(configured, value)) {
136+
host.showError(`Unknown model: ${value}. Run /model to see the configured aliases.`);
137+
return true;
138+
}
139+
host.setAppState({ dynamicWorkflowModel: value });
140+
host.showStatus(`Dynamic Workflow subagents will use ${value}.`);
141+
return true;
97142
}
98143

99144
async function applyDynamicWorkflowMode(

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const GOAL_NEXT_ARG_COMPLETIONS: readonly ArgCompletionSpec[] = [
2020
const DYNAMIC_WORKFLOW_ARG_COMPLETIONS: readonly ArgCompletionSpec[] = [
2121
{ value: 'on', description: 'Turn Dynamic Workflow mode on' },
2222
{ value: 'off', description: 'Turn Dynamic Workflow mode off' },
23+
{ value: 'model', description: 'Set the model Dynamic Workflow subagents run on' },
2324
];
2425

2526
const FAST_ARG_COMPLETIONS: readonly ArgCompletionSpec[] = [
@@ -154,7 +155,7 @@ export const BUILTIN_SLASH_COMMANDS = [
154155
{
155156
name: 'workflow',
156157
aliases: [],
157-
description: 'Toggle Dynamic Workflow or run a task in parallel',
158+
description: 'Toggle Dynamic Workflow, set its subagent model, or run a task in parallel',
158159
priority: 100,
159160
completeArgs: dynamicWorkflowArgumentCompletions,
160161
availability: 'idle-only',

apps/pythinker-code/src/tui/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ export interface AppState {
4040
permissionMode: PermissionMode;
4141
planMode: boolean;
4242
dynamicWorkflowMode: boolean;
43+
/** Model alias `/workflow` asks Dynamic Workflow subagents to run on, so workers
44+
* can use a cheaper or faster model than the agent orchestrating them. */
45+
dynamicWorkflowModel?: string;
4346
/** Whether provider-native Fast mode is requested for this session. */
4447
fastMode?: boolean;
4548
/** Whether the current model/provider accepts provider-native Fast mode. */

apps/pythinker-code/test/tui/commands/dynamic-workflow.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ function makeHost(
2222
hasSession?: boolean;
2323
permissionMode?: 'manual' | 'auto' | 'yolo';
2424
dynamicWorkflowMode?: boolean;
25+
availableModels?: Record<string, unknown>;
2526
} = {},
2627
) {
2728
const session = {
@@ -35,6 +36,9 @@ function makeHost(
3536
model: overrides.model ?? 'pythinker-model',
3637
permissionMode: overrides.permissionMode ?? 'auto',
3738
dynamicWorkflowMode: overrides.dynamicWorkflowMode ?? false,
39+
availableModels: overrides.availableModels ?? {
40+
'deepseek-v4': { provider: 'deepseek', model: 'deepseek-v4' },
41+
},
3842
},
3943
theme: currentTheme,
4044
transcriptContainer: { addChild: vi.fn() },
@@ -336,4 +340,55 @@ describe('handleDynamicWorkflowCommand', () => {
336340
expect(markerAddChild(host)).not.toHaveBeenCalled();
337341
expect(host.sendNormalUserInput).not.toHaveBeenCalled();
338342
});
343+
344+
it('sets, reports, and clears the Dynamic Workflow subagent model', async () => {
345+
const { host, session } = makeHost({ permissionMode: 'auto' });
346+
347+
await handleDynamicWorkflowCommand(host, 'model');
348+
expect(host.showStatus).toHaveBeenLastCalledWith(
349+
expect.stringContaining('use this session model'),
350+
);
351+
352+
await handleDynamicWorkflowCommand(host, 'model deepseek-v4');
353+
expect(host.showStatus).toHaveBeenLastCalledWith('Dynamic Workflow subagents will use deepseek-v4.');
354+
expect(host.state.appState.dynamicWorkflowModel).toBe('deepseek-v4');
355+
356+
await handleDynamicWorkflowCommand(host, 'model');
357+
expect(host.showStatus).toHaveBeenLastCalledWith(
358+
expect.stringContaining('subagents use deepseek-v4'),
359+
);
360+
361+
await handleDynamicWorkflowCommand(host, 'model off');
362+
expect(host.showStatus).toHaveBeenLastCalledWith(
363+
'Dynamic Workflow subagents now use this session model.',
364+
);
365+
expect(host.state.appState.dynamicWorkflowModel).toBeUndefined();
366+
367+
// A model subcommand must never be mistaken for a task prompt.
368+
expect(session.setDynamicWorkflowMode).not.toHaveBeenCalled();
369+
expect(host.sendNormalUserInput).not.toHaveBeenCalled();
370+
});
371+
372+
it('rejects a model alias that is not configured', async () => {
373+
const { host } = makeHost({ permissionMode: 'auto' });
374+
375+
await handleDynamicWorkflowCommand(host, 'model not-a-real-alias');
376+
377+
expect(host.showError).toHaveBeenCalledWith(
378+
expect.stringContaining('Unknown model: not-a-real-alias'),
379+
);
380+
expect(host.state.appState.dynamicWorkflowModel).toBeUndefined();
381+
expect(host.sendNormalUserInput).not.toHaveBeenCalled();
382+
});
383+
384+
it('asks the task to route subagents to the configured model', async () => {
385+
const { host } = makeHost({ permissionMode: 'auto' });
386+
387+
await handleDynamicWorkflowCommand(host, 'model deepseek-v4');
388+
await handleDynamicWorkflowCommand(host, 'Audit every route for missing auth');
389+
390+
expect(host.sendNormalUserInput).toHaveBeenCalledWith(
391+
'Audit every route for missing auth\n\nUse model "deepseek-v4" for the DynamicWorkflow subagents in this task.',
392+
);
393+
});
339394
});

apps/pythinker-code/test/tui/commands/registry.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,9 @@ describe('built-in slash command registry', () => {
124124
return items === null ? null : items.map((item) => item.value);
125125
};
126126

127-
expect(values('')).toEqual(['on', 'off']);
127+
expect(values('')).toEqual(['on', 'off', 'model']);
128128
expect(values('O')).toEqual(['on', 'off']);
129+
expect(values('mod')).toEqual(['model']);
129130
expect(dynamicWorkflowArgumentCompletions('of')).toEqual([
130131
{ value: 'off', label: 'off', description: 'Turn Dynamic Workflow mode off' },
131132
]);

docs/reference/slash-commands.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Some commands are only available in the idle state. Executing these commands whi
5151
| `/fast [on\|off\|status]` || Toggle provider-native Fast mode for the current session, or show its status. Without arguments, flips the current state | Status only |
5252
| `/workflow [on\|off]` || Toggle Dynamic Workflow mode without sending a prompt. Without arguments, flips the current state; explicitly passing `on`/`off` forces the setting. | No |
5353
| `/workflow <task>` || Turn Dynamic Workflow mode on, then send `<task>` as a normal prompt. If the turn completes normally, Dynamic Workflow mode turns off automatically. In `manual` permission mode, Pythinker Code asks whether to switch to `auto` or `yolo` before starting. | No |
54+
| `/workflow model [alias\|off]` || Ask Dynamic Workflow subagents to run on `alias` instead of the session model, so workers can use a cheaper or faster model than the agent orchestrating them. Without arguments, shows the current setting; `off` clears it. Lasts for the session. | No |
5455
| `/goal [...]` || Start or manage an autonomous goal | See below |
5556

5657
::: info

docs/reference/tools.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ Collaboration tools handle inter-Agent coordination, user interaction, and Skill
9191

9292
**`Agent`** delegates a subtask to a sub-Agent. Required parameters: `prompt` (complete task description) and `description` (a 3–5 word short summary). Optional parameters: `subagent_type` (defaults to `coder`), `resume` (ID of an existing Agent to resume; mutually exclusive with `subagent_type`), and `run_in_background` (defaults to false). Agent tasks have a fixed 30-minute timeout. In foreground mode the parent Agent waits for the sub-Agent to complete before continuing; in background mode a task ID is returned immediately and the result is automatically delivered back to the main Agent via a synthetic User message when done. When several foreground `Agent` calls run in the same step, the TUI groups them and shows each subagent's running, waiting, completed, or failed status with elapsed time. See [Agent & Sub-Agents](../customization/agents.md) for details.
9393

94-
**`DynamicWorkflow`** launches several independent subagents in parallel, resumes existing subagents through `resume_agent_ids`, or combines both in one call. It always requires `description`, a short summary of the whole workflow. Each entry in `items` launches one new subagent: without `prompt_template`, every entry is a complete prompt on its own; with `prompt_template`, the template must contain the `{{item}}` placeholder and each entry replaces it. Item prompts must be distinct — duplicates are rejected. Pass `subagent_type` to choose the profile used by every spawned subagent, or omit it to use `coder`. Without `resume_agent_ids`, the tool requires at least 2 items; with `resume_agent_ids`, it can resume one or more existing subagents. The tool supports up to 128 total subagents, waits for all of them to finish, and returns an aggregated report. Workflow subagents have no automatic timeout; they run until completion, failure, or user cancellation. If a model response calls `DynamicWorkflow`, that call must be the only tool call in the response; to run several workflows, call one `DynamicWorkflow`, wait for its result, then call the next, or combine the work into a single workflow. In `manual` permission mode, `DynamicWorkflow` calls outside active Dynamic Workflow mode request approval unless a permission rule allows them; while Dynamic Workflow mode is active, `DynamicWorkflow` itself is auto-approved. Permission rules match `DynamicWorkflow` by tool name only — argument patterns such as `DynamicWorkflow(workflow)` are not supported.
94+
**`DynamicWorkflow`** launches several independent subagents in parallel, resumes existing subagents through `resume_agent_ids`, or combines both in one call. It always requires `description`, a short summary of the whole workflow. Each entry in `items` launches one new subagent: without `prompt_template`, every entry is a complete prompt on its own; with `prompt_template`, the template must contain the `{{item}}` placeholder and each entry replaces it. Item prompts must be distinct — duplicates are rejected. Pass `subagent_type` to choose the profile used by every spawned subagent, or omit it to use `coder`. Pass `model` and `effort` to run this workflow's subagents on a different model than the agent orchestrating them — a cheaper or faster model for mechanical work, for example; both apply to every subagent in the call, and omitting them falls back to the subagent profile's own settings and then to the calling agent's. A `model` the provider cannot resolve falls back to the calling agent's model rather than failing the run. Without `resume_agent_ids`, the tool requires at least 2 items; with `resume_agent_ids`, it can resume one or more existing subagents. The tool supports up to 128 total subagents, waits for all of them to finish, and returns an aggregated report. Workflow subagents have no automatic timeout; they run until completion, failure, or user cancellation. If a model response calls `DynamicWorkflow`, that call must be the only tool call in the response; to run several workflows, call one `DynamicWorkflow`, wait for its result, then call the next, or combine the work into a single workflow. In `manual` permission mode, `DynamicWorkflow` calls outside active Dynamic Workflow mode request approval unless a permission rule allows them; while Dynamic Workflow mode is active, `DynamicWorkflow` itself is auto-approved. Permission rules match `DynamicWorkflow` by tool name only — argument patterns such as `DynamicWorkflow(workflow)` are not supported.
9595

9696
In the TUI, a foreground workflow shows a live framed mission-control panel with a coral title. The panel lists one row per subagent with a compact progress cube, state, task, current work, and elapsed time, followed by a recent-activity log. Each cube advances only through observed execution milestones such as startup, model output, tool use, and finalization; it does not predict time remaining. The summary reports only factual completion, failure, and cancellation counts plus elapsed time, without an estimated aggregate percentage or progress bar. In a narrow terminal the per-agent cubes are dropped before subagent identity or state; when vertical space runs out, rows are clipped in workflow-index order and the remainder is summarized as `+ N more agents`.
9797

packages/agent-core/src/session/subagent-batch.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ type BaseQueuedSubagentTask<T> = {
4949
readonly runInBackground: boolean;
5050
readonly timeout?: number;
5151
readonly signal?: AbortSignal;
52+
readonly modelAlias?: string;
53+
readonly thinkingLevel?: string;
5254
};
5355

5456
export type SpawnQueuedSubagentTask<T = unknown> = BaseQueuedSubagentTask<T> & {
@@ -286,6 +288,8 @@ export class SubagentBatch<T> {
286288
dynamicWorkflowIndex: task.dynamicWorkflowIndex,
287289
dynamicWorkflowItem: task.dynamicWorkflowItem,
288290
runInBackground: task.runInBackground,
291+
modelAlias: task.modelAlias,
292+
thinkingLevel: task.thinkingLevel,
289293
signal: attempt.controller.signal,
290294
onReady: () => {
291295
this.markAttemptReady(attempt);

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Use DynamicWorkflow when several independent subagents should run in parallel. W
44

55
Use `resume_agent_ids` to continue subagents that already exist from earlier work, such as ones that failed: map each agent id to the prompt for that resumed subagent (usually `continue` if no extra information is needed). You may combine `resume_agent_ids` with `items` in the same call to resume existing subagents and launch new ones. Do not duplicate resumed work in `items`.
66

7+
Use `model` and `effort` to run this workflow's subagents on a different model than the one orchestrating them, such as a cheaper or faster model for mechanical work while the orchestration stays on the current model. Both apply to every subagent in the call. Omitting either falls back to the subagent type's own setting, and then to your current setting. A `model` that is not a configured alias also falls back to your current model rather than failing the workflow.
8+
79
Use enough subagents to keep the work focused and parallel. DynamicWorkflow supports up to 128 subagents, and launches are queued automatically, so it is safe to split large tasks into many clear, independent items. Workflow subagents have no automatic timeout; they run until completion, failure, or user cancellation.
810

911
If `DynamicWorkflow` is called, that call must be the only tool call in the response.

0 commit comments

Comments
 (0)