You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: packages/opencode/AGENTS.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -13,7 +13,7 @@
13
13
14
14
Use these rules when writing or migrating Effect code.
15
15
16
-
See `specs/effect-migration.md` for the compact pattern reference and examples.
16
+
See `specs/effect/migration.md` for the compact pattern reference and examples.
17
17
18
18
## Core
19
19
@@ -51,7 +51,7 @@ See `specs/effect-migration.md` for the compact pattern reference and examples.
51
51
52
52
## Effect.cached for deduplication
53
53
54
-
Use `Effect.cached` when multiple concurrent callers should share a single in-flight computation rather than storing `Fiber | undefined` or `Promise | undefined` manually. See `specs/effect-migration.md` for the full pattern.
54
+
Use `Effect.cached` when multiple concurrent callers should share a single in-flight computation rather than storing `Fiber | undefined` or `Promise | undefined` manually. See `specs/effect/migration.md` for the full pattern.
Practical notes for an eventual migration of `packages/opencode` server routes from the current Hono handlers to Effect `HttpApi`, either as a full replacement or as a parallel surface.
4
+
5
+
## Goal
6
+
7
+
Use Effect `HttpApi` where it gives us a better typed contract for:
8
+
9
+
- route definition
10
+
- request decoding and validation
11
+
- typed success and error responses
12
+
- OpenAPI generation
13
+
- handler composition inside Effect
14
+
15
+
This should be treated as a later-stage HTTP boundary migration, not a prerequisite for ongoing service, route-handler, or schema work.
16
+
17
+
## Core model
18
+
19
+
`HttpApi` is definition-first.
20
+
21
+
-`HttpApi` is the root API
22
+
-`HttpApiGroup` groups related endpoints
23
+
-`HttpApiEndpoint` defines a single route and its request / response schemas
24
+
- handlers are implemented separately from the contract
25
+
26
+
This is a better fit once route inputs and outputs are already moving toward Effect Schema-first models.
27
+
28
+
## Why it is relevant here
29
+
30
+
The current route-effectification work is already pushing handlers toward:
31
+
32
+
- one `AppRuntime.runPromise(Effect.gen(...))` body
33
+
- yielding services from context
34
+
- using typed Effect errors instead of Promise wrappers
35
+
36
+
That work is a good prerequisite for `HttpApi`. Once the handler body is already a composed Effect, the remaining migration is mostly about replacing the Hono route declaration and validator layer.
37
+
38
+
## What HttpApi gives us
39
+
40
+
### Contracts
41
+
42
+
Request params, query, payload, success payloads, and typed error payloads are declared in one place using Effect Schema.
43
+
44
+
### Validation and decoding
45
+
46
+
Incoming data is decoded through Effect Schema instead of hand-maintained Zod validators per route.
47
+
48
+
### OpenAPI
49
+
50
+
`HttpApi` can derive OpenAPI from the API definition, which overlaps with the current `describeRoute(...)` and `resolver(...)` pattern.
51
+
52
+
### Typed errors
53
+
54
+
`Schema.TaggedErrorClass` maps naturally to endpoint error contracts.
55
+
56
+
## Likely fit for opencode
57
+
58
+
Best fit first:
59
+
60
+
- JSON request / response endpoints
61
+
- route groups that already mostly delegate into services
62
+
- endpoints whose request and response models can be defined with Effect Schema
63
+
64
+
Harder / later fit:
65
+
66
+
- SSE endpoints
67
+
- websocket endpoints
68
+
- streaming handlers
69
+
- routes with heavy Hono-specific middleware assumptions
70
+
71
+
## Current blockers and gaps
72
+
73
+
### Schema split
74
+
75
+
Many route boundaries still use Zod-first validators. That does not block all experimentation, but full `HttpApi` adoption is easier after the domain and boundary types are more consistently Schema-first with `.zod` compatibility only where needed.
76
+
77
+
### Mixed handler styles
78
+
79
+
Many current `server/instance/*.ts` handlers still call async facades directly. Migrating those to composed `Effect.gen(...)` handlers is the low-risk step to do first.
80
+
81
+
### Non-JSON routes
82
+
83
+
The server currently includes SSE, websocket, and streaming-style endpoints. Those should not be the first `HttpApi` targets.
84
+
85
+
### Existing Hono integration
86
+
87
+
The current server composition, middleware, and docs flow are Hono-centered today. That suggests a parallel or incremental adoption plan is safer than a flag day rewrite.
88
+
89
+
## Recommended strategy
90
+
91
+
### 1. Finish the prerequisites first
92
+
93
+
- continue route-handler effectification in `server/instance/*.ts`
94
+
- continue schema migration toward Effect Schema-first DTOs and errors
95
+
- keep removing service facades
96
+
97
+
### 2. Start with one parallel group
98
+
99
+
Introduce one small `HttpApi` group for plain JSON endpoints only. Good initial candidates are the least stateful endpoints in:
100
+
101
+
-`server/instance/question.ts`
102
+
-`server/instance/provider.ts`
103
+
-`server/instance/permission.ts`
104
+
105
+
Avoid `session.ts`, SSE, websocket, and TUI-facing routes first.
106
+
107
+
### 3. Reuse existing services
108
+
109
+
Do not re-architect business logic during the HTTP migration. `HttpApi` handlers should call the same Effect services already used by the Hono handlers.
110
+
111
+
### 4. Run in parallel before replacing
112
+
113
+
Prefer mounting an experimental `HttpApi` surface alongside the existing Hono routes first. That lowers migration risk and lets us compare:
114
+
115
+
- handler ergonomics
116
+
- OpenAPI output
117
+
- auth and middleware integration
118
+
- test ergonomics
119
+
120
+
### 5. Migrate JSON route groups gradually
121
+
122
+
If the parallel slice works well, migrate additional JSON route groups one at a time. Leave streaming-style endpoints on Hono until there is a clear reason to move them.
123
+
124
+
## Proposed first steps
125
+
126
+
-[ ] add one small spike that defines an `HttpApi` group for a simple JSON route set
127
+
-[ ] use Effect Schema request / response types for that slice
128
+
-[ ] keep the underlying service calls identical to the current handlers
129
+
-[ ] compare generated OpenAPI against the current Hono/OpenAPI setup
130
+
-[ ] document how auth, instance lookup, and error mapping would compose in the new stack
131
+
-[ ] decide after the spike whether `HttpApi` should stay parallel, replace only some groups, or become the long-term default
132
+
133
+
## Rule of thumb
134
+
135
+
Do not start with the hardest route file.
136
+
137
+
If `HttpApi` is adopted here, it should arrive after the handler body is already Effect-native and after the relevant request / response models have moved to Effect Schema.
Copy file name to clipboardExpand all lines: packages/opencode/specs/effect/migration.md
+7-90Lines changed: 7 additions & 90 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -230,55 +230,9 @@ Still open at the service-shape level:
230
230
-[ ]`SyncEvent` — `sync/index.ts` (deferred pending sync with James)
231
231
-[ ]`Workspace` — `control-plane/workspace.ts` (deferred pending sync with James)
232
232
233
-
## Tool interface → Effect
233
+
## Tool migration
234
234
235
-
`Tool.Def.execute` and `Tool.Info.init` already return `Effect` on this branch, and the current tools in `src/tool/*.ts` have been migrated to the Effect-native `Tool.define(...)` shape.
236
-
237
-
The remaining work here is follow-on cleanup rather than the top-level tool interface migration:
238
-
239
-
1. Remove internal `Effect.promise(...)` bridges where practical
240
-
2. Keep replacing raw platform helpers with Effect services inside tool bodies
241
-
3. Update remaining callers and tests to prefer `yield* info.init()` / `Tool.init(...)` over older Promise-oriented patterns
242
-
243
-
### Tool migration details
244
-
245
-
With `Tool.Info.init()` now effectful, use this transitional pattern for migrated tools that still need Promise-based boundaries internally:
246
-
247
-
-`Tool.defineEffect(...)` should `yield*` the services the tool depends on and close over them in the returned tool definition.
248
-
- Keep the bridge at the Promise boundary only inside the tool body when required by external APIs. Do not return Promise-based init callbacks from `Tool.define()`.
249
-
- If a tool starts requiring new services, wire them into `ToolRegistry.defaultLayer` so production callers resolve the same dependencies as tests.
250
-
251
-
Tool tests should use the existing Effect helpers in `packages/opencode/test/lib/effect.ts`:
252
-
253
-
- Use `testEffect(...)` / `it.live(...)` instead of creating fake local wrappers around effectful tools.
254
-
- Yield the real tool export, then initialize it: `const info = yield* ReadTool`, `const tool = yield* info.init()`.
255
-
- Run tests inside a real instance with `provideTmpdirInstance(...)` or `provideInstance(tmpdirScoped(...))` so instance-scoped services resolve exactly as they do in production.
256
-
257
-
This keeps migrated tool tests aligned with the production service graph today, and makes the eventual `Tool.Info` → `Effect` cleanup mostly mechanical later.
258
-
259
-
Individual tools, ordered by value:
260
-
261
-
-[x]`apply_patch.ts` — HIGH: multi-step orchestration, error accumulation, Bus events
-[x]`webfetch.ts` — MEDIUM: fetch with UA retry, size limits → HttpClient
269
-
-[x]`websearch.ts` — MEDIUM: MCP over HTTP → HttpClient
270
-
-[x]`task.ts` — MEDIUM: task state management
271
-
-[x]`ls.ts` — MEDIUM: bounded directory listing over ripgrep-backed traversal
272
-
-[x]`multiedit.ts` — MEDIUM: sequential edit orchestration over `edit.ts`
273
-
-[x]`glob.ts` — LOW: simple async generator
274
-
-[x]`lsp.ts` — LOW: dispatch switch over LSP operations
275
-
-[x]`question.ts` — LOW: prompt wrapper
276
-
-[x]`skill.ts` — LOW: skill tool adapter
277
-
-[x]`todo.ts` — LOW: todo persistence wrapper
278
-
-[x]`invalid.ts` — LOW: invalid-tool fallback
279
-
-[x]`plan.ts` — LOW: plan file operations
280
-
281
-
`batch.ts` was removed from `src/tool/` and is no longer tracked here.
235
+
Tool-specific migration guidance and checklist live in `tools.md`.
282
236
283
237
## Effect service adoption in already-migrated code
284
238
@@ -298,11 +252,7 @@ Some already-effectified areas still use raw `Filesystem.*` or `Process.spawn` i
298
252
299
253
`util/filesystem.ts` is still used widely across `src/`, and raw `fs` / `fs/promises` imports still exist in multiple tooling and infrastructure files. As services and tools are effectified, they should switch from `Filesystem.*` to yielding `AppFileSystem.Service` where possible — this should happen naturally during each migration, not as a separate sweep.
300
254
301
-
Current raw fs users that will convert during tool migration:
302
-
303
-
-`tool/read.ts` — fs.createReadStream, readline
304
-
-`file/ripgrep.ts` — fs/promises
305
-
-`patch/index.ts` — fs, fs/promises
255
+
Tool-specific filesystem cleanup notes live in `tools.md`.
306
256
307
257
## Primitives & utilities
308
258
@@ -344,47 +294,14 @@ For each service, the migration is roughly:
344
294
-`ShareNext` — migrated 2026-04-11. Swapped remaining async callers to `AppRuntime.runPromise(ShareNext.Service.use(...))`, removed the `makeRuntime(...)` facade, and kept instance bootstrap on the shared app runtime.
345
295
-`SessionTodo` — migrated 2026-04-10. Already matched the target service shape in `session/todo.ts`: single namespace, traced Effect methods, and no `makeRuntime(...)` facade remained; checklist updated to reflect the completed migration.
346
296
-`Storage` — migrated 2026-04-10. One production caller (`Session.diff`) and all storage.test.ts tests converted to effectful style. Facades and `makeRuntime` removed.
347
-
-`SessionRunState` — migrated 2026-04-11. Single caller in `server/routes/session.ts` converted; facade removed.
348
-
-`Account` — migrated 2026-04-11. Callers in `server/routes/experimental.ts` and `cli/cmd/account.ts` converted; facade removed.
297
+
-`SessionRunState` — migrated 2026-04-11. Single caller in `server/instance/session.ts` converted; facade removed.
298
+
-`Account` — migrated 2026-04-11. Callers in `server/instance/experimental.ts` and `cli/cmd/account.ts` converted; facade removed.
-`FileWatcher` — migrated 2026-04-11. Callers in `project/bootstrap.ts` and test converted; facade removed.
352
-
-`Question` — migrated 2026-04-11. Callers in `server/routes/question.ts` and test converted; facade removed.
302
+
-`Question` — migrated 2026-04-11. Callers in `server/instance/question.ts` and test converted; facade removed.
353
303
-`Truncate` — migrated 2026-04-11. Caller in `tool/tool.ts` and test converted; facade removed.
354
304
355
305
## Route handler effectification
356
306
357
-
Route handlers should wrap their entire body in a single `AppRuntime.runPromise(Effect.gen(...))` call, yielding services from context rather than calling facades one-by-one. This eliminates multiple `runPromise` round-trips and lets handlers compose naturally.
When migrating, always use `{ concurrency: "unbounded" }` with `Effect.all` — route handlers should run independent service calls in parallel, not sequentially.
382
-
383
-
Route files to convert (each handler that calls facades should be wrapped):
Practical reference for converting server route handlers in `packages/opencode` to a single `AppRuntime.runPromise(Effect.gen(...))` body.
4
+
5
+
## Goal
6
+
7
+
Route handlers should wrap their entire body in a single `AppRuntime.runPromise(Effect.gen(...))` call, yielding services from context rather than calling facades one-by-one.
8
+
9
+
This eliminates multiple `runPromise` round-trips and lets handlers compose naturally.
- Wrap the whole handler body in one `AppRuntime.runPromise(Effect.gen(...))` call when the handler is service-heavy.
36
+
- Yield services from context instead of calling async facades repeatedly.
37
+
- When independent service calls can run in parallel, use `Effect.all(..., { concurrency: "unbounded" })`.
38
+
- Prefer one composed Effect body over multiple separate `runPromise(...)` calls in the same handler.
39
+
40
+
## Current route files
41
+
42
+
Current instance route files live under `src/server/instance`, not `server/routes`.
43
+
44
+
The main migration targets are:
45
+
46
+
-[ ]`server/instance/session.ts` — heaviest; still has many direct facade calls for Session, SessionPrompt, SessionRevert, SessionCompaction, SessionShare, SessionSummary, Agent, Bus
47
+
-[ ]`server/instance/global.ts` — still has direct facade calls for Config and instance lifecycle actions
48
+
-[ ]`server/instance/provider.ts` — still has direct facade calls for Config and Provider
49
+
-[ ]`server/instance/question.ts` — partially converted; still worth tracking here until it consistently uses the composed style
50
+
-[ ]`server/instance/pty.ts` — still calls Pty facades directly
51
+
-[ ]`server/instance/experimental.ts` — mixed state; some handlers are already composed, others still use facades
52
+
53
+
Additional route files that still participate in the migration:
54
+
55
+
-[ ]`server/instance/index.ts` — Vcs, Agent, Skill, LSP, Format
-[ ]`server/instance/middleware.ts` — Session and Workspace lookups
62
+
63
+
## Notes
64
+
65
+
- Some handlers already use `AppRuntime.runPromise(Effect.gen(...))` in isolated places. Keep pushing those files toward one consistent style.
66
+
- Route conversion is closely tied to facade removal. As services lose `makeRuntime`-backed async exports, route handlers should switch to yielding the service directly.
0 commit comments