From bf50d1c028e973ccc0beffdf568fca417b62f020 Mon Sep 17 00:00:00 2001 From: James Long Date: Mon, 13 Apr 2026 13:33:13 -0400 Subject: feat(core): expose workspace adaptors to plugins (#21927) --- packages/plugin/src/index.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) (limited to 'packages/plugin/src/index.ts') diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index 1afb55daa..49d995c6f 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -24,11 +24,44 @@ export type ProviderContext = { options: Record } +export type WorkspaceInfo = { + id: string + type: string + name: string + branch: string | null + directory: string | null + extra: unknown | null + projectID: string +} + +export type WorkspaceTarget = + | { + type: "local" + directory: string + } + | { + type: "remote" + url: string | URL + headers?: HeadersInit + } + +export type WorkspaceAdaptor = { + name: string + description: string + configure(config: WorkspaceInfo): WorkspaceInfo | Promise + create(config: WorkspaceInfo, from?: WorkspaceInfo): Promise + remove(config: WorkspaceInfo): Promise + target(config: WorkspaceInfo): WorkspaceTarget | Promise +} + export type PluginInput = { client: ReturnType project: Project directory: string worktree: string + experimental_workspace: { + register(type: string, adaptor: WorkspaceAdaptor): void + } serverUrl: URL $: BunShell } -- cgit v1.2.3 From 34e2429c492495d059cbc63b86d02a58a1b3ca65 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 13 Apr 2026 20:14:53 -0500 Subject: feat: add experimental.compaction.autocontinue hook to disable auto continuing after compaction (#22361) --- packages/opencode/src/session/compaction.ts | 70 +++++++++++++++-------- packages/opencode/test/session/compaction.test.ts | 57 ++++++++++++++++++ packages/plugin/src/index.ts | 18 ++++++ 3 files changed, 120 insertions(+), 25 deletions(-) (limited to 'packages/plugin/src/index.ts') diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index b280971c7..c4934b625 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -310,31 +310,51 @@ When constructing the summary, try to stick to this template: } if (!replay) { - const continueMsg = yield* session.updateMessage({ - id: MessageID.ascending(), - role: "user", - sessionID: input.sessionID, - time: { created: Date.now() }, - agent: userMessage.agent, - model: userMessage.model, - }) - const text = - (input.overflow - ? "The previous request exceeded the provider's size limit due to large media attachments. The conversation was compacted and media files were removed from context. If the user was asking about attached images or files, explain that the attachments were too large to process and suggest they try again with smaller or fewer files.\n\n" - : "") + - "Continue if you have next steps, or stop and ask for clarification if you are unsure how to proceed." - yield* session.updatePart({ - id: PartID.ascending(), - messageID: continueMsg.id, - sessionID: input.sessionID, - type: "text", - synthetic: true, - text, - time: { - start: Date.now(), - end: Date.now(), - }, - }) + const info = yield* provider.getProvider(userMessage.model.providerID) + if ( + (yield* plugin.trigger( + "experimental.compaction.autocontinue", + { + sessionID: input.sessionID, + agent: userMessage.agent, + model: yield* provider.getModel(userMessage.model.providerID, userMessage.model.modelID), + provider: { + source: info.source, + info, + options: info.options, + }, + message: userMessage, + overflow: input.overflow === true, + }, + { enabled: true }, + )).enabled + ) { + const continueMsg = yield* session.updateMessage({ + id: MessageID.ascending(), + role: "user", + sessionID: input.sessionID, + time: { created: Date.now() }, + agent: userMessage.agent, + model: userMessage.model, + }) + const text = + (input.overflow + ? "The previous request exceeded the provider's size limit due to large media attachments. The conversation was compacted and media files were removed from context. If the user was asking about attached images or files, explain that the attachments were too large to process and suggest they try again with smaller or fewer files.\n\n" + : "") + + "Continue if you have next steps, or stop and ask for clarification if you are unsure how to proceed." + yield* session.updatePart({ + id: PartID.ascending(), + messageID: continueMsg.id, + sessionID: input.sessionID, + type: "text", + synthetic: true, + text, + time: { + start: Date.now(), + end: Date.now(), + }, + }) + } } } diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 2b0908ee9..206f417d1 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -244,6 +244,20 @@ function plugin(ready: ReturnType) { }) } +function autocontinue(enabled: boolean) { + return Layer.mock(Plugin.Service)({ + trigger: (name: Name, _input: Input, output: Output) => { + if (name !== "experimental.compaction.autocontinue") return Effect.succeed(output) + return Effect.sync(() => { + ;(output as { enabled: boolean }).enabled = enabled + return output + }) + }, + list: () => Effect.succeed([]), + init: () => Effect.void, + }) +} + describe("session.compaction.isOverflow", () => { test("returns true when token count exceeds usable context", async () => { await using tmp = await tmpdir() @@ -671,6 +685,49 @@ describe("session.compaction.process", () => { }) }) + test("allows plugins to disable synthetic continue prompt", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const session = await Session.create({}) + const msg = await user(session.id, "hello") + const rt = runtime("continue", autocontinue(false), wide()) + try { + const msgs = await Session.messages({ sessionID: session.id }) + const result = await rt.runPromise( + SessionCompaction.Service.use((svc) => + svc.process({ + parentID: msg.id, + messages: msgs, + sessionID: session.id, + auto: true, + }), + ), + ) + + const all = await Session.messages({ sessionID: session.id }) + const last = all.at(-1) + + expect(result).toBe("continue") + expect(last?.info.role).toBe("assistant") + expect( + all.some( + (msg) => + msg.info.role === "user" && + msg.parts.some( + (part) => + part.type === "text" && part.synthetic && part.text.includes("Continue if you have next steps"), + ), + ), + ).toBe(false) + } finally { + await rt.dispose() + } + }, + }) + }) + test("replays the prior user turn on overflow when earlier context exists", async () => { await using tmp = await tmpdir() await Instance.provide({ diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index 49d995c6f..d53c23a89 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -304,6 +304,24 @@ export interface Hooks { input: { sessionID: string }, output: { context: string[]; prompt?: string }, ) => Promise + /** + * Called after compaction succeeds and before a synthetic user + * auto-continue message is added. + * + * - `enabled`: Defaults to `true`. Set to `false` to skip the synthetic + * user "continue" turn. + */ + "experimental.compaction.autocontinue"?: ( + input: { + sessionID: string + agent: string + model: Model + provider: ProviderContext + message: UserMessage + overflow: boolean + }, + output: { enabled: boolean }, + ) => Promise "experimental.text.complete"?: ( input: { sessionID: string; messageID: string; partID: string }, output: { text: string }, -- cgit v1.2.3