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
35 changes: 28 additions & 7 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,13 +274,13 @@ export namespace SessionPrompt {
let msgs = await MessageV2.filterCompacted(MessageV2.stream(sessionID))

let lastUser: MessageV2.User | undefined
let lastAssistant: MessageV2.Assistant | undefined
let lastAssistant: MessageV2.WithParts | undefined
let lastFinished: MessageV2.Assistant | undefined
let tasks: (MessageV2.CompactionPart | MessageV2.SubtaskPart)[] = []
for (let i = msgs.length - 1; i >= 0; i--) {
const msg = msgs[i]
if (!lastUser && msg.info.role === "user") lastUser = msg.info as MessageV2.User
if (!lastAssistant && msg.info.role === "assistant") lastAssistant = msg.info as MessageV2.Assistant
if (!lastAssistant && msg.info.role === "assistant") lastAssistant = msg
if (!lastFinished && msg.info.role === "assistant" && msg.info.finish)
lastFinished = msg.info as MessageV2.Assistant
if (lastUser && lastFinished) break
Expand All @@ -292,12 +292,33 @@ export namespace SessionPrompt {

if (!lastUser) throw new Error("No user message found in stream. This should never happen.")
if (
lastAssistant?.finish &&
!["tool-calls", "unknown"].includes(lastAssistant.finish) &&
lastUser.id < lastAssistant.id
lastAssistant?.info.role === "assistant" &&
lastAssistant.info.finish &&
!["tool-calls", "unknown"].includes(lastAssistant.info.finish) &&
lastUser.id < lastAssistant.info.id
) {
log.info("exiting loop", { sessionID })
break
// Only exit if the last assistant message actually has text content or no tool calls
const hasText = lastAssistant.parts.some((p) => p.type === "text" && !p.synthetic)
const hasToolCalls = lastAssistant.parts.some((p) => p.type === "tool")

if (hasText || !hasToolCalls) {
log.info("exiting loop", {
sessionID,
finish: lastAssistant.info.finish,
lastUserID: lastUser.id,
lastAssistantID: lastAssistant.info.id,
hasText,
hasToolCalls,
})
break
}

log.info("continuing loop despite finish reason", {
sessionID,
finish: lastAssistant.info.finish,
hasText,
hasToolCalls,
})
}

step++
Expand Down
24 changes: 18 additions & 6 deletions packages/opencode/src/tool/bash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,20 @@ export const BashTool = Tool.define("bash", async () => {
.string()
.describe(
"Clear, concise description of what this command does in 5-10 words. Examples:\nInput: ls\nOutput: Lists files in current directory\n\nInput: git status\nOutput: Shows working tree status\n\nInput: npm install\nOutput: Installs package dependencies\n\nInput: mkdir foo\nOutput: Creates directory 'foo'",
),
)
.optional(),
}),
formatValidationError(error) {
if (error instanceof z.ZodError) {
return `Invalid parameters for tool 'bash':\n${(error as any).errors
.map((e: z.ZodIssue) => `- ${e.path.join(".")}: ${e.message}`)
.join("\n")}\n\nMake sure to provide 'command' and a concise 'description' of what the command does.`
}
return `Invalid parameters for tool 'bash': ${error}`
},
async execute(params, ctx) {
const cwd = params.workdir || Instance.directory
const description = params.description || params.command
if (params.timeout !== undefined && params.timeout < 0) {
throw new Error(`Invalid timeout value: ${params.timeout}. Timeout must be a positive number.`)
}
Expand Down Expand Up @@ -170,7 +180,7 @@ export const BashTool = Tool.define("bash", async () => {
ctx.metadata({
metadata: {
output: "",
description: params.description,
description,
},
})

Expand All @@ -180,7 +190,7 @@ export const BashTool = Tool.define("bash", async () => {
metadata: {
// truncate the metadata to avoid GIANT blobs of data (has nothing to do w/ what agent can access)
output: output.length > MAX_METADATA_LENGTH ? output.slice(0, MAX_METADATA_LENGTH) + "\n\n..." : output,
description: params.description,
description,
},
})
}
Expand Down Expand Up @@ -244,14 +254,16 @@ export const BashTool = Tool.define("bash", async () => {
output += "\n\n<bash_metadata>\n" + resultMetadata.join("\n") + "\n</bash_metadata>"
}

const finalOutput = output.trim() || "(Command executed successfully with no output)"

return {
title: params.description,
title: description,
metadata: {
output: output.length > MAX_METADATA_LENGTH ? output.slice(0, MAX_METADATA_LENGTH) + "\n\n..." : output,
exit: proc.exitCode,
description: params.description,
description,
},
output,
output: finalOutput,
}
},
}
Expand Down
3 changes: 2 additions & 1 deletion packages/opencode/src/tool/ls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ export const ListTool = Tool.define("list", {
return output
}

const output = `${searchPath}/\n` + renderDir(".", 0)
const output =
files.length > 0 ? `${searchPath}/\n` + renderDir(".", 0) : `(No files found in directory: ${searchPath})`

return {
title: path.relative(Instance.worktree, searchPath),
Expand Down
21 changes: 15 additions & 6 deletions packages/opencode/src/tool/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Config } from "../config/config"
import { PermissionNext } from "@/permission/next"

const parameters = z.object({
description: z.string().describe("A short (3-5 words) description of the task"),
description: z.string().describe("A short (3-5 words) description of the task").optional(),
prompt: z.string().describe("The task for the agent to perform"),
subagent_type: z.string().describe("The type of specialized agent to use for this task"),
session_id: z.string().describe("Existing Task session to continue").optional(),
Expand All @@ -38,8 +38,17 @@ export const TaskTool = Tool.define("task", async (ctx) => {
return {
description,
parameters,
formatValidationError(error) {
if (error instanceof z.ZodError) {
return `Invalid parameters for tool 'task':\n${(error as any).errors
.map((e: z.ZodIssue) => `- ${e.path.join(".")}: ${e.message}`)
.join("\n")}\n\nMake sure to provide 'prompt', 'subagent_type' and a concise 'description'.`
}
return `Invalid parameters for tool 'task': ${error}`
},
async execute(params: z.infer<typeof parameters>, ctx) {
const config = await Config.get()
const taskDescription = params.description || "Task"

// Skip permission check when user explicitly invoked via @ or command subtask
if (!ctx.extra?.bypassAgentCheck) {
Expand All @@ -48,7 +57,7 @@ export const TaskTool = Tool.define("task", async (ctx) => {
patterns: [params.subagent_type],
always: ["*"],
metadata: {
description: params.description,
description: taskDescription,
subagent_type: params.subagent_type,
},
})
Expand All @@ -67,7 +76,7 @@ export const TaskTool = Tool.define("task", async (ctx) => {

return await Session.create({
parentID: ctx.sessionID,
title: params.description + ` (@${agent.name} subagent)`,
title: taskDescription + ` (@${agent.name} subagent)`,
permission: [
{
permission: "todowrite",
Expand Down Expand Up @@ -100,7 +109,7 @@ export const TaskTool = Tool.define("task", async (ctx) => {
if (msg.info.role !== "assistant") throw new Error("Not an assistant message")

ctx.metadata({
title: params.description,
title: taskDescription,
metadata: {
sessionId: session.id,
},
Expand All @@ -122,7 +131,7 @@ export const TaskTool = Tool.define("task", async (ctx) => {
},
}
ctx.metadata({
title: params.description,
title: taskDescription,
metadata: {
summary: Object.values(parts).sort((a, b) => a.id.localeCompare(b.id)),
sessionId: session.id,
Expand Down Expand Up @@ -176,7 +185,7 @@ export const TaskTool = Tool.define("task", async (ctx) => {
const output = text + "\n\n" + ["<task_metadata>", `session_id: ${session.id}`, "</task_metadata>"].join("\n")

return {
title: params.description,
title: taskDescription,
metadata: {
summary,
sessionId: session.id,
Expand Down
8 changes: 8 additions & 0 deletions packages/opencode/src/tool/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ export const WriteTool = Tool.define("write", {
content: z.string().describe("The content to write to the file"),
filePath: z.string().describe("The absolute path to the file to write (must be absolute, not relative)"),
}),
formatValidationError(error) {
if (error instanceof z.ZodError) {
return `Invalid parameters for tool 'write':\n${(error as any).errors
.map((e: z.ZodIssue) => `- ${e.path.join(".")}: ${e.message}`)
.join("\n")}\n\nMake sure to provide 'content' and 'filePath'.`
}
return `Invalid parameters for tool 'write': ${error}`
},
async execute(params, ctx) {
const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath)
await assertExternalDirectory(ctx, filepath)
Expand Down