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
1 change: 0 additions & 1 deletion packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ export const layer = Layer.effect(
cancel: (sessionID: SessionID) => cancel(sessionID),
resolvePromptParts: (template: string) => resolvePromptParts(template),
prompt: (input: PromptInput) => prompt(input).pipe(Effect.catch(Effect.die)),
loop: (input: LoopInput) => loop(input),
} satisfies TaskPromptOps
})

Expand Down
8 changes: 1 addition & 7 deletions packages/opencode/src/tool/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { GlobTool } from "./glob"
import { GrepTool } from "./grep"
import { ReadTool } from "./read"
import { TaskTool } from "./task"
import { TaskStatusTool } from "./task_status"
import { TodoWriteTool } from "./todo"
import { WebFetchTool } from "./webfetch"
import { WriteTool } from "./write"
Expand Down Expand Up @@ -53,7 +52,6 @@ import { Skill } from "../skill"
import { Permission } from "@/permission"
import { Reference } from "@/reference/reference"
import { BackgroundJob } from "@/background/job"
import { SessionStatus } from "@/session/status"
import { RuntimeFlags } from "@/effect/runtime-flags"

const log = Log.create({ service: "tool.registry" })
Expand Down Expand Up @@ -91,7 +89,6 @@ export const layer: Layer.Layer<
| Agent.Service
| Skill.Service
| Session.Service
| SessionStatus.Service
| BackgroundJob.Service
| Provider.Service
| Git.Service
Expand Down Expand Up @@ -119,7 +116,6 @@ export const layer: Layer.Layer<

