chore: generate

This commit is contained in:
opencode-agent[bot]
2026-05-14 16:42:18 +00:00
parent 22de34c4de
commit 756488d534
5 changed files with 80 additions and 45 deletions

View File

@@ -107,7 +107,10 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(Layer.provide(BackgroundJob.defaultLayer), Layer.provide(SessionStatus.defaultLayer))
export const defaultLayer = layer.pipe(
Layer.provide(BackgroundJob.defaultLayer),
Layer.provide(SessionStatus.defaultLayer),
)
const cancelBackgroundJobs = Effect.fn("SessionRunState.cancelBackgroundJobs")(function* (
background: BackgroundJob.Interface,

View File

@@ -71,7 +71,12 @@ function backgroundOutput(sessionID: SessionID) {
].join("\n")
}
function backgroundMessage(input: { sessionID: SessionID; description: string; state: "completed" | "error"; text: string }) {
function backgroundMessage(input: {
sessionID: SessionID
description: string
state: "completed" | "error"
text: string
}) {
const tag = input.state === "completed" ? "task_result" : "task_error"
const title =
input.state === "completed"
@@ -106,7 +111,9 @@ export const TaskTool = Tool.define(
const cfg = yield* config.get()
const runInBackground = params.background === true
if (runInBackground && !flags.experimentalBackgroundSubagents) {
return yield* Effect.fail(new Error("Background subagents require OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true"))
return yield* Effect.fail(
new Error("Background subagents require OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true"),
)
}
if (!ctx.extra?.bypassAgentCheck) {
@@ -196,27 +203,30 @@ 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,
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 }))
})
yield* ops.loop({ sessionID: ctx.sessionID }).pipe(Effect.ignore, Effect.forkIn(scope, { startImmediately: true }))
})
const continueIfIdle = Effect.fn("TaskTool.continueIfIdle")(function* (input: {
userID: MessageID
@@ -225,7 +235,10 @@ export const TaskTool = Tool.define(
yield* resumeWhenIdle(input).pipe(Effect.ignore, Effect.forkIn(scope, { startImmediately: true }))
})
const inject = Effect.fn("TaskTool.injectBackgroundResult")(function* (state: "completed" | "error", text: string) {
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,
@@ -249,7 +262,9 @@ export const TaskTool = Tool.define(
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. Use task_status to check progress.`),
)
}
if (runInBackground) {

View File

@@ -14,7 +14,9 @@ const POLL_MS = 300
const Parameters = Schema.Struct({
task_id: SessionID.annotate({ description: "The task_id returned by the task tool" }),
wait: Schema.optional(Schema.Boolean).annotate({ description: "When true, wait until the task reaches a terminal state or timeout" }),
wait: Schema.optional(Schema.Boolean).annotate({
description: "When true, wait until the task reaches a terminal state or timeout",
}),
timeout_ms: Schema.optional(PositiveInt).annotate({
description: "Maximum milliseconds to wait when wait=true (default: 60000)",
}),
@@ -39,7 +41,8 @@ function inspectMessage(message: MessageV2.WithParts): InspectResult | undefined
if (message.info.role !== "assistant") return
const text = message.parts.findLast((part) => part.type === "text")?.text ?? ""
if (message.info.error) return { state: "error", text: text || errorText(message.info.error) }
if (message.info.finish && !["tool-calls", "unknown"].includes(message.info.finish)) return { state: "completed", text }
if (message.info.finish && !["tool-calls", "unknown"].includes(message.info.finish))
return { state: "completed", text }
return { state: "running", text: text || "Task is still running." }
}
@@ -51,7 +54,9 @@ export const TaskStatusTool = Tool.define(
const status = yield* SessionStatus.Service
const flags = yield* RuntimeFlags.Service
const inspect: (taskID: SessionID) => Effect.Effect<InspectResult> = Effect.fn("TaskStatusTool.inspect")(function* (taskID: SessionID) {
const inspect: (taskID: SessionID) => Effect.Effect<InspectResult> = Effect.fn("TaskStatusTool.inspect")(function* (
taskID: SessionID,
) {
const job = yield* jobs.get(taskID)
if (job) {
return {
@@ -75,26 +80,32 @@ export const TaskStatusTool = Tool.define(
}
}
const latestAssistant = yield* sessions.findMessage(taskID, (item) => item.info.role === "assistant").pipe(Effect.orDie)
const latestAssistant = yield* sessions
.findMessage(taskID, (item) => item.info.role === "assistant")
.pipe(Effect.orDie)
if (Option.isSome(latestAssistant)) {
const latest = inspectMessage(latestAssistant.value)
if (!latest) return { state: "error", text: "Task is not running in this process." }
if (latest.state === "running") return { state: "error", text: "Task is not running in this process and has no final output." }
if (latest.state === "running")
return { state: "error", text: "Task is not running in this process and has no final output." }
return latest
}
return { state: "error", text: "Task is not running in this process and has not produced output." }
})
const waitForTerminal: (taskID: SessionID, timeout: number) => Effect.Effect<{ result: InspectResult; timedOut: boolean }> = Effect.fn(
"TaskStatusTool.waitForTerminal",
)(function* (taskID: SessionID, timeout: number) {
const result = yield* inspect(taskID)
if (result.state !== "running") return { result, timedOut: false }
if (timeout <= 0) return { result, timedOut: true }
const sleep = Math.min(POLL_MS, timeout)
yield* Effect.sleep(`${sleep} millis`)
return yield* waitForTerminal(taskID, timeout - sleep)
})
const waitForTerminal: (
taskID: SessionID,
timeout: number,
) => Effect.Effect<{ result: InspectResult; timedOut: boolean }> = Effect.fn("TaskStatusTool.waitForTerminal")(
function* (taskID: SessionID, timeout: number) {
const result = yield* inspect(taskID)
if (result.state !== "running") return { result, timedOut: false }
if (timeout <= 0) return { result, timedOut: true }
const sleep = Math.min(POLL_MS, timeout)
yield* Effect.sleep(`${sleep} millis`)
return yield* waitForTerminal(taskID, timeout - sleep)
},
)
const run = Effect.fn("TaskStatusTool.execute")(function* (
params: Schema.Schema.Type<typeof Parameters>,
@@ -161,7 +172,8 @@ export const TaskStatusTool = Tool.define(
return {
description: DESCRIPTION,
parameters: Parameters,
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) => run(params, ctx).pipe(Effect.orDie),
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
run(params, ctx).pipe(Effect.orDie),
}
}),
)

View File

@@ -111,9 +111,11 @@ describe("tool.registry", () => {
const agent = yield* Agent.Service
const build = yield* agent.get("build")
if (!build) throw new Error("build agent not found")
const task = (
yield* registry.tools({ providerID: ProviderID.opencode, modelID: ModelID.make("test"), agent: build })
).find((tool) => tool.id === "task")
const task = (yield* registry.tools({
providerID: ProviderID.opencode,
modelID: ModelID.make("test"),
agent: build,
})).find((tool) => tool.id === "task")
expect(task?.jsonSchema).toBeDefined()
expect((task?.jsonSchema?.properties as Record<string, unknown> | undefined)?.background).toBeUndefined()

View File

@@ -1738,7 +1738,10 @@ ToolRegistry.register({
const title = createMemo(() => agent().name ?? i18n.t("ui.tool.agent.default"))
const tone = createMemo(() => agent().color)
const subtitle = createMemo(() => {
const value = typeof props.input.description === "string" && props.input.description ? props.input.description : childSessionId()
const value =
typeof props.input.description === "string" && props.input.description
? props.input.description
: childSessionId()
if (!value) return value
if (props.metadata.background === true) return `${value} (background)`
return value