mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-28 03:34:39 +00:00
Compare commits
1 Commits
production
...
implement-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2453f40d88 |
@@ -127,14 +127,36 @@ export function Session() {
|
||||
.toSorted((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
||||
})
|
||||
const messages = createMemo(() => sync.data.message[route.sessionID] ?? [])
|
||||
const localPermissions = createMemo(() => sync.data.permission[route.sessionID] ?? [])
|
||||
const localQuestions = createMemo(() => sync.data.question[route.sessionID] ?? [])
|
||||
const childSessions = createMemo(() => {
|
||||
if (session()?.parentID) return []
|
||||
return children().filter((x) => x.id !== route.sessionID)
|
||||
})
|
||||
const permissions = createMemo(() => {
|
||||
if (session()?.parentID) return []
|
||||
return children().flatMap((x) => sync.data.permission[x.id] ?? [])
|
||||
const child = childSessions().flatMap((x) => sync.data.permission[x.id] ?? [])
|
||||
return [...localPermissions(), ...child]
|
||||
})
|
||||
const questions = createMemo(() => {
|
||||
if (session()?.parentID) return []
|
||||
return children().flatMap((x) => sync.data.question[x.id] ?? [])
|
||||
const child = childSessions().flatMap((x) => sync.data.question[x.id] ?? [])
|
||||
return [...localQuestions(), ...child]
|
||||
})
|
||||
const activeSubagents = createMemo(() =>
|
||||
childSessions().flatMap((item) => {
|
||||
const status = sync.data.session_status?.[item.id]
|
||||
if (status?.type === "busy" || status?.type === "retry") {
|
||||
return [
|
||||
{
|
||||
session: item,
|
||||
status,
|
||||
},
|
||||
]
|
||||
}
|
||||
return []
|
||||
}),
|
||||
)
|
||||
|
||||
const pending = createMemo(() => {
|
||||
return messages().findLast((x) => x.role === "assistant" && !x.time.completed)?.id
|
||||
@@ -1115,6 +1137,29 @@ export function Session() {
|
||||
</For>
|
||||
</scrollbox>
|
||||
<box flexShrink={0}>
|
||||
<Show when={activeSubagents().length > 0}>
|
||||
<box paddingLeft={3} paddingBottom={1} gap={0}>
|
||||
<text fg={theme.text}>
|
||||
<span style={{ fg: theme.textMuted }}>Subagents</span> {activeSubagents().length} running
|
||||
<span style={{ fg: theme.textMuted }}> · {keybind.print("session_child_cycle")} open</span>
|
||||
</text>
|
||||
<For each={activeSubagents().slice(0, 3)}>
|
||||
{(item) => (
|
||||
<text
|
||||
fg={theme.textMuted}
|
||||
onMouseUp={() => {
|
||||
navigate({
|
||||
type: "session",
|
||||
sessionID: item.session.id,
|
||||
})
|
||||
}}
|
||||
>
|
||||
↳ {Locale.truncate(item.session.title, 42)}
|
||||
</text>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={permissions().length > 0}>
|
||||
<PermissionPrompt request={permissions()[0]} />
|
||||
</Show>
|
||||
@@ -1122,7 +1167,7 @@ export function Session() {
|
||||
<QuestionPrompt request={questions()[0]} />
|
||||
</Show>
|
||||
<Prompt
|
||||
visible={!session()?.parentID && permissions().length === 0 && questions().length === 0}
|
||||
visible={!session()?.parentID && localPermissions().length === 0 && localQuestions().length === 0}
|
||||
ref={(r) => {
|
||||
prompt = r
|
||||
promptRef.set(r)
|
||||
@@ -1131,7 +1176,7 @@ export function Session() {
|
||||
r.set(route.initialPrompt)
|
||||
}
|
||||
}}
|
||||
disabled={permissions().length > 0 || questions().length > 0}
|
||||
disabled={localPermissions().length > 0 || localQuestions().length > 0}
|
||||
onSubmit={() => {
|
||||
toBottom()
|
||||
}}
|
||||
@@ -1886,22 +1931,47 @@ function Task(props: ToolProps<typeof TaskTool>) {
|
||||
const { theme } = useTheme()
|
||||
const keybind = useKeybind()
|
||||
const { navigate } = useRoute()
|
||||
const local = useLocal()
|
||||
const sync = useSync()
|
||||
|
||||
const tools = createMemo(() => {
|
||||
const msgs = createMemo(() => {
|
||||
const sessionID = props.metadata.sessionId
|
||||
const msgs = sync.data.message[sessionID ?? ""] ?? []
|
||||
return msgs.flatMap((msg) =>
|
||||
return sync.data.message[sessionID ?? ""] ?? []
|
||||
})
|
||||
const tools = createMemo(() =>
|
||||
msgs().flatMap((msg) =>
|
||||
(sync.data.part[msg.id] ?? [])
|
||||
.filter((part): part is ToolPart => part.type === "tool")
|
||||
.map((part) => ({ tool: part.tool, state: part.state })),
|
||||
)
|
||||
})
|
||||
),
|
||||
)
|
||||
|
||||
const current = createMemo(() => tools().findLast((x) => x.state.status !== "pending"))
|
||||
|
||||
const isRunning = createMemo(() => props.part.state.status === "running")
|
||||
const background = createMemo(() => props.metadata.background === true)
|
||||
const status = createMemo(() => {
|
||||
const sessionID = props.metadata.sessionId
|
||||
if (!sessionID) return
|
||||
return sync.data.session_status?.[sessionID]
|
||||
})
|
||||
const counts = createMemo(() => {
|
||||
const all = tools()
|
||||
const done = all.filter((item) => item.state.status === "completed" || item.state.status === "error").length
|
||||
return {
|
||||
all: all.length,
|
||||
done,
|
||||
}
|
||||
})
|
||||
const childRunning = createMemo(() => status()?.type === "busy" || status()?.type === "retry")
|
||||
const backgroundRunning = createMemo(() => background() && childRunning())
|
||||
const failed = createMemo(() => {
|
||||
if (!background() || childRunning()) return false
|
||||
return !!msgs().findLast((msg) => msg.role === "assistant")?.error
|
||||
})
|
||||
const isRunning = createMemo(() => props.part.state.status === "running" || childRunning())
|
||||
const toolLabel = createMemo(() => {
|
||||
const total = counts().all
|
||||
if (!childRunning()) return `${total} toolcalls`
|
||||
return `${counts().done}/${total} toolcalls`
|
||||
})
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
@@ -1918,8 +1988,21 @@ function Task(props: ToolProps<typeof TaskTool>) {
|
||||
>
|
||||
<box>
|
||||
<text style={{ fg: theme.textMuted }}>
|
||||
{props.input.description} ({tools().length} toolcalls)
|
||||
{props.input.description} ({toolLabel()})
|
||||
<Show when={background()}>
|
||||
<span> · background</span>
|
||||
</Show>
|
||||
</text>
|
||||
<Show when={background()}>
|
||||
<text style={{ fg: failed() ? theme.error : backgroundRunning() ? theme.warning : theme.textMuted }}>
|
||||
↳{" "}
|
||||
{backgroundRunning()
|
||||
? "running in background"
|
||||
: failed()
|
||||
? "background task failed"
|
||||
: "background task finished"}
|
||||
</text>
|
||||
</Show>
|
||||
<Show when={current()}>
|
||||
{(item) => {
|
||||
const title = item().state.status === "completed" ? (item().state as any).title : ""
|
||||
|
||||
@@ -7,6 +7,7 @@ import { GrepTool } from "./grep"
|
||||
import { BatchTool } from "./batch"
|
||||
import { ReadTool } from "./read"
|
||||
import { TaskTool } from "./task"
|
||||
import { TaskStatusTool } from "./task_status"
|
||||
import { TodoWriteTool, TodoReadTool } from "./todo"
|
||||
import { WebFetchTool } from "./webfetch"
|
||||
import { WriteTool } from "./write"
|
||||
@@ -110,6 +111,7 @@ export namespace ToolRegistry {
|
||||
EditTool,
|
||||
WriteTool,
|
||||
TaskTool,
|
||||
TaskStatusTool,
|
||||
WebFetchTool,
|
||||
TodoWriteTool,
|
||||
// TodoReadTool,
|
||||
|
||||
@@ -22,8 +22,52 @@ const parameters = z.object({
|
||||
)
|
||||
.optional(),
|
||||
command: z.string().describe("The command that triggered this task").optional(),
|
||||
background: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("When true, launch the subagent in the background and return immediately"),
|
||||
})
|
||||
|
||||
function output(sessionID: string, text: string) {
|
||||
return [
|
||||
`task_id: ${sessionID} (for resuming to continue this task if needed)`,
|
||||
"",
|
||||
"<task_result>",
|
||||
text,
|
||||
"</task_result>",
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
function backgroundOutput(sessionID: string) {
|
||||
return [
|
||||
`task_id: ${sessionID} (for polling this task with task_status)`,
|
||||
"state: running",
|
||||
"",
|
||||
"<task_result>",
|
||||
"Background task started. Continue your current work and call task_status when you need the result.",
|
||||
"</task_result>",
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
function backgroundMessage(input: {
|
||||
sessionID: string
|
||||
description: string
|
||||
state: "completed" | "error"
|
||||
text: string
|
||||
}) {
|
||||
const tag = input.state === "completed" ? "task_result" : "task_error"
|
||||
const title =
|
||||
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")
|
||||
}
|
||||
|
||||
function errorText(error: unknown) {
|
||||
if (error instanceof Error) return error.message
|
||||
return String(error)
|
||||
}
|
||||
|
||||
export const TaskTool = Tool.define("task", async (ctx) => {
|
||||
const agents = await Agent.list().then((x) => x.filter((a) => a.mode !== "primary"))
|
||||
|
||||
@@ -103,62 +147,94 @@ export const TaskTool = Tool.define("task", async (ctx) => {
|
||||
const msg = await MessageV2.get({ sessionID: ctx.sessionID, messageID: ctx.messageID })
|
||||
if (msg.info.role !== "assistant") throw new Error("Not an assistant message")
|
||||
|
||||
const model = agent.model ?? {
|
||||
const parentModel = {
|
||||
modelID: msg.info.modelID,
|
||||
providerID: msg.info.providerID,
|
||||
}
|
||||
const model = agent.model ?? parentModel
|
||||
const background = params.background === true
|
||||
const metadata = {
|
||||
sessionId: session.id,
|
||||
model,
|
||||
...(background ? { background: true } : {}),
|
||||
}
|
||||
|
||||
ctx.metadata({
|
||||
title: params.description,
|
||||
metadata: {
|
||||
sessionId: session.id,
|
||||
model,
|
||||
},
|
||||
metadata,
|
||||
})
|
||||
|
||||
const messageID = Identifier.ascending("message")
|
||||
const run = async () => {
|
||||
const promptParts = await SessionPrompt.resolvePromptParts(params.prompt)
|
||||
const result = await SessionPrompt.prompt({
|
||||
messageID: Identifier.ascending("message"),
|
||||
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,
|
||||
})
|
||||
return result.parts.findLast((x) => x.type === "text")?.text ?? ""
|
||||
}
|
||||
|
||||
if (background) {
|
||||
const inject = (state: "completed" | "error", text: string) =>
|
||||
SessionPrompt.prompt({
|
||||
sessionID: ctx.sessionID,
|
||||
noReply: true,
|
||||
model: {
|
||||
modelID: parentModel.modelID,
|
||||
providerID: parentModel.providerID,
|
||||
},
|
||||
agent: ctx.agent,
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
synthetic: true,
|
||||
text: backgroundMessage({
|
||||
sessionID: session.id,
|
||||
description: params.description,
|
||||
state,
|
||||
text,
|
||||
}),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
void run()
|
||||
.then((text) => {
|
||||
void inject("completed", text).catch(() => {})
|
||||
})
|
||||
.catch((error) => {
|
||||
void inject("error", errorText(error)).catch(() => {})
|
||||
})
|
||||
|
||||
return {
|
||||
title: params.description,
|
||||
metadata,
|
||||
output: backgroundOutput(session.id),
|
||||
}
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
SessionPrompt.cancel(session.id)
|
||||
}
|
||||
ctx.abort.addEventListener("abort", cancel)
|
||||
using _ = defer(() => ctx.abort.removeEventListener("abort", cancel))
|
||||
const promptParts = await SessionPrompt.resolvePromptParts(params.prompt)
|
||||
|
||||
const result = await 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,
|
||||
})
|
||||
|
||||
const text = result.parts.findLast((x) => x.type === "text")?.text ?? ""
|
||||
|
||||
const output = [
|
||||
`task_id: ${session.id} (for resuming to continue this task if needed)`,
|
||||
"",
|
||||
"<task_result>",
|
||||
text,
|
||||
"</task_result>",
|
||||
].join("\n")
|
||||
const text = await run()
|
||||
|
||||
return {
|
||||
title: params.description,
|
||||
metadata: {
|
||||
sessionId: session.id,
|
||||
model,
|
||||
},
|
||||
output,
|
||||
metadata,
|
||||
output: output(session.id, text),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -17,11 +17,13 @@ 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
|
||||
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.
|
||||
2. By default, task waits for completion and returns the result immediately, along with a task_id you can reuse later to continue the same subagent session.
|
||||
3. Set background=true to launch asynchronously. In background mode, continue your current work without waiting.
|
||||
4. For background runs, use task_status(task_id=..., wait=false) to poll, or wait=true to block until done (optionally with timeout_ms).
|
||||
5. 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.
|
||||
6. The agent's outputs should generally be trusted
|
||||
7. 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).
|
||||
8. 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.
|
||||
|
||||
Example usage (NOTE: The agents below are fictional examples for illustration only - use the actual agents listed above):
|
||||
|
||||
|
||||
140
packages/opencode/src/tool/task_status.ts
Normal file
140
packages/opencode/src/tool/task_status.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import z from "zod"
|
||||
import { Tool } from "./tool"
|
||||
import DESCRIPTION from "./task_status.txt"
|
||||
import { Identifier } from "../id/id"
|
||||
import { Session } from "../session"
|
||||
import { SessionStatus } from "../session/status"
|
||||
import { MessageV2 } from "../session/message-v2"
|
||||
|
||||
type State = "running" | "completed" | "error"
|
||||
|
||||
const DEFAULT_TIMEOUT = 60_000
|
||||
const POLL_MS = 300
|
||||
|
||||
const parameters = z.object({
|
||||
task_id: Identifier.schema("session").describe("The task_id returned by the task tool"),
|
||||
wait: z.boolean().optional().describe("When true, wait until the task reaches a terminal state or timeout"),
|
||||
timeout_ms: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.describe("Maximum milliseconds to wait when wait=true (default: 60000)"),
|
||||
})
|
||||
|
||||
function format(input: { taskID: string; state: State; text: string }) {
|
||||
return [`task_id: ${input.taskID}`, `state: ${input.state}`, "", "<task_result>", input.text, "</task_result>"].join(
|
||||
"\n",
|
||||
)
|
||||
}
|
||||
|
||||
function errorText(error: NonNullable<MessageV2.Assistant["error"]>) {
|
||||
const data = error.data as Record<string, unknown> | undefined
|
||||
const message = data?.message
|
||||
if (typeof message === "string" && message) return message
|
||||
return error.name
|
||||
}
|
||||
|
||||
async function inspect(taskID: string) {
|
||||
const status = SessionStatus.get(taskID)
|
||||
if (status.type === "busy" || status.type === "retry") {
|
||||
return {
|
||||
state: "running" as const,
|
||||
text: status.type === "retry" ? `Task is retrying: ${status.message}` : "Task is still running.",
|
||||
}
|
||||
}
|
||||
|
||||
for await (const item of MessageV2.stream(taskID)) {
|
||||
if (item.info.role !== "assistant") continue
|
||||
|
||||
const text = item.parts.findLast((part) => part.type === "text")?.text ?? ""
|
||||
if (item.info.error) {
|
||||
const summary = errorText(item.info.error)
|
||||
return {
|
||||
state: "error" as const,
|
||||
text: text || summary,
|
||||
}
|
||||
}
|
||||
|
||||
const done = item.info.finish && !["tool-calls", "unknown"].includes(item.info.finish)
|
||||
if (done) {
|
||||
return {
|
||||
state: "completed" as const,
|
||||
text,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
state: "running" as const,
|
||||
text: text || "Task is still running.",
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
state: "running" as const,
|
||||
text: "Task has started but has not produced output yet.",
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms: number, abort: AbortSignal) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (abort.aborted) {
|
||||
reject(new Error("Task status polling aborted"))
|
||||
return
|
||||
}
|
||||
|
||||
const onAbort = () => {
|
||||
clearTimeout(timer)
|
||||
reject(new Error("Task status polling aborted"))
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
abort.removeEventListener("abort", onAbort)
|
||||
resolve()
|
||||
}, ms)
|
||||
|
||||
abort.addEventListener("abort", onAbort, { once: true })
|
||||
})
|
||||
}
|
||||
|
||||
export const TaskStatusTool = Tool.define("task_status", {
|
||||
description: DESCRIPTION,
|
||||
parameters,
|
||||
async execute(params, ctx) {
|
||||
await Session.get(params.task_id)
|
||||
|
||||
let result = await inspect(params.task_id)
|
||||
if (!params.wait || result.state !== "running") {
|
||||
return {
|
||||
title: "Task status",
|
||||
metadata: {
|
||||
task_id: params.task_id,
|
||||
state: result.state,
|
||||
timed_out: false,
|
||||
},
|
||||
output: format({ taskID: params.task_id, state: result.state, text: result.text }),
|
||||
}
|
||||
}
|
||||
|
||||
const timeout = params.timeout_ms ?? DEFAULT_TIMEOUT
|
||||
const end = Date.now() + timeout
|
||||
while (Date.now() < end) {
|
||||
const left = end - Date.now()
|
||||
await sleep(Math.min(POLL_MS, left), ctx.abort)
|
||||
result = await inspect(params.task_id)
|
||||
if (result.state !== "running") break
|
||||
}
|
||||
|
||||
const done = result.state !== "running"
|
||||
const text = done ? result.text : `Timed out after ${timeout}ms while waiting for task completion.`
|
||||
return {
|
||||
title: "Task status",
|
||||
metadata: {
|
||||
task_id: params.task_id,
|
||||
state: result.state,
|
||||
timed_out: !done,
|
||||
},
|
||||
output: format({ taskID: params.task_id, state: result.state, text }),
|
||||
}
|
||||
},
|
||||
})
|
||||
13
packages/opencode/src/tool/task_status.txt
Normal file
13
packages/opencode/src/tool/task_status.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
Poll the status of a subagent task launched with the task tool.
|
||||
|
||||
Use this to check background tasks started with `task(background=true)`.
|
||||
|
||||
Parameters:
|
||||
- `task_id` (required): the task session id returned by the task tool
|
||||
- `wait` (optional): when true, wait for completion
|
||||
- `timeout_ms` (optional): max wait duration in milliseconds when `wait=true`
|
||||
|
||||
Returns compact, parseable output:
|
||||
- `task_id`
|
||||
- `state` (`running`, `completed`, or `error`)
|
||||
- `<task_result>...</task_result>` containing final output, error summary, or current progress text
|
||||
141
packages/opencode/test/tool/task_status.test.ts
Normal file
141
packages/opencode/test/tool/task_status.test.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Session } from "../../src/session"
|
||||
import { Identifier } from "../../src/id/id"
|
||||
import { SessionStatus } from "../../src/session/status"
|
||||
import { TaskStatusTool } from "../../src/tool/task_status"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
|
||||
const ctx = {
|
||||
sessionID: "session_test",
|
||||
messageID: "message_test",
|
||||
callID: "call_test",
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
messages: [],
|
||||
metadata: () => {},
|
||||
ask: async () => {},
|
||||
}
|
||||
|
||||
async function assistant(input: { sessionID: string; text: string; error?: string }) {
|
||||
const msg = await Session.updateMessage({
|
||||
id: Identifier.ascending("message"),
|
||||
sessionID: input.sessionID,
|
||||
role: "assistant",
|
||||
time: {
|
||||
created: Date.now(),
|
||||
completed: Date.now(),
|
||||
},
|
||||
parentID: Identifier.ascending("message"),
|
||||
modelID: "test-model",
|
||||
providerID: "test-provider",
|
||||
mode: "build",
|
||||
agent: "build",
|
||||
path: {
|
||||
cwd: process.cwd(),
|
||||
root: process.cwd(),
|
||||
},
|
||||
cost: 0,
|
||||
tokens: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
reasoning: 0,
|
||||
cache: {
|
||||
read: 0,
|
||||
write: 0,
|
||||
},
|
||||
},
|
||||
finish: "stop",
|
||||
...(input.error
|
||||
? {
|
||||
error: new MessageV2.APIError({
|
||||
message: input.error,
|
||||
isRetryable: false,
|
||||
}).toObject(),
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
|
||||
await Session.updatePart({
|
||||
id: Identifier.ascending("part"),
|
||||
sessionID: input.sessionID,
|
||||
messageID: msg.id,
|
||||
type: "text",
|
||||
text: input.text,
|
||||
})
|
||||
}
|
||||
|
||||
describe("tool.task_status", () => {
|
||||
test("returns running while session status is busy", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const tool = await TaskStatusTool.init()
|
||||
|
||||
SessionStatus.set(session.id, { type: "busy" })
|
||||
const result = await tool.execute({ task_id: session.id }, ctx)
|
||||
|
||||
expect(result.output).toContain("state: running")
|
||||
SessionStatus.set(session.id, { type: "idle" })
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("returns completed with final task output", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const tool = await TaskStatusTool.init()
|
||||
|
||||
await assistant({
|
||||
sessionID: session.id,
|
||||
text: "all done",
|
||||
})
|
||||
|
||||
const result = await tool.execute({ task_id: session.id }, ctx)
|
||||
expect(result.output).toContain("state: completed")
|
||||
expect(result.output).toContain("all done")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("wait=true blocks until terminal status", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const tool = await TaskStatusTool.init()
|
||||
|
||||
SessionStatus.set(session.id, { type: "busy" })
|
||||
setTimeout(async () => {
|
||||
SessionStatus.set(session.id, { type: "idle" })
|
||||
await assistant({
|
||||
sessionID: session.id,
|
||||
text: "finished later",
|
||||
})
|
||||
}, 150)
|
||||
|
||||
const result = await tool.execute(
|
||||
{
|
||||
task_id: session.id,
|
||||
wait: true,
|
||||
timeout_ms: 4_000,
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
|
||||
expect(result.output).toContain("state: completed")
|
||||
expect(result.output).toContain("finished later")
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user