From e6af3bbba581608c5ed10a99ff1d12accee8da10 Mon Sep 17 00:00:00 2001 From: Janni Turunen Date: Thu, 12 Feb 2026 09:51:25 +0200 Subject: [PATCH 1/2] feat(opencode): add sync mode to task tool (#182) --- .fork-features/manifest.json | 9 ++- packages/opencode/src/session/prompt.ts | 1 + packages/opencode/src/tool/task.ts | 87 ++++++++++++++++++++++++ packages/opencode/src/tool/task.txt | 35 +++++++++- packages/opencode/test/tool/task.test.ts | 45 ++++++++++++ 5 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 packages/opencode/test/tool/task.test.ts diff --git a/.fork-features/manifest.json b/.fork-features/manifest.json index 3bf1926e07d2..309a97a07f14 100644 --- a/.fork-features/manifest.json +++ b/.fork-features/manifest.json @@ -9,7 +9,7 @@ "features": { "async-tasks": { "status": "active", - "description": "Background task execution with slot-based concurrency (max 5). Enables async agent orchestration via /task and /check_task tools.", + "description": "Background task execution with slot-based concurrency (max 5). Supports both async and sync modes. Enables agent orchestration via /task and /check_task tools.", "issue": "https://github.com/randomm/opencode/issues/159", "newFiles": [ "packages/opencode/src/session/async-tasks.ts", @@ -28,13 +28,16 @@ "TaskTool", "CheckTaskTool", "Agent.get(task.agent)", - "parent_session_id" + "parent_session_id", + "sync", + "sync mode execution", + "parameters.sync" ], "tests": ["packages/opencode/test/tool/check_task.test.ts", "packages/opencode/test/session/async-tasks.test.ts"], "upstreamTracking": { "relatedPRs": ["anomalyco/opencode#7206"], "relatedIssues": ["anomalyco/opencode#5887"], - "absorptionSignals": ["backgroundTaskResults", "BackgroundTask", "task.*concurrency.*slot"] + "absorptionSignals": ["backgroundTaskResults", "BackgroundTask", "task.*concurrency.*slot", "sync.*mode"] } }, "rg-tool": { diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index fd35af14e1d2..0746ffa1854d 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -378,6 +378,7 @@ export namespace SessionPrompt { description: task.description, subagent_type: task.agent, command: task.command, + sync: false, } await Plugin.trigger( "tool.execute.before", diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index b0913531079a..ea1abc109c3e 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -26,6 +26,7 @@ const parameters = z.object({ subagent_type: z.string().describe("The type of specialized agent to use for this task"), session_id: z.string().describe("Existing Task session to continue").optional(), command: z.string().describe("The command that triggered this task").optional(), + sync: z.boolean().describe("Execute synchronously and wait for result").optional(), }) const MAX_CONCURRENT_TASKS_PER_SESSION = 5 @@ -296,6 +297,92 @@ export const TaskTool = Tool.define("task", async (initCtx) => { release_slot: result.releaseSlot, } + const taskTimeoutMs = 10 * 60 * 1000 + const syncAbortController = new AbortController() + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + SessionPrompt.cancel(session.id) + syncAbortController.abort() + reject(new Error("Task timeout after 10 minutes")) + }, taskTimeoutMs) + }) + + // Check for abort before sync execution to prevent race condition + if (ctx.abort.aborted) { + unsub() + if (!slotReleased && result.releaseSlot) { + result.releaseSlot() + slotReleased = true + } + return { + title: params.description, + output: JSON.stringify({ + task_id: taskId, + status: "aborted", + message: "Task aborted before execution", + }), + metadata: { sessionId: session.id } as TaskResultMetadata, + } + } + + // Sync mode: execute synchronously and return result directly + if (params.sync) { + try { + const promptResult = await Promise.race([ + SessionPrompt.prompt({ + messageID, + sessionID: session.id, + model: { + modelID: model.modelID, + providerID: model.providerID, + }, + agent: agent.name, + tools: { + todowrite: false, + todoread: false, + ...(hasTaskPermission ? {} : { task: false }), + ...Object.fromEntries((config.experimental?.primary_tools ?? []).map((t) => [t, false])), + }, + parts: promptParts, + }), + timeoutPromise, + ]) + const textPart = promptResult.parts.find((p) => p.type === "text" && !p.synthetic) + const textResult = textPart && "text" in textPart ? textPart.text : undefined + if (result.releaseSlot) { + result.releaseSlot() + slotReleased = true + } + return { + title: params.description, + output: JSON.stringify({ + task_id: taskId, + status: "completed", + result: textResult, + }), + metadata: { sessionId: session.id } as TaskResultMetadata, + } + } catch (e) { + if (!slotReleased && result.releaseSlot) { + result.releaseSlot() + slotReleased = true + } + const errorMessage = e instanceof Error ? e.message : String(e) + return { + title: params.description, + output: JSON.stringify({ + task_id: taskId, + status: "failed", + error: errorMessage, + }), + metadata: { sessionId: session.id } as TaskResultMetadata, + } + } finally { + unsub() + } + } + + // Async mode: spawn background task enableAutoWakeup(ctx.sessionID) try { diff --git a/packages/opencode/src/tool/task.txt b/packages/opencode/src/tool/task.txt index 585cce8f9d0a..f5bd80f67c15 100644 --- a/packages/opencode/src/tool/task.txt +++ b/packages/opencode/src/tool/task.txt @@ -5,6 +5,8 @@ Available agent types and the tools they have access to: When using the Task tool, you must specify a subagent_type parameter to select which agent type to use. +sync: A boolean indicating whether to execute the task synchronously. When true, the task runs immediately and returns the result directly. When false (default), the task runs asynchronously and returns a task_id that can be checked with /check_task. + When to use the Task tool: - When you are instructed to execute custom slash commands. Use the Task tool with the slash command invocation as the entire prompt. The slash command can take arguments. For example: Task(description="Check the file", prompt="/check-file path/to/file.py") @@ -16,7 +18,38 @@ When NOT to use the Task tool: Usage notes: -1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses + 1. Launch multiple agents并发 agents whenever possible, to maximize performance; to do that, use a single message with multiple tool uses + 2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result. The output includes a task_id you can reuse later to continue the same subagent session. + 3. Each agent invocation starts with a new context unless you provide task_id to resume the same subagent session (which continues with its previous messages and tool outputs). When starting fresh, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you. + 4. The agent's outputs should generally be trusted + 5. Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent. Tell it how to verify its work if possible (e.g., relevant test commands). + 6. If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement. + + Parameters: + - `description`: A short (3-5 words) description of the task + - `prompt`: The task for the agent to perform + - `subagent_type`: The type of specialized agent to use for this task +- `session_id`: (optional) Existing task session ID to continue the same subagent session + - `command`: (optional) The slash command that triggered this task + - `sync`: (Optional, default: `false`) Run task synchronously and wait for result. Default: true) returns status "started" and result is delivered later; sync: `true` returns result or error directly in the response output. + + ## Execution Modes + + ### Async Mode (default, sync: false) + Task runs in the background. Returns immediately with a task_id. Use /check_task to poll for completion. + + ### Sync Mode (sync: true) + Task runs synchronously and blocks until completion. Returns the result directly without using the event bus. Useful when you need the result immediately before continuing. + + Response Output: + 1. **Async mode (sync=false, default)**: Returns `{ task_id, status, "started", message: "Task dispatched to @agent" }` immediately. Task runs in background and results are delivered via event bus. + 2. **Sync mode (sync=true)**: Waits for the task to complete, returns `{ task: completion "...", status:"completed"./"failed"} }`: + - `{ task_id, status: "completed", result: "..." }` - Task completed successfully, text return is returned. + - `{ task_id: "started", "failed", message: "Task `timeout...}` `awaited: "Task timed out: `new Error(new: TimeoutExceeded())`) + * `}` + - `{ task_id: "started:", error: "Error message"}` - Task + execution failed + 3. Both modes respect the 5-task limit per session and 10-minute timeout 2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result. The output includes a task_id you can reuse later to continue the same subagent session. 3. Each agent invocation starts with a fresh context unless you provide task_id to resume the same subagent session (which continues with its previous messages and tool outputs). When starting fresh, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you. 4. The agent's outputs should generally be trusted diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts new file mode 100644 index 000000000000..24d747ce011f --- /dev/null +++ b/packages/opencode/test/tool/task.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, test } from "bun:test" +import { TaskTool } from "../../src/tool/task" +import { Instance } from "../../src/project/instance" +import { tmpdir } from "../fixture/fixture" + +const ctx = { + sessionID: "test-parent", + messageID: "msg-1", + callID: "", + agent: "test", + abort: AbortSignal.any([]), + metadata: () => {}, + ask: async () => {}, + extra: { bypassAgentCheck: true }, +} + +describe("tool.task", () => { + test("validates sync parameter", async () => { + await Instance.provide({ + directory: "/tmp/test", + fn: async () => { + const tool = await TaskTool.init() + const parameters = tool.parameters + + const valid1 = parameters.safeParse({ + description: "Test", + prompt: "Task", + subagent_type: "developer", + }) + expect(valid1.success).toBe(true) + + const valid2 = parameters.safeParse({ + description: "Test", + prompt: "Task", + subagent_type: "developer", + sync: true, + }) + expect(valid2.success).toBe(true) + + const invalid = parameters.safeParse({ sync: true }) + expect(invalid.success).toBe(false) + }, + }) + }) +}) From a34f104104af91eccde8ccdf7045c7f50d0c4861 Mon Sep 17 00:00:00 2001 From: Janni Turunen Date: Thu, 12 Feb 2026 09:55:55 +0200 Subject: [PATCH 2/2] docs(opencode): fix task tool documentation errors (#183) --- packages/opencode/src/tool/task.txt | 40 +++++++++++++++++------------ 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/opencode/src/tool/task.txt b/packages/opencode/src/tool/task.txt index f5bd80f67c15..972cb4e33888 100644 --- a/packages/opencode/src/tool/task.txt +++ b/packages/opencode/src/tool/task.txt @@ -18,7 +18,7 @@ When NOT to use the Task tool: Usage notes: - 1. Launch multiple agents并发 agents whenever possible, to maximize performance; to do that, use a single message with multiple tool uses + 1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses 2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result. The output includes a task_id you can reuse later to continue the same subagent session. 3. Each agent invocation starts with a new context unless you provide task_id to resume the same subagent session (which continues with its previous messages and tool outputs). When starting fresh, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you. 4. The agent's outputs should generally be trusted @@ -31,7 +31,7 @@ Usage notes: - `subagent_type`: The type of specialized agent to use for this task - `session_id`: (optional) Existing task session ID to continue the same subagent session - `command`: (optional) The slash command that triggered this task - - `sync`: (Optional, default: `false`) Run task synchronously and wait for result. Default: true) returns status "started" and result is delivered later; sync: `true` returns result or error directly in the response output. + - `sync`: (Optional, default: `false`) When true, runs synchronously and returns result directly. When false (default), returns immediately with task_id; use /check_task to poll for completion. ## Execution Modes @@ -41,20 +41,28 @@ Usage notes: ### Sync Mode (sync: true) Task runs synchronously and blocks until completion. Returns the result directly without using the event bus. Useful when you need the result immediately before continuing. - Response Output: - 1. **Async mode (sync=false, default)**: Returns `{ task_id, status, "started", message: "Task dispatched to @agent" }` immediately. Task runs in background and results are delivered via event bus. - 2. **Sync mode (sync=true)**: Waits for the task to complete, returns `{ task: completion "...", status:"completed"./"failed"} }`: - - `{ task_id, status: "completed", result: "..." }` - Task completed successfully, text return is returned. - - `{ task_id: "started", "failed", message: "Task `timeout...}` `awaited: "Task timed out: `new Error(new: TimeoutExceeded())`) - * `}` - - `{ task_id: "started:", error: "Error message"}` - Task - execution failed - 3. Both modes respect the 5-task limit per session and 10-minute timeout -2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result. The output includes a task_id you can reuse later to continue the same subagent session. -3. Each agent invocation starts with a fresh context unless you provide task_id to resume the same subagent session (which continues with its previous messages and tool outputs). When starting fresh, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you. -4. The agent's outputs should generally be trusted -5. Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent. Tell it how to verify its work if possible (e.g., relevant test commands). -6. If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement. +Response Output: + 1. **Async mode (sync=false, default)**: Returns immediately with task_id. + 2. **Sync mode (sync=true)**: Returns result or error directly. + + Examples: + + **Success (sync=true):** + ```json + { "task_id": "task_123", "status": "completed", "result": "Task output here" } + ``` + + **Failure (sync=true):** + ```json + { "task_id": "task_123", "status": "failed", "error": "Error message" } + ``` + + **Async (sync=false, default):** + ```json + { "task_id": "task_123", "status": "started", "message": "Task dispatched to @agent" } + ``` + +Note: Both modes respect the 5-task limit per session and 10-minute timeout. Example usage (NOTE: The agents below are fictional examples for illustration only - use the actual agents listed above):