Repository navigation
feat(codemode): let extension globals return Effects - #59948
Conversation
There was a problem hiding this comment.
This works as described. An Effect global starts only after extension.before admits the call, and it runs with the execution's services. I checked it with the added tests, the full codemode suite (1469 pass), tsc on codemode and core (both clean), and a few scratch checks of the typing.
Public API (the @opencode/codemode library API only; no server, SDK, config or CLI changes):
Extensiongains aGlobalstype parameter.Extension.makeis now generic.- New types:
Extension.ServicesandCodeMode.Environment. CodeMode.Options,ExecuteOptions,makeandexecutegain anExtensionstype parameter, defaulting toReadonlyArray<Extension>.hooksis now typed over the combined environment.
Existing callers keep compiling.
One gap worth closing before the follow-up: when a caller gives an explicit type argument, as core does with CodeMode.make<typeof tools>(...), the extensions' requirements are silently dropped (see inline comment).
Related: #54595
| /** Creates an Effect-native runtime over explicit, schema-described tools and extensions. */ | ||
| export const make = < | ||
| const Provided extends Record<string, unknown> = {}, | ||
| const Extensions extends ReadonlyArray<Extension> = ReadonlyArray<Extension>, |
There was a problem hiding this comment.
When a caller passes only the first type argument, TypeScript doesn't infer the rest, so Extensions falls back to ReadonlyArray<Extension> and Extension.Services becomes never. Core calls CodeMode.make<typeof tools>({ tools, extensions: [CodeModeWeb.extension], ... }) (packages/core/src/codemode/tool.ts:253), so once fetch uses Session there, the runtime would still type as needing no services. Forgetting provideService would then only show up at run time as ExecutionFailure: Service not found: …. I reproduced this with a scratch file. Passing extensions as a widened ReadonlyArray<Extension> has the same effect.
One option is to have core stop passing the explicit type argument (or pass both) in the follow-up. Another is to make the fallback not resolve to never, so the requirement can't be dropped without anyone noticing.
There was a problem hiding this comment.
The new commit, which keeps Extension.Services emittable, looks right: inference of the extensions' requirements still type-checks, the codemode tests pass (1469), and tsc is clean on codemode and core. It doesn't touch the earlier point about an explicit type argument (CodeMode.make<typeof tools>) dropping those requirements, so that thread stays open. The public API impact is the same as in my first review.
There was a problem hiding this comment.
The new commits fix the earlier point. With Extensions now defaulting to [], a call that names only the tools type but passes extensions no longer compiles, so their requirements can't be silently dropped. Core's call site is updated, and the new type assertion in the tests checks this. Codemode tests pass (1469), and tsc is clean on codemode and core.
Public API (the @opencode/codemode library only): on top of the earlier additions, the default of the Extensions type parameter on make, execute, Options and ExecuteOptions changes from ReadonlyArray<Extension> to []. A caller that passes an explicit tools type together with extensions now gets a compile error until it also names the extensions type. Core is the only such caller in the repo, and it is updated. Calls that rely on inference are unaffected.
There was a problem hiding this comment.
The two new commits only reorganise the extension call path: classifying a global's result now happens in callHost, and the async promise path is pending. Behaviour is the same, and an Effect global still starts only after extension.before admits the call. Codemode tests pass (1469), and tsc is clean on codemode and core. These commits leave the public API unchanged.
Problem
Code Mode globals such as
fetchare plain functions that run the moment the program calls them. They can't see anything about the execution (session, agent, message), so core has no way to apply session permissions to them the way it does for tools. Theextension.beforehook can't help either: the host function has already started by the time the hook runs, so a failing hook only throws away the result.Change
A global can now return an Effect. The interpreter turns it into a program promise like it already does for a Promise. An Effect is lazy, so it starts only after
extension.beforeadmits the call, and its requirements join the runtime's environment the same way a tool's do.const runtime = CodeMode.make({ extensions: [web] }) -// Runtime<never> +// Runtime<Session>: what web's Effect globals need runtime.execute(code).pipe(Effect.provideService(Session, session))Code Mode itself stays unaware of permissions. The host owns that, as it does for tools.
When a caller passes the tools type explicitly, it now has to pass the extensions type too. Otherwise the extensions' requirements could be silently dropped and only fail at run time with "Service not found". Core's one call site in
packages/core/src/codemode/tool.tsis updated.Out of scope
fetchonto this and applying thewebfetchpermission rules to it. That comes in a follow-up PR stacked on this one.res.json) can also return Effects, but their requirements aren't tracked in the type.Verification
bun testinpackages/codemode: 1469 passbun typecheckandbun run typecheck:tscinpackages/codemode,bun typecheckinpackages/core: cleanAdded by the opencode bot (AI-generated).
Related: #54595
Public API: adds an Extensions type parameter (default []) to CodeMode.make/execute/Options, a generic Extension.make, Extension.Services and CodeMode.Environment; callers passing an explicit tools type with extensions must now also name the extensions type