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: 5 additions & 2 deletions src/cli/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ import { QuickJSRuntimeFactory } from "@/node/services/ptc/quickjsRuntime";
import { resolveWorkflowScript } from "@/node/services/workflows/workflowScriptResolver";
import { WorkflowRunStore } from "@/node/services/workflows/WorkflowRunStore";
import { WorkflowService } from "@/node/services/workflows/WorkflowService";
import { WorkflowTaskServiceAdapter } from "@/node/services/workflows/WorkflowTaskServiceAdapter";
import {
DEFAULT_WORKFLOW_AGENT_ID,
WorkflowTaskServiceAdapter,
} from "@/node/services/workflows/WorkflowTaskServiceAdapter";
import { hasAnyConfiguredProvider, buildProvidersFromEnv } from "@/node/utils/providerRequirements";
import { getParseOptions } from "./argv";
import { exitAfterStdoutFlush } from "./processExit";
Expand Down Expand Up @@ -371,7 +374,7 @@ function createWorkflowService(input: {
taskService: input.ctx.services.taskService,
parentWorkspaceId: input.ctx.workspaceId,
workflowRunId: runId,
defaultAgentId: "explore",
defaultAgentId: DEFAULT_WORKFLOW_AGENT_ID,
experiments,
modelString: input.model,
thinkingLevel: input.thinkingLevel,
Expand Down
2 changes: 1 addition & 1 deletion src/node/builtinSkills/deep-research.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ workflow_run({
});
```

The workflow scopes search angles, searches and fetches sources, extracts falsifiable claims, verifies claims adversarially, and synthesizes a cited report with caveats.
The workflow scopes search angles, searches and fetches sources, extracts falsifiable claims, verifies claims adversarially with exec agents using their configured defaults, and synthesizes a cited report with caveats.
9 changes: 9 additions & 0 deletions src/node/builtinSkills/deep-research/workflow.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ const MAX_VERIFY_CLAIMS = 25;
const MAX_PARALLEL_FETCH = 5;
const MAX_PARALLEL_VERIFY = 12;

const EXPLORE_AGENT = "explore";
const EXEC_AGENT = "exec";

const SCOPE_SCHEMA = {
type: "object",
required: ["question", "summary", "angles"],
Expand Down Expand Up @@ -150,6 +153,7 @@ export default function workflow({ args, phase, log, agent, parallel, pipeline }
const scope = agent(buildScopePrompt(question), {
id: "scope",
title: "Scope research angles",
agentId: EXPLORE_AGENT,
schema: SCOPE_SCHEMA,
});
const angles = scope.angles.slice(0, 6);
Expand All @@ -167,6 +171,7 @@ export default function workflow({ args, phase, log, agent, parallel, pipeline }
agent(buildSearchPrompt(question, angle), {
id: stableId("search", angleIndex, angle.label),
title: "Search: " + angle.label,
agentId: EXPLORE_AGENT,
schema: SEARCH_SCHEMA,
}),
(searchResult, angleIndex) => {
Expand Down Expand Up @@ -199,6 +204,7 @@ export default function workflow({ args, phase, log, agent, parallel, pipeline }
agent(buildFetchPrompt(question, source, angle.label), {
id: stableId("fetch", angleIndex + "-" + sourceIndex, hostFromUrl(source.url)),
title: "Fetch: " + hostFromUrl(source.url),
agentId: EXPLORE_AGENT,
schema: EXTRACT_SCHEMA,
})
),
Expand Down Expand Up @@ -260,6 +266,8 @@ export default function workflow({ args, phase, log, agent, parallel, pipeline }
agent(buildVerifyPrompt(question, spec.claim, spec.voteIndex), {
id: stableId("verify", spec.claimIndex + "-" + spec.voteIndex, spec.claim.claim),
title: "Verify claim " + (spec.claimIndex + 1) + "." + (spec.voteIndex + 1),
agentId: EXEC_AGENT,
onRefusal: "fail",
schema: VERDICT_SCHEMA,
})
),
Expand Down Expand Up @@ -289,6 +297,7 @@ export default function workflow({ args, phase, log, agent, parallel, pipeline }
const report = agent(buildSynthesisPrompt(question, confirmed, killed), {
id: "synthesize",
title: "Synthesize research report",
agentId: EXEC_AGENT,
schema: REPORT_SCHEMA,
});

Expand Down
2 changes: 1 addition & 1 deletion src/node/builtinSkills/workflow-authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ Required options:
- `id`: stable step ID used for replay; never derive from unstable ordering unless the input ordering is stable.
- `schema`: optional JSON object schema. When present, the child reports schema-shaped data through `agent_report` and `agent()` returns that structured object directly. When omitted, `agent()` returns the child report markdown string.

Optional fields include `title`, `agentType`, `isolation`, and `onRefusal`. `model` and `effort` are rejected for now instead of being silently ignored.
Workflow agents default to `exec`. Optional fields include `title`, `agentId` (preferred), legacy `agentType`, `model`, `thinking`, `isolation`, and `onRefusal`. Use `agentId: "explore"` for read-only research/discovery stages. `model` accepts the same aliases/full model strings as the UI, `thinking` accepts `off|low|medium|high|xhigh|max` or a numeric index, and `effort` is rejected to avoid ambiguous provider-specific behavior.

```js
const scope = agent("Scope this topic", {
Expand Down
7 changes: 5 additions & 2 deletions src/node/orpc/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,10 @@ import {
WorkflowService,
type WorkflowBackgroundRunTerminalEvent,
} from "@/node/services/workflows/WorkflowService";
import { WorkflowTaskServiceAdapter } from "@/node/services/workflows/WorkflowTaskServiceAdapter";
import {
DEFAULT_WORKFLOW_AGENT_ID,
WorkflowTaskServiceAdapter,
} from "@/node/services/workflows/WorkflowTaskServiceAdapter";
import { WorkflowArgsValidationError } from "@/node/services/workflows/workflowArgs";
import { resolveWorkflowScript } from "@/node/services/workflows/workflowScriptResolver";
import { isProjectTrusted } from "@/node/utils/projectTrust";
Expand Down Expand Up @@ -396,7 +399,7 @@ export async function resolveWorkflowContext(
parentWorkspaceId: workspaceId,
workflowRunId: runId,
workflowName,
defaultAgentId: "explore",
defaultAgentId: DEFAULT_WORKFLOW_AGENT_ID,
patchToolConfig: {
workspaceId,
cwd: workspacePath,
Expand Down
13 changes: 11 additions & 2 deletions src/node/services/agentSkills/builtInSkillContent.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const BUILTIN_SKILL_FILES: Record<string, Record<string, string>> = {
"});",
"```",
"",
"The workflow scopes search angles, searches and fetches sources, extracts falsifiable claims, verifies claims adversarially, and synthesizes a cited report with caveats.",
"The workflow scopes search angles, searches and fetches sources, extracts falsifiable claims, verifies claims adversarially with exec agents using their configured defaults, and synthesizes a cited report with caveats.",
"",
].join("\n"),
"workflow.js": [
Expand Down Expand Up @@ -56,6 +56,9 @@ export const BUILTIN_SKILL_FILES: Record<string, Record<string, string>> = {
"const MAX_PARALLEL_FETCH = 5;",
"const MAX_PARALLEL_VERIFY = 12;",
"",
'const EXPLORE_AGENT = "explore";',
'const EXEC_AGENT = "exec";',
"",
"const SCOPE_SCHEMA = {",
' type: "object",',
' required: ["question", "summary", "angles"],',
Expand Down Expand Up @@ -179,6 +182,7 @@ export const BUILTIN_SKILL_FILES: Record<string, Record<string, string>> = {
" const scope = agent(buildScopePrompt(question), {",
' id: "scope",',
' title: "Scope research angles",',
" agentId: EXPLORE_AGENT,",
" schema: SCOPE_SCHEMA,",
" });",
" const angles = scope.angles.slice(0, 6);",
Expand All @@ -196,6 +200,7 @@ export const BUILTIN_SKILL_FILES: Record<string, Record<string, string>> = {
" agent(buildSearchPrompt(question, angle), {",
' id: stableId("search", angleIndex, angle.label),',
' title: "Search: " + angle.label,',
" agentId: EXPLORE_AGENT,",
" schema: SEARCH_SCHEMA,",
" }),",
" (searchResult, angleIndex) => {",
Expand Down Expand Up @@ -228,6 +233,7 @@ export const BUILTIN_SKILL_FILES: Record<string, Record<string, string>> = {
" agent(buildFetchPrompt(question, source, angle.label), {",
' id: stableId("fetch", angleIndex + "-" + sourceIndex, hostFromUrl(source.url)),',
' title: "Fetch: " + hostFromUrl(source.url),',
" agentId: EXPLORE_AGENT,",
" schema: EXTRACT_SCHEMA,",
" })",
" ),",
Expand Down Expand Up @@ -289,6 +295,8 @@ export const BUILTIN_SKILL_FILES: Record<string, Record<string, string>> = {
" agent(buildVerifyPrompt(question, spec.claim, spec.voteIndex), {",
' id: stableId("verify", spec.claimIndex + "-" + spec.voteIndex, spec.claim.claim),',
' title: "Verify claim " + (spec.claimIndex + 1) + "." + (spec.voteIndex + 1),',
" agentId: EXEC_AGENT,",
' onRefusal: "fail",',
" schema: VERDICT_SCHEMA,",
" })",
" ),",
Expand Down Expand Up @@ -318,6 +326,7 @@ export const BUILTIN_SKILL_FILES: Record<string, Record<string, string>> = {
" const report = agent(buildSynthesisPrompt(question, confirmed, killed), {",
' id: "synthesize",',
' title: "Synthesize research report",',
" agentId: EXEC_AGENT,",
" schema: REPORT_SCHEMA,",
" });",
"",
Expand Down Expand Up @@ -7667,7 +7676,7 @@ export const BUILTIN_SKILL_FILES: Record<string, Record<string, string>> = {
"- `id`: stable step ID used for replay; never derive from unstable ordering unless the input ordering is stable.",
"- `schema`: optional JSON object schema. When present, the child reports schema-shaped data through `agent_report` and `agent()` returns that structured object directly. When omitted, `agent()` returns the child report markdown string.",
"",
"Optional fields include `title`, `agentType`, `isolation`, and `onRefusal`. `model` and `effort` are rejected for now instead of being silently ignored.",
'Workflow agents default to `exec`. Optional fields include `title`, `agentId` (preferred), legacy `agentType`, `model`, `thinking`, `isolation`, and `onRefusal`. Use `agentId: "explore"` for read-only research/discovery stages. `model` accepts the same aliases/full model strings as the UI, `thinking` accepts `off|low|medium|high|xhigh|max` or a numeric index, and `effort` is rejected to avoid ambiguous provider-specific behavior.',
"",
"```js",
'const scope = agent("Scope this topic", {',
Expand Down
7 changes: 5 additions & 2 deletions src/node/services/aiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,10 @@ import {
WorkflowService,
type WorkflowRunStatusChangedEvent,
} from "@/node/services/workflows/WorkflowService";
import { WorkflowTaskServiceAdapter } from "@/node/services/workflows/WorkflowTaskServiceAdapter";
import {
DEFAULT_WORKFLOW_AGENT_ID,
WorkflowTaskServiceAdapter,
} from "@/node/services/workflows/WorkflowTaskServiceAdapter";
import { resolveWorkflowScript } from "@/node/services/workflows/workflowScriptResolver";
import { isProjectTrusted } from "@/node/utils/projectTrust";

Expand Down Expand Up @@ -1782,7 +1785,7 @@ export class AIService extends EventEmitter {
parentWorkspaceId: workspaceId,
workflowRunId: runId,
workflowName,
defaultAgentId: "explore",
defaultAgentId: DEFAULT_WORKFLOW_AGENT_ID,
patchToolConfig: {
workspaceId,
cwd: workspacePath,
Expand Down
44 changes: 44 additions & 0 deletions src/node/services/workflows/WorkflowRunner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,50 @@ describe("WorkflowRunner", () => {
expect(seenSpecs[1]).toMatchObject({ id: "markdown" });
});

test("new agent API maps agent, model, and thinking options", async () => {
using tmp = new DisposableTempDir("workflow-runner-agent-options");
const store = new WorkflowRunStore({
sessionDir: tmp.path,
staleLeaseMs: WORKFLOW_RUNNER_TEST_STALE_LEASE_MS,
});
await store.createRun({
id: "wfr_agent_options",
workspaceId: "workspace-1",
workflow: definition,
source: `export default function workflow({ agent }) {
const result = agent("Verify claim", {
id: "verify",
agentType: "exec",
model: "fable",
thinking: "high",
});
return { reportMarkdown: result };
}
`,
args: {},
now: "2026-05-29T00:00:00.000Z",
});
const seenSpecs: WorkflowAgentSpec[] = [];
const runner = createRunner(store, {
async runAgent(spec) {
seenSpecs.push(spec);
return { taskId: "task_verify", reportMarkdown: "verified", structuredOutput: {} };
},
});

await expect(runner.run("wfr_agent_options")).resolves.toEqual({
reportMarkdown: "verified",
});
expect(seenSpecs).toEqual([
expect.objectContaining({
id: "verify",
agentId: "exec",
modelString: "anthropic:claude-fable-5",
thinkingLevel: "high",
}),
]);
});

test("parallel runs agent thunks concurrently and returns ordered schema outputs", async () => {
using tmp = new DisposableTempDir("workflow-runner-parallel-api");
const store = new WorkflowRunStore({
Expand Down
58 changes: 54 additions & 4 deletions src/node/services/workflows/WorkflowRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type {
WorkflowRunEvent,
WorkflowStepRecord,
} from "@/common/types/workflow";
import { parseThinkingInput, type ParsedThinkingInput } from "@/common/types/thinking";
import { normalizeModelInput } from "@/common/utils/ai/normalizeModelInput";
import assert from "@/common/utils/assert";
import { getErrorMessage } from "@/common/utils/errors";
import {
Expand Down Expand Up @@ -38,6 +40,8 @@ export interface WorkflowAgentSpec {
prompt: string;
title?: string;
agentId?: string;
modelString?: string;
thinkingLevel?: ParsedThinkingInput;
isolation?: "fork" | "none";
outputSchema?: unknown;
/** Internal marker for new `agent(prompt, { id })` prose-only steps. */
Expand Down Expand Up @@ -2302,6 +2306,39 @@ function parseStartedWorkflowAgentHandle(rawHandle: unknown): StartedWorkflowAge
return { handleId };
}

function parseWorkflowAgentModelString(rawValue: unknown): string | undefined {
if (rawValue === undefined) {
return undefined;
}
assert(
typeof rawValue === "string" && rawValue.trim().length > 0,
"agent model must be a non-empty string"
);
const normalized = normalizeModelInput(rawValue);
assert(
normalized.model != null,
`agent model "${rawValue}" must be a known alias or provider:model string`
);
return normalized.model;
}

function parseWorkflowAgentThinkingLevel(rawValue: unknown): ParsedThinkingInput | undefined {
if (rawValue === undefined) {
return undefined;
}
const value = typeof rawValue === "number" ? String(rawValue) : rawValue;
assert(
typeof value === "string" && value.trim().length > 0,
"agent thinking must be a non-empty string or numeric index"
);
const parsed = parseThinkingInput(value);
assert(
parsed !== undefined,
"agent thinking must be one of off, low, medium, high, xhigh, max, or a numeric index"
);
return parsed;
}

function parseWorkflowAgentSpec(
rawSpec: unknown,
options: { allowMissingOutputSchema: boolean }
Expand All @@ -2323,6 +2360,14 @@ function parseWorkflowAgentSpec(
if (typeof spec.agentId === "string" && spec.agentId.length > 0) {
parsed.agentId = spec.agentId;
}
const modelString = parseWorkflowAgentModelString(spec.modelString);
if (modelString !== undefined) {
parsed.modelString = modelString;
}
const thinkingLevel = parseWorkflowAgentThinkingLevel(spec.thinkingLevel);
if (thinkingLevel !== undefined) {
parsed.thinkingLevel = thinkingLevel;
}
if (spec.isolation !== undefined) {
assert(
spec.isolation === "fork" || spec.isolation === "none",
Expand Down Expand Up @@ -2540,13 +2585,18 @@ function __muxAgent(prompt, options) {
if (typeof options.id !== "string" || options.id.length === 0) {
throw new Error("agent replay boundary requires a stable id");
}
if (Object.prototype.hasOwnProperty.call(options, "model")) {
throw new Error("agent options.model is not supported yet");
}
if (Object.prototype.hasOwnProperty.call(options, "effort")) {
throw new Error("agent options.effort is not supported yet");
throw new Error("agent options.effort is not supported; use options.thinking");
}
const spec = { ...options, prompt };
if (Object.prototype.hasOwnProperty.call(spec, "model")) {
spec.modelString = spec.model;
delete spec.model;
}
if (Object.prototype.hasOwnProperty.call(spec, "thinking")) {
spec.thinkingLevel = spec.thinking;
delete spec.thinking;
}
if (Object.prototype.hasOwnProperty.call(spec, "schema")) {
spec.outputSchema = spec.schema;
delete spec.schema;
Expand Down
Loading
Loading