fix(core): keep background shell notices accurate after completion
This commit is contained in:
parent
674d08f9be
commit
15227b57a9
4 changed files with 42 additions and 2 deletions
|
|
@ -18,8 +18,10 @@ export const DEFAULT_TIMEOUT_MS = 2 * 60 * 1_000
|
|||
export const MAX_TIMEOUT_MS = 10 * 60 * 1_000
|
||||
export const MAX_CAPTURE_BYTES = 1024 * 1024
|
||||
|
||||
// Persisted in the transcript, so it must stay accurate after the command later
|
||||
// finishes; do not claim the command is currently running.
|
||||
const BACKGROUND_STARTED =
|
||||
"The command has not completed; it is now running in the background."
|
||||
"The command was moved to the background. You will be notified automatically when it finishes. DO NOT sleep, poll, or proactively check on its progress."
|
||||
|
||||
export const Input = Schema.Struct({
|
||||
command: Schema.String.annotate({ description: "Shell command string to execute" }),
|
||||
|
|
@ -124,8 +126,11 @@ export const Plugin = {
|
|||
: state === "error"
|
||||
? (result.info!.error ?? "Command failed")
|
||||
: "Command cancelled"
|
||||
// The description makes the completion visible in clients; synthetic
|
||||
// messages without one are model-facing context only.
|
||||
return runtime.session.synthetic({
|
||||
sessionID,
|
||||
description: `Background command ${state}: ${command.split("\n")[0]}`,
|
||||
text: `<shell id="${callID}" state="${state}" command="${command}">\n${text}\n</shell>`,
|
||||
})
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -67,8 +67,11 @@ export const Plugin = {
|
|||
state: "completed" | "error" | "cancelled",
|
||||
text: string,
|
||||
) {
|
||||
// The description makes the completion visible in clients; synthetic
|
||||
// messages without one are model-facing context only.
|
||||
yield* runtime.session.synthetic({
|
||||
sessionID: parentID,
|
||||
description: `Subagent ${state}: ${description}`,
|
||||
text: `<subagent id="${childID}" state="${state}" description="${description}">\n${text}\n</subagent>`,
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -455,6 +455,37 @@ describe("ShellTool", () => {
|
|||
),
|
||||
)
|
||||
|
||||
it.live("notifies with a visible description when a background command completes", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => {
|
||||
reset()
|
||||
return withSession(tmp.path, (registry) =>
|
||||
Effect.gen(function* () {
|
||||
const sessions = yield* SessionV2.Service
|
||||
yield* settleTool(registry, call({ command: helloCommand, background: true }))
|
||||
const awaitNotice = (remaining = 1000): Effect.Effect<SessionMessage.Message, Error> =>
|
||||
Effect.gen(function* () {
|
||||
const notice = (yield* sessions.context(sessionID)).find((message) => message.type === "synthetic")
|
||||
if (notice) return notice
|
||||
if (remaining <= 0)
|
||||
return yield* Effect.fail(new Error("Timed out waiting for background completion notice"))
|
||||
yield* Effect.promise(() => Bun.sleep(1))
|
||||
return yield* awaitNotice(remaining - 1)
|
||||
})
|
||||
const notice = yield* awaitNotice()
|
||||
expect(notice).toMatchObject({
|
||||
type: "synthetic",
|
||||
description: `Background command completed: ${helloCommand}`,
|
||||
text: expect.stringContaining('state="completed"'),
|
||||
})
|
||||
}),
|
||||
)
|
||||
},
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]().then(() => undefined)),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("backgrounds a foreground command when the session is signaled", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
|
|
@ -484,7 +515,7 @@ describe("ShellTool", () => {
|
|||
expect(settled.output?.structured).toMatchObject({ truncated: false })
|
||||
expect(settled.output?.content[0]).toMatchObject({
|
||||
type: "text",
|
||||
text: expect.stringContaining("running in the background"),
|
||||
text: expect.stringContaining("moved to the background"),
|
||||
})
|
||||
expect(shellID).toStartWith("sh_")
|
||||
|
||||
|
|
|
|||
|
|
@ -279,6 +279,7 @@ describe("SubagentTool", () => {
|
|||
expect(synthetic).toHaveLength(1)
|
||||
expect(synthetic[0]?.text).toContain(`<subagent id="${childID}" state="completed"`)
|
||||
expect(synthetic[0]?.text).toContain(childText)
|
||||
expect(synthetic[0]?.description).toBe("Subagent completed: background review")
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue