Skip to content
Merged
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
15 changes: 9 additions & 6 deletions packages/opencode/src/cli/cmd/debug/agent.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { EOL } from "os"
import { basename } from "path"
import { Effect } from "effect"
import { Agent } from "../../../agent/agent"
import { Provider } from "../../../provider/provider"
import { Session } from "../../../session"
Expand Down Expand Up @@ -158,13 +159,15 @@ async function createToolContext(agent: Agent.Info) {
abort: new AbortController().signal,
messages: [],
metadata: () => {},
async ask(req: Omit<Permission.Request, "id" | "sessionID" | "tool">) {
for (const pattern of req.patterns) {
const rule = Permission.evaluate(req.permission, pattern, ruleset)
if (rule.action === "deny") {
throw new Permission.DeniedError({ ruleset })
ask(req: Omit<Permission.Request, "id" | "sessionID" | "tool">) {
return Effect.sync(() => {
for (const pattern of req.patterns) {
const rule = Permission.evaluate(req.permission, pattern, ruleset)
if (rule.action === "deny") {
throw new Permission.DeniedError({ ruleset })
}
}
}
})
},
}
}
8 changes: 4 additions & 4 deletions packages/opencode/src/file/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export namespace FileTime {
readonly read: (sessionID: SessionID, file: string) => Effect.Effect<void>
readonly get: (sessionID: SessionID, file: string) => Effect.Effect<Date | undefined>
readonly assert: (sessionID: SessionID, filepath: string) => Effect.Effect<void>
readonly withLock: <T>(filepath: string, fn: () => Promise<T>) => Effect.Effect<T>
readonly withLock: <T>(filepath: string, fn: () => Effect.Effect<T>) => Effect.Effect<T>
}

export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/FileTime") {}
Expand Down Expand Up @@ -103,8 +103,8 @@ export namespace FileTime {
)
})

