Skip to content

feat(codemode): let extension globals return Effects - #59948

Merged
rekram1-node merged 6 commits into
devfrom
effect-extensions
Oct 28, 2026
Merged

rekram1-node merged 6 commits into
devfrom
effect-extensions

Conversation

@rekram1-node

@rekram1-node rekram1-node commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Code Mode globals such as fetch are 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. The extension.before hook 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.

today:  fetch(url) → request starts → extension.before → 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.before admits 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))
after:  fetch(url) → extension.before → Effect runs (can read Session) → result

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.ts is updated.

Out of scope

  • Moving core's fetch onto this and applying the webfetch permission rules to it. That comes in a follow-up PR stacked on this one.
  • Functions returned inside results (like res.json) can also return Effects, but their requirements aren't tracked in the type.

Verification

  • bun test in packages/codemode: 1469 pass
  • bun typecheck and bun run typecheck:tsc in packages/codemode, bun typecheck in packages/core: clean

Added 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

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):

  • Extension gains a Globals type parameter.
  • Extension.make is now generic.
  • New types: Extension.Services and CodeMode.Environment.
  • CodeMode.Options, ExecuteOptions, make and execute gain an Extensions type parameter, defaulting to ReadonlyArray<Extension>.
  • hooks is 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

Comment thread packages/codemode/src/codemode.ts Outdated
/** 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>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@rekram1-node
rekram1-node merged commit a882cf9 into dev Oct 28, 2026
20 of 21 checks passed
@rekram1-node
rekram1-node deleted the effect-extensions branch October 28, 2026 18:56
@amrkmn amrkmn mentioned this pull request Oct 29, 2026
1 task
AbizardShafwan pushed a commit to AbizardShafwan/opencode that referenced this pull request Nov 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant