diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index b2cc0f9bbc07..24b3da77c983 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -3,6 +3,7 @@ import { BusEvent } from "@/bus/bus-event" import { Config } from "@/config/config" import { InstanceState } from "@/effect/instance-state" import { makeRuntime } from "@/effect/run-service" +import { Plugin } from "@/plugin" import { ProjectID } from "@/project/schema" import { Instance } from "@/project/instance" import { MessageID, SessionID } from "@/session/schema" @@ -193,6 +194,7 @@ export namespace Permission { const deferred = yield* Deferred.make() pending.set(id, { info, deferred }) yield* bus.publish(Event.Asked, info) + Plugin.trigger("permission.request", info, {}).catch(() => {}) return yield* Effect.ensuring( Deferred.await(deferred), Effect.sync(() => { diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 3158393f1145..9a4ba4d1eeec 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -180,7 +180,7 @@ export namespace SessionCompaction { const model = agent.model ? yield* provider.getModel(agent.model.providerID, agent.model.modelID) : yield* provider.getModel(userMessage.model.providerID, userMessage.model.modelID) - // Allow plugins to inject context or replace compaction prompt. + yield* Effect.promise(() => Plugin.trigger("preCompact", { sessionID: input.sessionID }, {})) const compacting = yield* plugin.trigger( "experimental.session.compacting", { sessionID: input.sessionID }, @@ -342,7 +342,7 @@ When constructing the summary, try to stick to this template: } if (processor.message.error) return "stop" - if (result === "continue") yield* bus.publish(Event.Compacted, { sessionID: input.sessionID }) + if (result === "continue") { yield* bus.publish(Event.Compacted, { sessionID: input.sessionID }); yield* Effect.promise(() => Plugin.trigger("postCompact", { sessionID: input.sessionID, success: true }, {})) } return result }) diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 65032de96252..eb17e65ba5a0 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -8,6 +8,7 @@ import { type ProviderMetadata } from "ai" import { Config } from "../config/config" import { Flag } from "../flag/flag" import { Installation } from "../installation" +import { Plugin } from "@/plugin" import { Database, NotFoundError, eq, and, gte, isNull, desc, like, inArray, lt } from "../storage/db" import { SyncEvent } from "../sync" @@ -403,6 +404,10 @@ export namespace Session { yield* Effect.sync(() => SyncEvent.run(Event.Created, { sessionID: result.id, info: result })) + yield* Effect.promise(() => + Plugin.trigger("session.start", { sessionID: result.id, directory: result.directory, project: ctx.project }, {}), + ) + const cfg = yield* config.get() if (!result.parentID && (Flag.OPENCODE_AUTO_SHARE || cfg.share === "auto")) { yield* share(result.id).pipe(Effect.ignore, Effect.forkIn(scope)) @@ -469,6 +474,10 @@ export namespace Session { SyncEvent.run(Event.Deleted, { sessionID, info: session }) SyncEvent.remove(sessionID) }) + + yield* Effect.promise(() => + Plugin.trigger("session.end", { sessionID, directory: session.directory }, {}), + ) } catch (e) { log.error(e) } diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 146c73f27712..fd4ea32c4c2a 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -484,7 +484,22 @@ export namespace SessionProcessor { yield* abort() } if (ctx.needsCompaction) return "compact" - if (ctx.blocked || ctx.assistantMessage.error || aborted) return "stop" + if (ctx.blocked || ctx.assistantMessage.error || aborted) { + if (aborted) { + yield* Effect.promise(() => + Plugin.trigger("stop", { sessionID: ctx.sessionID, reason: "user_abort" }, {}), + ) + } else if (ctx.assistantMessage.error) { + yield* Effect.promise(() => + Plugin.trigger("stop", { sessionID: ctx.sessionID, reason: "error" }, {}), + ) + } else { + yield* Effect.promise(() => + Plugin.trigger("stop", { sessionID: ctx.sessionID, reason: "blocked" }, {}), + ) + } + return "stop" + } return "continue" }).pipe(Effect.onInterrupt(() => abort().pipe(Effect.asVoid))) }) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 24996c8d4b29..b5de9db42140 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -451,6 +451,10 @@ NOTE: At any point in time through this workflow you should feel free to ask the { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID }, { args }, ) + + yield* Effect.promise(() => + Plugin.trigger("preToolUse", { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID }, { args }), + ) const result = yield* Effect.promise(() => item.execute(args, ctx)) const output = { ...result, @@ -466,6 +470,14 @@ NOTE: At any point in time through this workflow you should feel free to ask the { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID, args }, output, ) + + yield* Effect.promise(() => + Plugin.trigger( + "postToolUse", + { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID, args, result: output }, + {}, + ), + ) return output }), ) @@ -489,19 +501,29 @@ NOTE: At any point in time through this workflow you should feel free to ask the { tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId }, { args }, ) + yield* Effect.promise(() => + Plugin.trigger("preToolUse", { tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId }, { args }), + ) yield* Effect.promise(() => ctx.ask({ permission: key, metadata: {}, patterns: ["*"], always: ["*"] })) - const result: Awaited>> = yield* Effect.promise(() => + const mcpResult: Awaited>> = yield* Effect.promise(() => execute(args, opts), ) yield* plugin.trigger( "tool.execute.after", { tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId, args }, - result, + mcpResult, + ) + yield* Effect.promise(() => + Plugin.trigger( + "postToolUse", + { tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId, args, result: mcpResult as any }, + {}, + ), ) const textParts: string[] = [] const attachments: Omit[] = [] - for (const contentItem of result.content) { + for (const contentItem of mcpResult.content) { if (contentItem.type === "text") textParts.push(contentItem.text) else if (contentItem.type === "image") { attachments.push({ @@ -525,7 +547,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the const truncated = yield* truncate.output(textParts.join("\n\n"), {}, input.agent) const metadata = { - ...(result.metadata ?? {}), + ...(mcpResult.metadata ?? {}), truncated: truncated.truncated, ...(truncated.truncated && { outputPath: truncated.outputPath }), } @@ -540,7 +562,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the sessionID: ctx.sessionID, messageID: input.processor.message.id, })), - content: result.content, + content: mcpResult.content, } }), ) diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index 473cac8a9bff..97468bcbf1e1 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -225,18 +225,18 @@ export interface Hooks { ) => Promise "tool.execute.before"?: ( input: { tool: string; sessionID: string; callID: string }, - output: { args: any }, + output: { args: Record }, ) => Promise "shell.env"?: ( input: { cwd: string; sessionID?: string; callID?: string }, output: { env: Record }, ) => Promise "tool.execute.after"?: ( - input: { tool: string; sessionID: string; callID: string; args: any }, + input: { tool: string; sessionID: string; callID: string; args: Record }, output: { title: string output: string - metadata: any + metadata: Record }, ) => Promise "experimental.chat.messages.transform"?: ( @@ -272,5 +272,75 @@ export interface Hooks { /** * Modify tool definitions (description and parameters) sent to LLM */ - "tool.definition"?: (input: { toolID: string }, output: { description: string; parameters: any }) => Promise + "tool.definition"?: (input: { toolID: string }, output: { description: string; parameters: Record }) => Promise + + /** + * Called when a new session starts + */ + "session.start"?: ( + input: { sessionID: string; directory: string; project?: Project }, + output: {}, + ) => Promise + + /** + * Called when a session ends + */ + "session.end"?: ( + input: { sessionID: string; directory: string }, + output: {}, + ) => Promise + + /** + * Called when the agent is stopped (user interrupt or completion) + */ + "stop"?: ( + input: { sessionID: string; reason?: string }, + output: {}, + ) => Promise + + /** + * Called before a tool is executed. Allows modifying arguments. + */ + "preToolUse"?: ( + input: { tool: string; sessionID: string; callID: string }, + output: { args: Record }, + ) => Promise + + /** + * Called after a tool executes successfully + */ + "postToolUse"?: ( + input: { + tool: string + sessionID: string + callID: string + args: Record + result: { title?: string; content?: string; base64?: string } + }, + output: {}, + ) => Promise + + /** + * Called when a permission request is made + */ + "permission.request"?: ( + input: Permission, + output: {}, + ) => Promise + + /** + * Called before session compaction + */ + "preCompact"?: ( + input: { sessionID: string }, + output: {}, + ) => Promise + + /** + * Called after session compaction + */ + "postCompact"?: ( + input: { sessionID: string; success: boolean }, + output: {}, + ) => Promise }