const withLock = Effect.fn("FileTime.withLock")(function* <T>(filepath: string, fn: () => Promise<T>) {
return yield* Effect.promise(fn).pipe((yield* getLock(filepath)).withPermits(1))
const withLock = Effect.fn("FileTime.withLock")(function* <T>(filepath: string, fn: () => Effect.Effect<T>) {
return yield* fn().pipe((yield* getLock(filepath)).withPermits(1))
})

return Service.of({ read, get, assert, withLock })
Expand All @@ -128,6 +128,6 @@ export namespace FileTime {
}

export async function withLock<T>(filepath: string, fn: () => Promise<T>): Promise<T> {
return runPromise((s) => s.withLock(filepath, fn))
return runPromise((s) => s.withLock(filepath, () => Effect.promise(fn)))
}
}
131 changes: 70 additions & 61 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ export namespace SessionPrompt {
const state = yield* SessionRunState.Service
const revert = yield* SessionRevert.Service

const run = {
promise: <A, E>(effect: Effect.Effect<A, E>) =>
Effect.runPromise(effect.pipe(Effect.provide(EffectLogger.layer))),
fork: <A, E>(effect: Effect.Effect<A, E>) =>
Effect.runFork(effect.pipe(Effect.provide(EffectLogger.layer))),
}

const cancel = Effect.fn("SessionPrompt.cancel")(function* (sessionID: SessionID) {
yield* elog.info("cancel", { sessionID })
yield* state.cancel(sessionID)
Expand Down Expand Up @@ -358,7 +365,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
agent: input.agent.name,
messages: input.messages,
metadata: (val) =>
Effect.runPromise(
run.promise(
input.processor.updateToolCall(options.toolCallId, (match) => {
if (!["running", "pending"].includes(match.state.status)) return match
return {
Expand All @@ -374,14 +381,14 @@ NOTE: At any point in time through this workflow you should feel free to ask the
}),
),
ask: (req) =>
Effect.runPromise(
permission.ask({
permission
.ask({
...req,
sessionID: input.session.id,
tool: { messageID: input.processor.message.id, callID: options.toolCallId },
ruleset: Permission.merge(input.agent.permission, input.session.permission ?? []),
}),
),
})
.pipe(Effect.orDie),
})

for (const item of yield* registry.tools({
Expand All @@ -395,15 +402,15 @@ NOTE: At any point in time through this workflow you should feel free to ask the
description: item.description,
inputSchema: jsonSchema(schema as any),
execute(args, options) {
return Effect.runPromise(
return run.promise(
Effect.gen(function* () {
const ctx = context(args, options)
yield* plugin.trigger(
"tool.execute.before",
{ tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID },
{ args },
)
const result = yield* Effect.promise(() => item.execute(args, ctx))
const result = yield* item.execute(args, ctx)
const output = {
...result,
attachments: result.attachments?.map((attachment) => ({
Expand Down Expand Up @@ -436,15 +443,15 @@ NOTE: At any point in time through this workflow you should feel free to ask the
const transformed = ProviderTransform.schema(input.model, schema)
item.inputSchema = jsonSchema(transformed)
item.execute = (args, opts) =>
Effect.runPromise(
run.promise(
Effect.gen(function* () {
const ctx = context(args, opts)
yield* plugin.trigger(
"tool.execute.before",
{ tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId },
{ args },
)
yield* Effect.promise(() => ctx.ask({ permission: key, metadata: {}, patterns: ["*"], always: ["*"] }))
yield* ctx.ask({ permission: key, metadata: {}, patterns: ["*"], always: ["*"] })
const result: Awaited<ReturnType<NonNullable<typeof execute>>> = yield* Effect.promise(() =>
execute(args, opts),
)
Expand Down Expand Up @@ -576,45 +583,46 @@ NOTE: At any point in time through this workflow you should feel free to ask the
}

let error: Error | undefined
const result = yield* Effect.promise((signal) =>
taskTool
.execute(taskArgs, {
agent: task.agent,
messageID: assistantMessage.id,
sessionID,
abort: signal,
callID: part.callID,
extra: { bypassAgentCheck: true, promptOps },
messages: msgs,
metadata(val: { title?: string; metadata?: Record<string, any> }) {
return Effect.runPromise(
Effect.gen(function* () {
part = yield* sessions.updatePart({
...part,
type: "tool",
state: { ...part.state, ...val },
} satisfies MessageV2.ToolPart)
}),
)
},
ask(req: any) {
return Effect.runPromise(
permission.ask({
...req,
sessionID,
ruleset: Permission.merge(taskAgent.permission, session.permission ?? []),
}),
)
},
})
.catch((e) => {
error = e instanceof Error ? e : new Error(String(e))
const taskAbort = new AbortController()
const result = yield* taskTool
.execute(taskArgs, {
agent: task.agent,
messageID: assistantMessage.id,
sessionID,
abort: taskAbort.signal,
callID: part.callID,
extra: { bypassAgentCheck: true, promptOps },
messages: msgs,
metadata(val: { title?: string; metadata?: Record<string, any> }) {
return run.promise(
Effect.gen(function* () {
part = yield* sessions.updatePart({
...part,
type: "tool",
state: { ...part.state, ...val },
} satisfies MessageV2.ToolPart)
}),
)
},
ask: (req: any) =>
permission
.ask({
...req,
sessionID,
ruleset: Permission.merge(taskAgent.permission, session.permission ?? []),
})
.pipe(Effect.orDie),
})
.pipe(
Effect.catchCause((cause) => {
const defect = Cause.squash(cause)
error = defect instanceof Error ? defect : new Error(String(defect))
log.error("subtask execution failed", { error, agent: task.agent, description: task.description })
return undefined
return Effect.void
}),
).pipe(
Effect.onInterrupt(() =>
Effect.gen(function* () {
Effect.onInterrupt(() =>
Effect.gen(function* () {
taskAbort.abort()
assistantMessage.finish = "tool-calls"
assistantMessage.time.completed = Date.now()
yield* sessions.updateMessage(assistantMessage)
Expand All @@ -630,9 +638,8 @@ NOTE: At any point in time through this workflow you should feel free to ask the
},
} satisfies MessageV2.ToolPart)
}
}),
),
)
})),
)

const attachments = result?.attachments?.map((attachment) => ({
...attachment,
Expand Down Expand Up @@ -855,7 +862,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
output += chunk
if (part.state.status === "running") {
part.state.metadata = { output, description: "" }
void Effect.runFork(sessions.updatePart(part))
void run.fork(sessions.updatePart(part))
}
}),
)
Expand Down Expand Up @@ -1037,19 +1044,21 @@ NOTE: At any point in time through this workflow you should feel free to ask the
if (yield* fsys.isDir(filepath)) part.mime = "application/x-directory"

const { read } = yield* registry.named()
const execRead = (args: Parameters<typeof read.execute>[0], extra?: Tool.Context["extra"]) =>
Effect.promise((signal: AbortSignal) =>
read.execute(args, {
const execRead = (args: Parameters<typeof read.execute>[0], extra?: Tool.Context["extra"]) => {
const controller = new AbortController()
return read
.execute(args, {
sessionID: input.sessionID,
abort: signal,
abort: controller.signal,
agent: input.agent!,
messageID: info.id,
extra: { bypassCwdCheck: true, ...extra },
messages: [],
metadata: async () => {},
ask: async () => {},
}),
)
metadata: () => {},
ask: () => Effect.void,
})
.pipe(Effect.onInterrupt(() => Effect.sync(() => controller.abort())))
}

if (part.mime === "text/plain") {
let offset: number | undefined
Expand Down Expand Up @@ -1655,9 +1664,9 @@ NOTE: At any point in time through this workflow you should feel free to ask the
})

const promptOps: TaskPromptOps = {
cancel: (sessionID) => Effect.runFork(cancel(sessionID)),
resolvePromptParts: (template) => Effect.runPromise(resolvePromptParts(template)),
prompt: (input) => Effect.runPromise(prompt(input)),
cancel: (sessionID) => run.fork(cancel(sessionID)),
resolvePromptParts: (template) => run.promise(resolvePromptParts(template)),
prompt: (input) => run.promise(prompt(input)),
}

return Service.of({
Expand Down
33 changes: 15 additions & 18 deletions packages/opencode/src/tool/apply_patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ const PatchParams = z.object({
patchText: z.string().describe("The full patch text that describes all changes to be made"),
})

export const ApplyPatchTool = Tool.defineEffect(
export const ApplyPatchTool = Tool.define(
"apply_patch",
Effect.gen(function* () {
const lsp = yield* LSP.Service
const afs = yield* AppFileSystem.Service
const format = yield* Format.Service
const bus = yield* Bus.Service

const run = Effect.fn("ApplyPatchTool.execute")(function* (params: z.infer<typeof PatchParams>, ctx: Tool.Context) {
if (!params.patchText) {
Expand Down Expand Up @@ -178,18 +179,16 @@ export const ApplyPatchTool = Tool.defineEffect(

// Check permissions if needed
const relativePaths = fileChanges.map((c) => path.relative(Instance.worktree, c.filePath).replaceAll("\\", "/"))
yield* Effect.promise(() =>
ctx.ask({
permission: "edit",
patterns: relativePaths,
always: ["*"],
metadata: {
filepath: relativePaths.join(", "),
diff: totalDiff,
files,
},
}),
)
yield* ctx.ask({
permission: "edit",
patterns: relativePaths,
always: ["*"],
metadata: {
filepath: relativePaths.join(", "),
diff: totalDiff,
files,
},
})

// Apply the changes
const updates: Array<{ file: string; event: "add" | "change" | "unlink" }> = []
Expand Down Expand Up @@ -228,13 +227,13 @@ export const ApplyPatchTool = Tool.defineEffect(

if (edited) {
yield* format.file(edited)
Bus.publish(File.Event.Edited, { file: edited })
yield* bus.publish(File.Event.Edited, { file: edited })
}
}

// Publish file change events
for (const update of updates) {
Bus.publish(FileWatcher.Event.Updated, update)
yield* bus.publish(FileWatcher.Event.Updated, update)
}

// Notify LSP of file changes and collect diagnostics
Expand Down Expand Up @@ -281,9 +280,7 @@ export const ApplyPatchTool = Tool.defineEffect(
return {
description: DESCRIPTION,
parameters: PatchParams,
async execute(params: z.infer<typeof PatchParams>, ctx) {
return Effect.runPromise(run(params, ctx).pipe(Effect.orDie))
},
execute: (params: z.infer<typeof PatchParams>, ctx: Tool.Context) => run(params, ctx).pipe(Effect.orDie),
}
}),
)
Loading
Loading