Skip to content

Commit f7f731e

Browse files
committed
feat(dynamic-workflow): run subagents on a chosen model
Let a workflow hand mechanical work to a cheaper or faster model while the orchestrating agent stays where it is. DynamicWorkflow accepts model and effort, carried on QueuedSubagentTask through SubagentBatch into RunSubagentOptions, where the existing option-then-profile-then-parent precedence resolves them. /workflow model <alias> stores the choice for the session and passes it to the task as an instruction, so the agent can still pick something else when the work plainly calls for it.
1 parent cf5b6b1 commit f7f731e

13 files changed

Lines changed: 147 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: 39 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,43 @@ 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+(.*))?$/i.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+
host.setAppState({ dynamicWorkflowModel: value });
133+
host.showStatus(`Dynamic Workflow subagents will use ${value}.`);
134+
return true;
97135
}
98136

99137
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: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,4 +336,41 @@ describe('handleDynamicWorkflowCommand', () => {
336336
expect(markerAddChild(host)).not.toHaveBeenCalled();
337337
expect(host.sendNormalUserInput).not.toHaveBeenCalled();
338338
});
339+
340+
it('sets, reports, and clears the Dynamic Workflow subagent model', async () => {
341+
const { host, session } = makeHost({ permissionMode: 'auto' });
342+
343+
await handleDynamicWorkflowCommand(host, 'model');
344+
expect(host.showStatus).toHaveBeenLastCalledWith(
345+
expect.stringContaining('use this session model'),
346+
);
347+
348+
await handleDynamicWorkflowCommand(host, 'model deepseek-v4');
349+
expect(host.showStatus).toHaveBeenLastCalledWith('Dynamic Workflow subagents will use deepseek-v4.');
350+
351+
await handleDynamicWorkflowCommand(host, 'model');
352+
expect(host.showStatus).toHaveBeenLastCalledWith(
353+
expect.stringContaining('subagents use deepseek-v4'),
354+
);
355+
356+
await handleDynamicWorkflowCommand(host, 'model off');
357+
expect(host.showStatus).toHaveBeenLastCalledWith(
358+
'Dynamic Workflow subagents now use this session model.',
359+
);
360+
361+
// A model subcommand must never be mistaken for a task prompt.
362+
expect(session.setDynamicWorkflowMode).not.toHaveBeenCalled();
363+
expect(host.sendNormalUserInput).not.toHaveBeenCalled();
364+
});
365+
366+
it('asks the task to route subagents to the configured model', async () => {
367+
const { host } = makeHost({ permissionMode: 'auto' });
368+
369+
await handleDynamicWorkflowCommand(host, 'model deepseek-v4');
370+
await handleDynamicWorkflowCommand(host, 'Audit every route for missing auth');
371+
372+
expect(host.sendNormalUserInput).toHaveBeenCalledWith(
373+
'Audit every route for missing auth\n\nUse model "deepseek-v4" for the DynamicWorkflow subagents in this task.',
374+
);
375+
});
339376
});

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. Omit them to use the subagent type's own settings.
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)