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