summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorShoubhit Dash <[email protected]>2026-04-15 03:59:12 +0530
committerGitHub <[email protected]>2026-04-15 03:59:12 +0530
commitf6409759e569cb3cf0479f9ba3453ff3b40ed1c2 (patch)
tree498484e2dd260d9d9f72fa8920c655c8fcafe895 /packages
parentf9d99f044df4d506aac897a1c27d1a0b1f894ae9 (diff)
downloadopencode-f6409759e569cb3cf0479f9ba3453ff3b40ed1c2.tar.gz
opencode-f6409759e569cb3cf0479f9ba3453ff3b40ed1c2.zip
fix: restore instance context in prompt runs (#22498)
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/effect/app-runtime.ts25
-rw-r--r--packages/opencode/src/session/prompt.ts31
-rw-r--r--packages/opencode/test/effect/app-runtime-logger.test.ts19
-rw-r--r--packages/opencode/test/session/prompt-effect.test.ts42
4 files changed, 103 insertions, 14 deletions
diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts
index 674ca1a2a..0d32bce08 100644
--- a/packages/opencode/src/effect/app-runtime.ts
+++ b/packages/opencode/src/effect/app-runtime.ts
@@ -1,5 +1,5 @@
import { Layer, ManagedRuntime } from "effect"
-import { memoMap } from "./run-service"
+import { attach, memoMap } from "./run-service"
import { Observability } from "./oltp"
import { AppFileSystem } from "@/filesystem"
@@ -97,4 +97,25 @@ export const AppLayer = Layer.mergeAll(
SessionShare.defaultLayer,
)
-export const AppRuntime = ManagedRuntime.make(AppLayer, { memoMap })
+const rt = ManagedRuntime.make(AppLayer, { memoMap })
+type Runtime = Pick<typeof rt, "runSync" | "runPromise" | "runPromiseExit" | "runFork" | "runCallback" | "dispose">
+const wrap = (effect: Parameters<typeof rt.runSync>[0]) => attach(effect as never) as never
+
+export const AppRuntime: Runtime = {
+ runSync(effect) {
+ return rt.runSync(wrap(effect))
+ },
+ runPromise(effect, options) {
+ return rt.runPromise(wrap(effect), options)
+ },
+ runPromiseExit(effect, options) {
+ return rt.runPromiseExit(wrap(effect), options)
+ },
+ runFork(effect) {
+ return rt.runFork(wrap(effect))
+ },
+ runCallback(effect) {
+ return rt.runCallback(wrap(effect))
+ },
+ dispose: () => rt.dispose(),
+}
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index a763b27b9..3efcc0365 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -104,12 +104,21 @@ export namespace SessionPrompt {
const summary = yield* SessionSummary.Service
const sys = yield* SystemPrompt.Service
const llm = yield* LLM.Service
- const ctx = yield* Effect.context()
-
- const run = {
- promise: <A, E>(effect: Effect.Effect<A, E>) => Effect.runPromiseWith(ctx)(effect),
- fork: <A, E>(effect: Effect.Effect<A, E>) => Effect.runForkWith(ctx)(effect),
- }
+ const runner = Effect.fn("SessionPrompt.runner")(function* () {
+ const ctx = yield* Effect.context()
+ return {
+ promise: <A, E>(effect: Effect.Effect<A, E>) => Effect.runPromiseWith(ctx)(effect),
+ fork: <A, E>(effect: Effect.Effect<A, E>) => Effect.runForkWith(ctx)(effect),
+ }
+ })
+ const ops = Effect.fn("SessionPrompt.ops")(function* () {
+ const run = yield* runner()
+ return {
+ cancel: (sessionID: SessionID) => run.fork(cancel(sessionID)),
+ resolvePromptParts: (template: string) => resolvePromptParts(template),
+ prompt: (input: PromptInput) => prompt(input),
+ } satisfies TaskPromptOps
+ })
const cancel = Effect.fn("SessionPrompt.cancel")(function* (sessionID: SessionID) {
yield* elog.info("cancel", { sessionID })
@@ -359,6 +368,8 @@ NOTE: At any point in time through this workflow you should feel free to ask the
}) {
using _ = log.time("resolveTools")
const tools: Record<string, AITool> = {}
+ const run = yield* runner()
+ const promptOps = yield* ops()
const context = (args: any, options: ToolExecutionOptions): Tool.Context => ({
sessionID: input.session.id,
@@ -528,6 +539,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
}) {
const { task, model, lastUser, sessionID, session, msgs } = input
const ctx = yield* InstanceState.context
+ const promptOps = yield* ops()
const { task: taskTool } = yield* registry.named()
const taskModel = task.model ? yield* getModel(task.model.providerID, task.model.modelID, sessionID) : model
const assistantMessage: MessageV2.Assistant = yield* sessions.updateMessage({
@@ -712,6 +724,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
const shellImpl = Effect.fn("SessionPrompt.shellImpl")(function* (input: ShellInput) {
const ctx = yield* InstanceState.context
+ const run = yield* runner()
const session = yield* sessions.get(input.sessionID)
if (session.revert) {
yield* revert.cleanup(session)
@@ -1659,12 +1672,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the
return result
})
- const promptOps: TaskPromptOps = {
- cancel: (sessionID) => run.fork(cancel(sessionID)),
- resolvePromptParts: (template) => resolvePromptParts(template),
- prompt: (input) => prompt(input),
- }
-
return Service.of({
cancel,
prompt,
diff --git a/packages/opencode/test/effect/app-runtime-logger.test.ts b/packages/opencode/test/effect/app-runtime-logger.test.ts
index c09775be3..8a7aab6cf 100644
--- a/packages/opencode/test/effect/app-runtime-logger.test.ts
+++ b/packages/opencode/test/effect/app-runtime-logger.test.ts
@@ -1,8 +1,11 @@
import { expect, test } from "bun:test"
import { Context, Effect, Layer, Logger } from "effect"
import { AppRuntime } from "../../src/effect/app-runtime"
+import { InstanceRef } from "../../src/effect/instance-ref"
import { EffectLogger } from "../../src/effect/logger"
import { makeRuntime } from "../../src/effect/run-service"
+import { Instance } from "../../src/project/instance"
+import { tmpdir } from "../fixture/fixture"
function check(loggers: ReadonlySet<Logger.Logger<unknown, any>>) {
return {
@@ -40,3 +43,19 @@ test("AppRuntime also installs EffectLogger through Observability.layer", async
expect(current.effectLogger).toBe(true)
expect(current.defaultLogger).toBe(false)
})
+
+test("AppRuntime attaches InstanceRef from ALS", async () => {
+ await using tmp = await tmpdir({ git: true })
+
+ const dir = await Instance.provide({
+ directory: tmp.path,
+ fn: () =>
+ AppRuntime.runPromise(
+ Effect.gen(function* () {
+ return (yield* InstanceRef)?.directory
+ }),
+ ),
+ })
+
+ expect(dir).toBe(tmp.path)
+})
diff --git a/packages/opencode/test/session/prompt-effect.test.ts b/packages/opencode/test/session/prompt-effect.test.ts
index 9523915bd..244f778ca 100644
--- a/packages/opencode/test/session/prompt-effect.test.ts
+++ b/packages/opencode/test/session/prompt-effect.test.ts
@@ -483,6 +483,48 @@ it.live("loop continues when finish is tool-calls", () =>
),
)
+it.live("glob tool keeps instance context during prompt runs", () =>
+ provideTmpdirServer(
+ ({ dir, llm }) =>
+ Effect.gen(function* () {
+ const prompt = yield* SessionPrompt.Service
+ const sessions = yield* Session.Service
+ const session = yield* sessions.create({
+ title: "Glob context",
+ permission: [{ permission: "*", pattern: "*", action: "allow" }],
+ })
+ const file = path.join(dir, "probe.txt")
+ yield* Effect.promise(() => Bun.write(file, "probe"))
+
+ yield* prompt.prompt({
+ sessionID: session.id,
+ agent: "build",
+ noReply: true,
+ parts: [{ type: "text", text: "find text files" }],
+ })
+ yield* llm.tool("glob", { pattern: "**/*.txt" })
+ yield* llm.text("done")
+
+ const result = yield* prompt.loop({ sessionID: session.id })
+ expect(result.info.role).toBe("assistant")
+
+ const msgs = yield* MessageV2.filterCompactedEffect(session.id)
+ const tool = msgs
+ .flatMap((msg) => msg.parts)
+ .find(
+ (part): part is CompletedToolPart =>
+ part.type === "tool" && part.tool === "glob" && part.state.status === "completed",
+ )
+ if (!tool) return
+
+ expect(tool.state.output).toContain(file)
+ expect(tool.state.output).not.toContain("No context found for instance")
+ expect(result.parts.some((part) => part.type === "text" && part.text === "done")).toBe(true)
+ }),
+ { git: true, config: providerCfg },
+ ),
+)
+
it.live("loop continues when finish is stop but assistant has tool parts", () =>
provideTmpdirServer(
Effect.fnUntraced(function* ({ llm }) {