const invalid = yield* InvalidTool
const task = yield* TaskTool
const taskStatus = yield* TaskStatusTool
const read = yield* ReadTool
const question = yield* QuestionTool
const todo = yield* TodoWriteTool
Expand Down Expand Up @@ -235,7 +231,6 @@ export const layer: Layer.Layer<
edit: Tool.init(edit),
write: Tool.init(writetool),
task: Tool.init(task),
task_status: Tool.init(taskStatus),
fetch: Tool.init(webfetch),
todo: Tool.init(todo),
search: Tool.init(websearch),
Expand All @@ -260,7 +255,6 @@ export const layer: Layer.Layer<
tool.edit,
tool.write,
tool.task,
...(flags.experimentalBackgroundSubagents ? [tool.task_status] : []),
tool.fetch,
tool.todo,
tool.search,
Expand Down Expand Up @@ -385,7 +379,7 @@ export const defaultLayer = Layer.suspend(() =>
Layer.provide(Skill.defaultLayer),
Layer.provide(Agent.defaultLayer),
Layer.provide(Session.defaultLayer),
Layer.provide(Layer.mergeAll(SessionStatus.defaultLayer, BackgroundJob.defaultLayer)),
Layer.provide(BackgroundJob.defaultLayer),
Layer.provide(Provider.defaultLayer),
Layer.provide(Layer.mergeAll(Git.defaultLayer, RepositoryCache.defaultLayer)),
Layer.provide(Reference.defaultLayer),
Expand Down
140 changes: 49 additions & 91 deletions packages/opencode/src/tool/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,36 @@ import * as Tool from "./tool"
import DESCRIPTION from "./task.txt"
import { ToolJsonSchema } from "./json-schema"
import { BackgroundJob } from "@/background/job"
import { Bus } from "@/bus"
import { Session } from "@/session/session"
import { SessionID, MessageID } from "../session/schema"
import { MessageV2 } from "../session/message-v2"
import { Agent } from "../agent/agent"
import { deriveSubagentSessionPermission } from "../agent/subagent-permissions"
import type { SessionPrompt } from "../session/prompt"
import { SessionStatus } from "@/session/status"
import { Config } from "@/config/config"
import { TuiEvent } from "@/cli/cmd/tui/event"
import { Cause, Effect, Exit, Option, Schema, Scope } from "effect"
import { Cause, Effect, Exit, Schema, Scope } from "effect"
import { EffectBridge } from "@/effect/bridge"
import { RuntimeFlags } from "@/effect/runtime-flags"

export interface TaskPromptOps {
cancel(sessionID: SessionID): Effect.Effect<void>
resolvePromptParts(template: string): Effect.Effect<SessionPrompt.PromptInput["parts"]>
prompt(input: SessionPrompt.PromptInput): Effect.Effect<MessageV2.WithParts>
loop(input: SessionPrompt.LoopInput): Effect.Effect<MessageV2.WithParts>
}

const id = "task"
const BACKGROUND_DESCRIPTION = [
"",
"",
[
"Background mode: background=true launches the subagent asynchronously.",
"Use task_status(task_id=..., wait=false) to poll, or wait=true to block until done.",
"Background mode: background=true launches the subagent asynchronously and returns immediately.",
"Foreground is the default; use it when you need the result before continuing.",
"Use background only for independent work that can run while you continue elsewhere.",
"You will be notified automatically when it finishes.",
].join(" "),
].join("\n")

const BaseParameters = Schema.Struct({
const BaseParameterFields = {
description: Schema.String.annotate({ description: "A short (3-5 words) description of the task" }),
prompt: Schema.String.annotate({ description: "The task for the agent to perform" }),
subagent_type: Schema.String.annotate({ description: "The type of specialized agent to use for this task" }),
Expand All @@ -42,40 +40,32 @@ const BaseParameters = Schema.Struct({
"This should only be set if you mean to resume a previous task (you can pass a prior task_id and the task will continue the same subagent session as before instead of creating a fresh one)",
}),
command: Schema.optional(Schema.String).annotate({ description: "The command that triggered this task" }),
})
}

const BaseParameters = Schema.Struct(BaseParameterFields)

export const Parameters = Schema.Struct({
description: Schema.String.annotate({ description: "A short (3-5 words) description of the task" }),
prompt: Schema.String.annotate({ description: "The task for the agent to perform" }),
subagent_type: Schema.String.annotate({ description: "The type of specialized agent to use for this task" }),
task_id: Schema.optional(Schema.String).annotate({
description:
"This should only be set if you mean to resume a previous task (you can pass a prior task_id and the task will continue the same subagent session as before instead of creating a fresh one)",
}),
command: Schema.optional(Schema.String).annotate({ description: "The command that triggered this task" }),
...BaseParameterFields,
background: Schema.optional(Schema.Boolean).annotate({
description: "When true, launch the subagent in the background and return immediately",
description: "Run the agent in the background. You will be notified when it completes.",
}),
})

function output(sessionID: SessionID, text: string) {
return [
`task_id: ${sessionID} (for resuming to continue this task if needed)`,
"",
"<task_result>",
text,
"</task_result>",
].join("\n")
return [`<task id="${sessionID}" state="completed">`, "<task_result>", text, "</task_result>", "</task>"].join(
"\n",
)
}

function backgroundOutput(sessionID: SessionID) {
return [
`task_id: ${sessionID} (for polling this task with task_status)`,
"state: running",
"",
`<task id="${sessionID}" state="running">`,
"<summary>Background task started</summary>",
"<task_result>",
"Background task started. Continue your current work and call task_status when you need the result.",
"Background task started. You will be notified automatically when it finishes; do not poll for progress.",
"Do not duplicate its work. Continue only with non-overlapping work, or stop if there is nothing else useful to do.",
"</task_result>",
"</task>",
].join("\n")
}

Expand All @@ -90,9 +80,14 @@ function backgroundMessage(input: {
input.state === "completed"
? `Background task completed: ${input.description}`
: `Background task failed: ${input.description}`
return [title, `task_id: ${input.sessionID}`, `state: ${input.state}`, "", `<${tag}>`, input.text, `</${tag}>`].join(
"\n",
)
return [
`<task id="${input.sessionID}" state="${input.state}">`,
`<summary>${title}</summary>`,
`<${tag}>`,
input.text,
`</${tag}>`,
"</task>",
].join("\n")
}

function errorText(error: unknown) {
Expand All @@ -105,11 +100,9 @@ export const TaskTool = Tool.define(
Effect.gen(function* () {
const agent = yield* Agent.Service
const background = yield* BackgroundJob.Service
const bus = yield* Bus.Service
const config = yield* Config.Service
const sessions = yield* Session.Service
const scope = yield* Scope.Scope
const status = yield* SessionStatus.Service
const flags = yield* RuntimeFlags.Service

const run = Effect.fn("TaskTool.execute")(function* (
Expand Down Expand Up @@ -141,9 +134,8 @@ export const TaskTool = Tool.define(
return yield* Effect.fail(new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`))
}

const taskID = params.task_id
const session = taskID
? yield* sessions.get(SessionID.make(taskID)).pipe(Effect.catchCause(() => Effect.succeed(undefined)))
const session = params.task_id
? yield* sessions.get(SessionID.make(params.task_id)).pipe(Effect.catchCause(() => Effect.succeed(undefined)))
: undefined
const parent = yield* sessions.get(ctx.sessionID)
const parentAgent = parent.agent
Expand Down Expand Up @@ -189,7 +181,6 @@ export const TaskTool = Tool.define(

const ops = ctx.extra?.promptOps as TaskPromptOps
if (!ops) return yield* Effect.fail(new Error("TaskTool requires promptOps in ctx.extra"))
const runCancel = yield* EffectBridge.make()

const runTask = Effect.fn("TaskTool.runTask")(function* () {
const parts = yield* ops.resolvePromptParts(params.prompt)
Expand All @@ -211,68 +202,34 @@ export const TaskTool = Tool.define(
return result.parts.findLast((item) => item.type === "text")?.text ?? ""
})

const resumeWhenIdle: (input: { userID: MessageID; state: "completed" | "error" }) => Effect.Effect<void> =
Effect.fn("TaskTool.resumeWhenIdle")(function* (input: { userID: MessageID; state: "completed" | "error" }) {
const latest = yield* sessions
.findMessage(ctx.sessionID, (item) => item.info.role === "user")
.pipe(Effect.orDie)
if (Option.isNone(latest)) return
if (latest.value.info.id !== input.userID) return
if ((yield* status.get(ctx.sessionID)).type !== "idle") {
yield* Effect.sleep("300 millis")
return yield* resumeWhenIdle(input)
}
yield* bus.publish(TuiEvent.ToastShow, {
title: input.state === "completed" ? "Background task complete" : "Background task failed",
message:
input.state === "completed"
? `Background task "${params.description}" finished. Resuming the main thread.`
: `Background task "${params.description}" failed. Resuming the main thread.`,
variant: input.state === "completed" ? "success" : "error",
duration: 5000,
})
yield* ops
.loop({ sessionID: ctx.sessionID })
.pipe(Effect.ignore, Effect.forkIn(scope, { startImmediately: true }))
})

const continueIfIdle = Effect.fn("TaskTool.continueIfIdle")(function* (input: {
userID: MessageID
state: "completed" | "error"
}) {
yield* resumeWhenIdle(input).pipe(Effect.ignore, Effect.forkIn(scope, { startImmediately: true }))
})

const inject = Effect.fn("TaskTool.injectBackgroundResult")(function* (
state: "completed" | "error",
text: string,
) {
const currentParent = yield* sessions.get(ctx.sessionID)
const message = yield* ops.prompt({
sessionID: ctx.sessionID,
noReply: true,
agent: currentParent.agent ?? ctx.agent,
parts: [
{
type: "text",
synthetic: true,
text: backgroundMessage({
sessionID: nextSession.id,
description: params.description,
state,
text,
}),
},
],
})
yield* continueIfIdle({ userID: message.info.id, state })
yield* ops
.prompt({
sessionID: ctx.sessionID,
agent: currentParent.agent ?? ctx.agent,
parts: [
{
type: "text",
synthetic: true,
text: backgroundMessage({
sessionID: nextSession.id,
description: params.description,
state,
text,
}),
},
],
})
.pipe(Effect.ignore, Effect.forkIn(scope, { startImmediately: true }))
})

const existing = yield* background.get(nextSession.id)
if (existing?.status === "running") {
return yield* Effect.fail(
new Error(`Task ${nextSession.id} is already running. Use task_status to check progress.`),
)
return yield* Effect.fail(new Error(`Task ${nextSession.id} is already running.`))
}

if (runInBackground) {
Expand Down Expand Up @@ -302,6 +259,7 @@ export const TaskTool = Tool.define(
}
}

const runCancel = yield* EffectBridge.make()
const cancel = ops.cancel(nextSession.id)

function onAbort() {
Expand Down
Loading
Loading