Skip to content
Open
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
2 changes: 1 addition & 1 deletion docs/agents/system-prompt.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ When the user asks for "best of n" work, assume they want the \`task\` tool's \`
Before spawning the batch, do a small amount of preliminary analysis to capture shared context, constraints, or evaluation criteria that would otherwise be repeated by every child.
Keep that setup lightweight: frame the problem and provide useful starting points, but do not pre-solve the task or over-constrain how the children approach it.
Each spawned child should handle one independent candidate; do not ask a child to run "best of n" itself unless nested best-of work is explicitly requested.
Picking the best candidate requires every report, so await the full batch (pass \`task_await\` \`min_completed\` equal to the batch size, or use a foreground grouped spawn) before selecting — but you may start setup-only work (e.g. preparing the evaluation rubric or integration scaffolding) as soon as the first candidate lands.
Picking the best candidate requires every report, so await the full batch with \`task_await({ task_ids: result.taskIds, min_completed: result.taskIds.length })\` (or use a foreground grouped spawn) before selecting — but you may start setup-only work (e.g. preparing the evaluation rubric or integration scaffolding) as soon as the first candidate lands.
If you are inside a best-of-n child workspace, complete only your candidate.
</best-of-n>

Expand Down
42 changes: 21 additions & 21 deletions docs/hooks/tools.mdx

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/cli/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,8 @@ function createWorkflowService(input: {
return new WorkflowService({
runStore: new WorkflowRunStore({ sessionDir: workspaceSessionDir }),
runtimeFactory: new QuickJSRuntimeFactory(),
withRunStartLock: (ownerWorkspaceId, operation) =>
input.ctx.services.taskService.withWorkspaceOwnedWorkStartLock(ownerWorkspaceId, operation),
taskAdapterFactory: (runId) =>
new WorkflowTaskServiceAdapter({
taskService: input.ctx.services.taskService,
Expand All @@ -445,6 +447,8 @@ function createWorkflowService(input: {
experiments,
modelString: input.model,
thinkingLevel: input.thinkingLevel,
cleanupWorkspaceBackgroundProcesses: (taskWorkspaceId) =>
input.ctx.services.backgroundProcessManager.cleanup(taskWorkspaceId),
getProjectTrusted: () => input.ctx.projectTrusted,
patchToolConfig: {
workspaceId: input.ctx.workspaceId,
Expand Down
45 changes: 23 additions & 22 deletions src/common/utils/tools/toolDefinitions.ts

Large diffs are not rendered by default.

119 changes: 65 additions & 54 deletions src/common/utils/tools/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,67 @@ export interface WorkflowServiceScriptInput {
sourceKind: "skill" | "workspace-file" | "inline";
}

export interface ToolWorkflowService {
getRun?(input: { workspaceId: string; runId: string }): Promise<unknown>;
listRuns?(input: { workspaceId: string }): Promise<unknown[]>;
startWorkflowInBackground?(input: {
script: WorkflowServiceScriptInput;
workspaceId: string;
projectTrusted: boolean;
args: unknown;
attentionPolicy?: BackgroundWorkAttentionPolicy;
onRunCreated?: (event: {
runId: string;
status: "pending";
result: null;
run: unknown;
}) => Promise<void> | void;
}): Promise<{ runId: string; status: string; result: unknown }>;
startWorkflow?(input: {
script: WorkflowServiceScriptInput;
workspaceId: string;
projectTrusted: boolean;
args: unknown;
abortSignal?: AbortSignal;
onRunCreated?: (event: {
runId: string;
status: "pending";
result: null;
run: unknown;
}) => Promise<void> | void;
}): Promise<{ runId: string; status: string; result: unknown }>;
interruptRun?(input: {
workspaceId: string;
runId: string;
deferTaskSweep?: boolean;
lockAlreadyHeld?: boolean;
retryTaskCleanup?: boolean;
onRunInterrupted?: (runId: string) => void;
}): Promise<unknown>;
resumeRun?(input: {
workspaceId: string;
runId: string;
projectTrusted: boolean;
abortSignal?: AbortSignal;
}): Promise<{ runId: string; status: string; result: unknown }>;
resumeRunInBackground?(input: {
workspaceId: string;
runId: string;
projectTrusted: boolean;
}): Promise<{ runId: string; status: string; result: unknown }>;
retryRunFromCheckpoint?(input: {
workspaceId: string;
runId: string;
projectTrusted: boolean;
abortSignal?: AbortSignal;
}): Promise<{ runId: string; status: string; result: unknown }>;
retryRunFromCheckpointInBackground?(input: {
workspaceId: string;
runId: string;
projectTrusted: boolean;
}): Promise<{ runId: string; status: string; result: unknown }>;
}

export interface ToolConfiguration {
/** Working directory for command execution - actual path in runtime's context (local or remote) */
cwd: string;
Expand Down Expand Up @@ -202,59 +263,9 @@ export interface ToolConfiguration {
/** Task orchestration for sub-agent tasks */
taskService?: TaskService;
/** Durable workflow lifecycle service for dynamic workflow tools. */
workflowService?: {
getRun?(input: { workspaceId: string; runId: string }): Promise<unknown>;
listRuns?(input: { workspaceId: string }): Promise<unknown[]>;
startWorkflowInBackground?(input: {
script: WorkflowServiceScriptInput;
workspaceId: string;
projectTrusted: boolean;
args: unknown;
attentionPolicy?: BackgroundWorkAttentionPolicy;
onRunCreated?: (event: {
runId: string;
status: "pending";
result: null;
run: unknown;
}) => Promise<void> | void;
}): Promise<{ runId: string; status: string; result: unknown }>;
startWorkflow?(input: {
script: WorkflowServiceScriptInput;
workspaceId: string;
projectTrusted: boolean;
args: unknown;
abortSignal?: AbortSignal;
onRunCreated?: (event: {
runId: string;
status: "pending";
result: null;
run: unknown;
}) => Promise<void> | void;
}): Promise<{ runId: string; status: string; result: unknown }>;
interruptRun?(input: { workspaceId: string; runId: string }): Promise<unknown>;
resumeRun?(input: {
workspaceId: string;
runId: string;
projectTrusted: boolean;
abortSignal?: AbortSignal;
}): Promise<{ runId: string; status: string; result: unknown }>;
resumeRunInBackground?(input: {
workspaceId: string;
runId: string;
projectTrusted: boolean;
}): Promise<{ runId: string; status: string; result: unknown }>;
retryRunFromCheckpoint?(input: {
workspaceId: string;
runId: string;
projectTrusted: boolean;
abortSignal?: AbortSignal;
}): Promise<{ runId: string; status: string; result: unknown }>;
retryRunFromCheckpointInBackground?(input: {
workspaceId: string;
runId: string;
projectTrusted: boolean;
}): Promise<{ runId: string; status: string; result: unknown }>;
};
workflowService?: ToolWorkflowService;
/** Resolve the workflow lifecycle service for a descendant workspace's session. */
workflowServiceForWorkspace?: (workspaceId: string) => ToolWorkflowService | null;
/** Workspace heartbeat settings service for model-facing heartbeat configuration. */
workspaceHeartbeatService?: WorkspaceHeartbeatToolService;
/** Workspace goal lifecycle service for model-facing goal tools. */
Expand Down Expand Up @@ -765,7 +776,7 @@ export async function getToolsForModel(
task_remove: wrap(createTaskRemoveTool(config)),
task_list: wrap(createTaskListTool(config)),

// Bash execution (foreground/background). Manage background output via task_await/task_list/task_terminate.
// Bash execution (foreground/background). Manage background output via task_await/task_list/task_stop.
bash: wrap(createBashTool(config)),

// Legacy bash process tools (deprecated)
Expand Down
2 changes: 1 addition & 1 deletion src/node/builtinSkills/orchestrate.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ In a workflow, the verifier becomes `agent(prompt, { id, schema, onRefusal: "fai
## Sequential protocol (only for dependency chains)

1. Spawn the prerequisite `exec` implementation task with `run_in_background: false`.
2. If step 1 returns `queued`/`running` without a completed report, call `task_await` with the returned `taskId` before attempting any patch apply. If step 1 returns `status: completed` inline, that same `taskId` still requires patch application.
2. If step 1 returns `queued`/`running` without a completed report, call `task_await({ task_ids: [result.taskId] })` before attempting any patch apply. If step 1 returns `status: completed` inline, that same `taskId` still requires patch application.
3. Dry-run apply its patch (`dry_run: true`); then apply for real (`dry_run: false`). If either step fails, follow the conflict playbook above (including `git am --abort` only when a real apply leaves a git-am session in progress).
4. Only then spawn the dependent task.

Expand Down
14 changes: 13 additions & 1 deletion src/node/orpc/router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ describe("router workflow routes", () => {
config,
aiService: {
waitForInit: mock(async () => undefined),
cleanupWorkspaceBackgroundProcesses: mock(async () => undefined),
getWorkspaceMetadata: mock(async () => ({
success: true,
data: {
Expand All @@ -149,7 +150,13 @@ describe("router workflow routes", () => {
getWorkflowContinuationSendOptions: mock(() => null),
sendMessage: mock(async () => ({ success: true, data: undefined })),
},
taskService: {},
taskService: {
withWorkspaceOwnedWorkStartLock: mock(
async <T>(_workspaceId: string, operation: () => Promise<T>) => await operation()
),
terminateAllDescendantAgentTasks: mock(async () => []),
markWorkflowRunEnded: mock(async () => undefined),
},
experimentsService: {
isExperimentEnabled: mock(() => options.enabled),
},
Expand Down Expand Up @@ -616,6 +623,9 @@ export default function workflow() { return { reportMarkdown: "should not run" }

let waitCalls = 0;
context.taskService = {
withWorkspaceOwnedWorkStartLock: mock(
async <T>(_workspaceId: string, operation: () => Promise<T>) => await operation()
),
create: mock(async () => ({ success: true, data: { taskId: "task_slow" } })),
waitForAgentReport: mock(async () => {
waitCalls += 1;
Expand All @@ -624,6 +634,8 @@ export default function workflow() { return { reportMarkdown: "should not run" }
}
return { reportMarkdown: "done", structuredOutput: {} };
}),
terminateAllDescendantAgentTasks: mock(async () => []),
markWorkflowRunEnded: mock(async () => undefined),
} as unknown as ORPCContext["taskService"];

const client = createRouterClient(router(), { context });
Expand Down
4 changes: 4 additions & 0 deletions src/node/orpc/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,8 @@ export async function resolveWorkflowContext(
options.notifyInterruptedBackgroundRunTerminal === true,
runStore: new WorkflowRunStore({ sessionDir: context.config.getSessionDir(workspaceId) }),
runtimeFactory: context.workflowRuntimeFactory,
withRunStartLock: (ownerWorkspaceId, operation) =>
context.taskService.withWorkspaceOwnedWorkStartLock(ownerWorkspaceId, operation),
taskAdapterFactory: (runId, workflowName) =>
new WorkflowTaskServiceAdapter({
taskService: context.taskService,
Expand All @@ -472,6 +474,8 @@ export async function resolveWorkflowContext(
workspaceSessionDir: context.config.getSessionDir(workspaceId),
trusted: projectTrusted,
},
cleanupWorkspaceBackgroundProcesses: (taskWorkspaceId) =>
context.aiService.cleanupWorkspaceBackgroundProcesses(taskWorkspaceId),
getProjectTrusted: resolveWorkflowProjectTrusted,
experiments: {
dynamicWorkflows: true,
Expand Down
Loading
Loading