Skip to content

Commit 02a1c5a

Browse files
Apply PR #20039: feat: bash->shell tool + pwsh/powershell/cmd/bash specific tool definitions so agents work better
2 parents d219b35 + 20c3461 commit 02a1c5a

18 files changed

Lines changed: 665 additions & 197 deletions

File tree

packages/opencode/src/acp/agent.ts

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import { LoadAPIKeyError } from "ai"
5151
import type { AssistantMessage, Event, OpencodeClient, SessionMessageResponse, ToolPart } from "@opencode-ai/sdk/v2"
5252
import { applyPatch } from "diff"
5353
import { InstallationVersion } from "@opencode-ai/core/installation/version"
54+
import { ShellToolID } from "@/tool/shell/id"
5455

5556
type ModeOption = { id: string; name: string; description?: string }
5657
type ModelOption = { modelId: string; name: string }
@@ -144,7 +145,7 @@ export class Agent implements ACPAgent {
144145
private sessionManager: ACPSessionManager
145146
private eventAbort = new AbortController()
146147
private eventStarted = false
147-
private bashSnapshots = new Map<string, string>()
148+
private shellSnapshots = new Map<string, string>()
148149
private toolStarts = new Set<string>()
149150
private permissionQueues = new Map<string, Promise<void>>()
150151
private permissionOptions: PermissionOption[] = [
@@ -283,16 +284,16 @@ export class Agent implements ACPAgent {
283284

284285
switch (part.state.status) {
285286
case "pending":
286-
this.bashSnapshots.delete(part.callID)
287+
this.shellSnapshots.delete(part.callID)
287288
return
288289

289290
case "running":
290-
const output = this.bashOutput(part)
291+
const output = this.shellOutput(part)
291292
const content: ToolCallContent[] = []
292293
if (output) {
293294
const hash = Hash.fast(output)
294-
if (part.tool === "bash") {
295-
if (this.bashSnapshots.get(part.callID) === hash) {
295+
if (part.tool === ShellToolID.id) {
296+
if (this.shellSnapshots.get(part.callID) === hash) {
296297
await this.connection
297298
.sessionUpdate({
298299
sessionId,
@@ -311,7 +312,7 @@ export class Agent implements ACPAgent {
311312
})
312313
return
313314
}
314-
this.bashSnapshots.set(part.callID, hash)
315+
this.shellSnapshots.set(part.callID, hash)
315316
}
316317
content.push({
317318
type: "content",
@@ -342,7 +343,7 @@ export class Agent implements ACPAgent {
342343

343344
case "completed": {
344345
this.toolStarts.delete(part.callID)
345-
this.bashSnapshots.delete(part.callID)
346+
this.shellSnapshots.delete(part.callID)
346347
const kind = toToolKind(part.tool)
347348
const content: ToolCallContent[] = [
348349
{
@@ -423,7 +424,7 @@ export class Agent implements ACPAgent {
423424
}
424425
case "error":
425426
this.toolStarts.delete(part.callID)
426-
this.bashSnapshots.delete(part.callID)
427+
this.shellSnapshots.delete(part.callID)
427428
await this.connection
428429
.sessionUpdate({
429430
sessionId,
@@ -837,10 +838,10 @@ export class Agent implements ACPAgent {
837838
await this.toolStart(sessionId, part)
838839
switch (part.state.status) {
839840
case "pending":
840-
this.bashSnapshots.delete(part.callID)
841+
this.shellSnapshots.delete(part.callID)
841842
break
842843
case "running":
843-
const output = this.bashOutput(part)
844+
const output = this.shellOutput(part)
844845
const runningContent: ToolCallContent[] = []
845846
if (output) {
846847
runningContent.push({
@@ -871,7 +872,7 @@ export class Agent implements ACPAgent {
871872
break
872873
case "completed":
873874
this.toolStarts.delete(part.callID)
874-
this.bashSnapshots.delete(part.callID)
875+
this.shellSnapshots.delete(part.callID)
875876
const kind = toToolKind(part.tool)
876877
const content: ToolCallContent[] = [
877878
{
@@ -951,7 +952,7 @@ export class Agent implements ACPAgent {
951952
break
952953
case "error":
953954
this.toolStarts.delete(part.callID)
954-
this.bashSnapshots.delete(part.callID)
955+
this.shellSnapshots.delete(part.callID)
955956
await this.connection
956957
.sessionUpdate({
957958
sessionId,
@@ -1105,8 +1106,8 @@ export class Agent implements ACPAgent {
11051106
}
11061107
}
11071108

1108-
private bashOutput(part: ToolPart) {
1109-
if (part.tool !== "bash") return
1109+
private shellOutput(part: ToolPart) {
1110+
if (part.tool !== ShellToolID.id) return
11101111
if (!("metadata" in part.state) || !part.state.metadata || typeof part.state.metadata !== "object") return
11111112
const output = part.state.metadata["output"]
11121113
if (typeof output !== "string") return
@@ -1549,9 +1550,9 @@ export class Agent implements ACPAgent {
15491550

15501551
function toToolKind(toolName: string): ToolKind {
15511552
const tool = toolName.toLocaleLowerCase()
1553+
if (tool === ShellToolID.id) return "execute"
1554+
15521555
switch (tool) {
1553-
case "bash":
1554-
return "execute"
15551556
case "webfetch":
15561557
return "fetch"
15571558

@@ -1576,6 +1577,8 @@ function toToolKind(toolName: string): ToolKind {
15761577

15771578
function toLocations(toolName: string, input: Record<string, any>): { path: string }[] {
15781579
const tool = toolName.toLocaleLowerCase()
1580+
if (tool === ShellToolID.id) return []
1581+
15791582
switch (tool) {
15801583
case "read":
15811584
case "edit":
@@ -1584,8 +1587,6 @@ function toLocations(toolName: string, input: Record<string, any>): { path: stri
15841587
case "glob":
15851588
case "grep":
15861589
return input["path"] ? [{ path: input["path"] }] : []
1587-
case "bash":
1588-
return []
15891590
default:
15901591
return []
15911592
}

packages/opencode/src/cli/cmd/github.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -879,7 +879,7 @@ export const GithubRunCommand = cmd({
879879
function subscribeSessionEvents() {
880880
const TOOL: Record<string, [string, string]> = {
881881
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
882-
bash: ["Bash", UI.Style.TEXT_DANGER_BOLD],
882+
bash: ["Shell", UI.Style.TEXT_DANGER_BOLD],
883883
edit: ["Edit", UI.Style.TEXT_SUCCESS_BOLD],
884884
glob: ["Glob", UI.Style.TEXT_INFO_BOLD],
885885
grep: ["Grep", UI.Style.TEXT_INFO_BOLD],

packages/opencode/src/cli/cmd/run.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ import { CodeSearchTool } from "../../tool/codesearch"
2323
import { WebSearchTool } from "../../tool/websearch"
2424
import { TaskTool } from "../../tool/task"
2525
import { SkillTool } from "../../tool/skill"
26-
import { BashTool } from "../../tool/bash"
26+
import { ShellTool } from "../../tool/shell"
27+
import { ShellToolID } from "../../tool/shell/id"
2728
import { TodoWriteTool } from "../../tool/todo"
2829
import { Locale } from "@/util/locale"
2930
import { AppRuntime } from "@/effect/app-runtime"
@@ -183,7 +184,7 @@ function skill(info: ToolProps<typeof SkillTool>) {
183184
})
184185
}
185186

186-
function bash(info: ToolProps<typeof BashTool>) {
187+
function shell(info: ToolProps<typeof ShellTool>) {
187188
const output = info.part.state.status === "completed" ? info.part.state.output?.trim() : undefined
188189
block(
189190
{
@@ -413,7 +414,7 @@ export const RunCommand = cmd({
413414
async function execute(sdk: OpencodeClient) {
414415
function tool(part: ToolPart) {
415416
try {
416-
if (part.tool === "bash") return bash(props<typeof BashTool>(part))
417+
if (part.tool === ShellToolID.id) return shell(props<typeof ShellTool>(part))
417418
if (part.tool === "glob") return glob(props<typeof GlobTool>(part))
418419
if (part.tool === "grep") return grep(props<typeof GrepTool>(part))
419420
if (part.tool === "read") return read(props<typeof ReadTool>(part))

packages/opencode/src/cli/cmd/tui/routes/session/index.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ import { Locale } from "@/util/locale"
3737
import type { Tool } from "@/tool/tool"
3838
import type { ReadTool } from "@/tool/read"
3939
import type { WriteTool } from "@/tool/write"
40-
import { BashTool } from "@/tool/bash"
40+
import { ShellTool } from "@/tool/shell"
41+
import { ShellToolID } from "@/tool/shell/id"
4142
import type { GlobTool } from "@/tool/glob"
4243
import { TodoWriteTool } from "@/tool/todo"
4344
import type { GrepTool } from "@/tool/grep"
@@ -1556,8 +1557,8 @@ function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMess
15561557
return (
15571558
<Show when={!shouldHide()}>
15581559
<Switch>
1559-
<Match when={props.part.tool === "bash"}>
1560-
<Bash {...toolprops} />
1560+
<Match when={props.part.tool === ShellToolID.id}>
1561+
<Shell {...toolprops} />
15611562
</Match>
15621563
<Match when={props.part.tool === "glob"}>
15631564
<Glob {...toolprops} />
@@ -1791,7 +1792,7 @@ function BlockTool(props: {
17911792
)
17921793
}
17931794

1794-
function Bash(props: ToolProps<typeof BashTool>) {
1795+
function Shell(props: ToolProps<typeof ShellTool>) {
17951796
const { theme } = useTheme()
17961797
const sync = useSync()
17971798
const isRunning = createMemo(() => props.part.state.status === "running")

packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
1515
import { Keybind } from "@/util/keybind"
1616
import { Locale } from "@/util/locale"
1717
import { Global } from "@opencode-ai/core/global"
18+
import { ShellToolID } from "@/tool/shell/id"
1819
import { useDialog } from "../../ui/dialog"
1920
import { getScrollAcceleration } from "../../util/scroll"
2021
import { useTuiConfig } from "../../context/tui-config"
@@ -287,7 +288,7 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
287288
}
288289
}
289290

290-
if (permission === "bash") {
291+
if (permission === ShellToolID.id) {
291292
const title =
292293
typeof data.description === "string" && data.description ? data.description : "Shell command"
293294
const command = typeof data.command === "string" ? data.command : ""

packages/opencode/src/session/llm.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ const live: Layer.Layer<
206206
input.model.providerID.toLowerCase().includes("litellm") ||
207207
input.model.api.id.toLowerCase().includes("litellm")
208208

209+
const repair = (toolName: string) => repairToolName(toolName, tools)
210+
209211
// LiteLLM/Bedrock rejects requests where the message history contains tool
210212
// calls but no tools param is present. When there are no active tools (e.g.
211213
// during compaction), inject a stub tool to satisfy the validation requirement.
@@ -239,7 +241,7 @@ const live: Layer.Layer<
239241
workflowModel.sessionID = input.sessionID
240242
workflowModel.systemPrompt = system.join("\n")
241243
workflowModel.toolExecutor = async (toolName, argsJson, _requestID) => {
242-
const t = tools[toolName]
244+
const t = tools[repair(toolName) ?? toolName]
243245
if (!t || !t.execute) {
244246
return { result: "", error: `Unknown tool: ${toolName}` }
245247
}
@@ -337,15 +339,15 @@ const live: Layer.Layer<
337339
})
338340
},
339341
async experimental_repairToolCall(failed) {
340-
const lower = failed.toolCall.toolName.toLowerCase()
341-
if (lower !== failed.toolCall.toolName && tools[lower]) {
342+
const repaired = repair(failed.toolCall.toolName)
343+
if (repaired && repaired !== failed.toolCall.toolName) {
342344
l.info("repairing tool call", {
343345
tool: failed.toolCall.toolName,
344-
repaired: lower,
346+
repaired,
345347
})
346348
return {
347349
...failed.toolCall,
348-
toolName: lower,
350+
toolName: repaired,
349351
}
350352
}
351353
return {
@@ -443,12 +445,22 @@ export const defaultLayer = Layer.suspend(() =>
443445
),
444446
)
445447

448+
export function repairToolName(toolName: string, tools: Record<string, Tool>) {
449+
const next = toolName.toLowerCase()
450+
if (!tools[next]) return
451+
return next
452+
}
453+
446454
function resolveTools(input: Pick<StreamInput, "tools" | "agent" | "permission" | "user">) {
447455
const disabled = Permission.disabled(
448456
Object.keys(input.tools),
449457
Permission.merge(input.agent.permission, input.permission ?? []),
450458
)
451-
return Record.filter(input.tools, (_, k) => input.user.tools?.[k] !== false && !disabled.has(k))
459+
return Record.filter(input.tools, (_, k) => {
460+
const userTool = input.user.tools?.[k]
461+
if (userTool !== undefined) return userTool !== false && !disabled.has(k)
462+
return !disabled.has(k)
463+
})
452464
}
453465

454466
// Check if messages contain any tool-call content

packages/opencode/src/session/prompt.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { Permission } from "@/permission"
4141
import { SessionStatus } from "./status"
4242
import { LLM } from "./llm"
4343
import { Shell } from "@/shell/shell"
44+
import { ShellToolID } from "@/tool/shell/id"
4445
import { AppFileSystem } from "@opencode-ai/core/filesystem"
4546
import { Truncate } from "@/tool/truncate"
4647
import { decodeDataUrl } from "@/util/data-url"
@@ -778,7 +779,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
778779
id: PartID.ascending(),
779780
messageID: msg.id,
780781
sessionID: input.sessionID,
781-
tool: "bash",
782+
tool: ShellToolID.id,
782783
callID: ulid(),
783784
state: {
784785
status: "running",

packages/opencode/src/tool/registry.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { PlanExitTool } from "./plan"
22
import { Session } from "@/session/session"
33
import { QuestionTool } from "./question"
4-
import { BashTool } from "./bash"
4+
import { ShellTool } from "./shell"
55
import { EditTool } from "./edit"
66
import { GlobTool } from "./glob"
77
import { GrepTool } from "./grep"
@@ -107,7 +107,7 @@ export const layer: Layer.Layer<
107107
const plan = yield* PlanExitTool
108108
const webfetch = yield* WebFetchTool
109109
const websearch = yield* WebSearchTool
110-
const bash = yield* BashTool
110+
const shell = yield* ShellTool
111111
const codesearch = yield* CodeSearchTool
112112
const globtool = yield* GlobTool
113113
const writetool = yield* WriteTool
@@ -188,7 +188,7 @@ export const layer: Layer.Layer<
188188

189189
const tool = yield* Effect.all({
190190
invalid: Tool.init(invalid),
191-
bash: Tool.init(bash),
191+
shell: Tool.init(shell),
192192
read: Tool.init(read),
193193
glob: Tool.init(globtool),
194194
grep: Tool.init(greptool),
@@ -211,7 +211,7 @@ export const layer: Layer.Layer<
211211
builtin: [
212212
tool.invalid,
213213
...(questionEnabled ? [tool.question] : []),
214-
tool.bash,
214+
tool.shell,
215215
tool.read,
216216
tool.glob,
217217
tool.grep,

0 commit comments

Comments
 (0)