Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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.

11 changes: 9 additions & 2 deletions src/common/utils/tools/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,14 @@ export interface ToolConfiguration {
run: unknown;
}) => Promise<void> | void;
}): Promise<{ runId: string; status: string; result: unknown }>;
interruptRun?(input: { workspaceId: string; runId: string }): Promise<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;
Expand Down Expand Up @@ -765,7 +772,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