refactor(sdk): extract shared schema and embedded host

This commit is contained in:
Kit Langton
2026-06-23 17:22:11 -04:00
parent bbf1021d4a
commit 845c0d47f7
70 changed files with 724 additions and 565 deletions
+9 -5
View File
@@ -153,8 +153,10 @@ _Avoid_: Response envelope
- Promise client construction is synchronous and network-free. It requires `baseUrl`, defaults to `globalThis.fetch`, accepts client-level headers, and merges them with per-call header overrides.
- Effect client construction accepts an explicit `baseUrl` and obtains `HttpClient.HttpClient` from the Effect environment. It does not install fetch or duplicate per-call transport policy; callers transform/provide the client for headers, tracing, retries, recording, and tests, while fiber interruption owns cancellation.
- Promise and Effect emitters each own their generated public type modules. The **SDK Contract IR**, not a physically shared generated type package, is the common source; this permits zero-Effect wire types and rich decoded Effect types to evolve independently.
- Promise and Effect client outputs ship from `@opencode-ai/client` behind isolated root, `/effect`, and `/effect/embedded` exports. The root export has no runtime path to Effect. `/effect` imports only Effect, Model, and Protocol; `/effect/embedded` additionally owns the scoped Core/server host. Bundle-boundary tests enforce these graphs.
- `/effect` and `/effect/embedded` re-export their decoded datatype facade from the client package. The backing values live in Model, so callers do not depend on internal package locations or Core's versioned names.
- Promise and Effect network clients ship from `@opencode-ai/client` behind isolated root and `/effect` exports. The root has no runtime path to Effect; `/effect` imports only Effect, Schema, and Protocol.
- The Effect-native scoped host belongs to `@opencode-ai/sdk-next`, which will assume the existing `@opencode-ai/sdk` name after legacy consumers migrate. Client remains network-only and SDK depends one-way on Client.
- SDK executes Server's assembled `HttpRouter` in memory. It opens no listener and performs no network I/O, while preserving Server routing, middleware, codecs, handlers, and errors.
- The Effect Client and SDK re-export their decoded datatype facade from Schema so callers do not depend on internal package locations or Core's versioned names.
- A capability intended for both networked and **Embedded OpenCode** belongs in the authoritative public `HttpApi`; embedded-only same-process capabilities extend **Embedded OpenCode** separately.
- `sessions.events({ sessionID, after })` is a public durable Session event stream. It verifies the Session, replays durable events after the optional aggregate sequence, continues with newly committed durable events, excludes live-only fragments, and is transported as SSE in both networked and embedded modes.
- `events.subscribe()` is a distinct public instance-wide live stream for Session and non-Session activity. It has no replay guarantee and includes connection, heartbeat, and instance-disposal lifecycle events; consumers recover from disconnection by refreshing authoritative state.
@@ -174,7 +176,7 @@ _Avoid_: Response envelope
- The public operation remains `sessions.prompt(...)`; `SessionInput.admit` is the internal primitive, while the public `Admission` result and `resume` option express its durable admission semantics.
- `sessions.create(...)` accepts an optional `location`. Omission resolves through the connected OpenCode instance's default or current location; an explicit value selects a known location. Networked and embedded transports use the same handler semantics.
- `sessions.switchAgent({ sessionID, agent })` is part of the common client alongside `sessions.switchModel(...)`. It affects subsequent Session activity and fails with `SessionNotFoundError` for an unknown Session.
- A future Layer constructor may provide **Embedded OpenCode** using the same scoped creation path; it does not define a second implementation.
- The **Embedded OpenCode** Layer delegates to the same scoped creation path; it does not define a second implementation.
- A **PTY Environment** adapter observes plugins in the request Location while passing the resolved PTY working directory to the hook; standalone servers use an empty adapter.
- A **Mid-Conversation System Message** lowers to the provider's native chronological instruction role when supported and to a wrapped chronological fallback otherwise.
- When the effective aggregate instruction set changes, its **Mid-Conversation System Message** includes the complete current ordered set and supersedes the prior aggregate value; when no ambient instructions remain, the message states that previously loaded instructions no longer apply.
@@ -193,11 +195,13 @@ _Avoid_: Response envelope
## Client contract architecture
Semantic values that mean the same thing internally and publicly live in the lightweight Model leaf. Core consumes Model for domain behavior; Protocol composes Model values into paths, payloads, envelopes, errors, cursors, and streams; Server imports both, hosts Protocol's exact groups, and owns protocol/domain adaptation. The root Promise client remains zero-Effect, `/effect` depends on Effect plus Model and Protocol, and `/effect/embedded` keeps Core plus Server behind the same public client surface.
Semantic values that mean the same thing internally and publicly live in the lightweight Schema leaf. Core consumes Schema for domain behavior; Protocol composes Schema values into paths, payloads, envelopes, errors, cursors, and streams; Server imports both, hosts Protocol's exact groups, and owns protocol/domain adaptation. The root Promise client remains zero-Effect, `/effect` depends on Effect plus Schema and Protocol, and `@opencode-ai/sdk-next` composes the scoped in-process host above Client, Core, and Server.
Shared public records are plain objects declared with `Schema.Struct`. A same-name inferred interface gives object records readable TypeScript signatures without constructors, prototypes, or nominal identity; unions retain explicit type aliases.
Before stabilizing the client API:
- Keep additional public schemas in Model and additional network groups in Protocol; neither package may transitively load databases, Drizzle, Session execution, providers, watchers, native modules, or WASM.
- Keep additional public schemas in Schema and additional network groups in Protocol; neither package may transitively load databases, Drizzle, Session execution, providers, watchers, native modules, or WASM.
- Replace the transitional server-internal Session location middleware tag when the remaining location-scoped groups move to Protocol or gain a narrow request-location service contract.
- Project the existing list response envelope to the stable client **Page** shape and enforce separate initial-query and cursor-continuation inputs without changing the hosted V2 wire contract.
- Settle the stable consumer namespace (`session` versus the current beta `sessions`) and use an explicit codegen annotation if the consumer name should differ from the server group identifier.
+33 -19
View File
@@ -112,10 +112,8 @@
"packages/client": {
"name": "@opencode-ai/client",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/model": "workspace:*",
"@opencode-ai/protocol": "workspace:*",
"@opencode-ai/server": "workspace:*",
"@opencode-ai/schema": "workspace:*",
},
"devDependencies": {
"@effect/platform-node": "catalog:",
@@ -299,8 +297,8 @@
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
"@opencode-ai/effect-sqlite-node": "workspace:*",
"@opencode-ai/llm": "workspace:*",
"@opencode-ai/model": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/schema": "workspace:*",
"@openrouter/ai-sdk-provider": "2.9.0",
"@opentelemetry/api": "1.9.0",
"@opentelemetry/context-async-hooks": "2.6.1",
@@ -530,18 +528,6 @@
"@typescript/native-preview": "catalog:",
},
},
"packages/model": {
"name": "@opencode-ai/model",
"dependencies": {
"@noble/hashes": "catalog:",
"effect": "catalog:",
},
"devDependencies": {
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:",
},
},
"packages/opencode": {
"name": "opencode",
"version": "1.17.9",
@@ -702,7 +688,19 @@
"packages/protocol": {
"name": "@opencode-ai/protocol",
"dependencies": {
"@opencode-ai/model": "workspace:*",
"@opencode-ai/schema": "workspace:*",
"effect": "catalog:",
},
"devDependencies": {
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:",
},
},
"packages/schema": {
"name": "@opencode-ai/schema",
"dependencies": {
"@noble/hashes": "catalog:",
"effect": "catalog:",
},
"devDependencies": {
@@ -721,6 +719,20 @@
"@types/semver": "^7.5.8",
},
},
"packages/sdk-next": {
"name": "@opencode-ai/sdk-next",
"dependencies": {
"@opencode-ai/client": "workspace:*",
"@opencode-ai/core": "workspace:*",
"@opencode-ai/server": "workspace:*",
"effect": "catalog:",
},
"devDependencies": {
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:",
},
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.17.9",
@@ -1868,16 +1880,18 @@
"@opencode-ai/llm": ["@opencode-ai/llm@workspace:packages/llm"],
"@opencode-ai/model": ["@opencode-ai/model@workspace:packages/model"],
"@opencode-ai/plugin": ["@opencode-ai/plugin@workspace:packages/plugin"],
"@opencode-ai/protocol": ["@opencode-ai/protocol@workspace:packages/protocol"],
"@opencode-ai/schema": ["@opencode-ai/schema@workspace:packages/schema"],
"@opencode-ai/script": ["@opencode-ai/script@workspace:packages/script"],
"@opencode-ai/sdk": ["@opencode-ai/sdk@workspace:packages/sdk/js"],
"@opencode-ai/sdk-next": ["@opencode-ai/sdk-next@workspace:packages/sdk-next"],
"@opencode-ai/server": ["@opencode-ai/server@workspace:packages/server"],
"@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"],
+4 -31
View File
@@ -6,15 +6,14 @@ Private generation target for clients derived directly from OpenCode's authorita
- `@opencode-ai/client`: zero-Effect Promise client using `fetch`.
- `@opencode-ai/client/effect`: rich Effect network client using an environment-provided `HttpClient`.
- `@opencode-ai/client/effect/embedded`: scoped embedded OpenCode host backed by Core and the in-memory HTTP router.
The generated surface contains `sessions.list`, `create`, `get`, `switchAgent`, `switchModel`, `prompt`, `compact`, `wait`, and `context`. The server and generator consume the exact same hosted `SessionGroup`. Run `bun run generate` after changing that contract and `bun run check:generated` to detect committed-output drift.
The Effect entrypoints use canonical decoded values such as `Session.ID`, `Location.Ref`, and `Prompt`. These datatypes come from the lightweight `@opencode-ai/model` package and are re-exported from both Effect entrypoints so callers depend only on the client surface. The authoritative `SessionGroup` lives in `@opencode-ai/protocol`; Server hosts that exact group and adapts it to Core.
The Effect entrypoint uses canonical decoded values such as `Session.ID`, `Location.Ref`, and `Prompt`. These datatypes come from the lightweight `@opencode-ai/schema` package and are re-exported so callers depend only on the client surface. The authoritative `SessionGroup` lives in `@opencode-ai/protocol`; Server hosts that exact group and adapts it to Core.
The Promise root remains structural and has no Core or Effect runtime dependency. `/effect` depends only on Effect, Model, and Protocol and is browser-bundle safe. `/effect/embedded` intentionally retains Core and Server internally. Bundle-boundary tests enforce these import graphs while preserving the public root, `/effect`, and `/effect/embedded` entrypoints.
The Promise root remains structural and has no Core or Effect runtime dependency. `/effect` depends only on Effect, Schema, and Protocol and is browser-bundle safe. Bundle-boundary tests enforce both import graphs.
Until that extraction, Effect consumers construct canonical decoded inputs:
Effect consumers construct canonical decoded inputs:
```ts
import { AbsolutePath, Location, OpenCode, Prompt } from "@opencode-ai/client/effect"
@@ -24,31 +23,5 @@ yield *
client.sessions.create({
location: Location.Ref.make({ directory: AbsolutePath.make("/workspace") }),
})
yield * client.sessions.prompt({ sessionID, prompt: new Prompt({ text: "Hello" }) })
yield * client.sessions.prompt({ sessionID, prompt: Prompt.make({ text: "Hello" }) })
```
The embedded entrypoint exposes a scoped host backed by the same server router, middleware, handlers, and HTTP codecs as the network client:
```ts
import { OpenCode } from "@opencode-ai/client/effect/embedded"
const opencode = yield * OpenCode.create()
const session = yield * opencode.sessions.get({ sessionID })
```
It also exposes embedded-only `tools.register(...)`. Closing the owning Effect Scope releases the router resources, location services, fibers, and scoped tool registrations.
Effect applications can provide the same scoped constructor as a service Layer:
```ts
const program = Effect.gen(function* () {
const opencode = yield* OpenCode.Service
return yield* opencode.sessions.get({ sessionID })
})
yield * program.pipe(Effect.provide(OpenCode.layer))
```
`OpenCode.layer` is only a dependency-injection adapter over `OpenCode.create()`; it does not define another embedded implementation.
The beta embedded host currently assumes one active host per database. Multiple hosts sharing durable Session storage require shared process-local execution coordination and remain deferred together with embedded streaming support.
+3 -6
View File
@@ -6,8 +6,7 @@
"license": "MIT",
"exports": {
".": "./src/index.ts",
"./effect": "./src/effect.ts",
"./effect/embedded": "./src/effect-embedded.ts"
"./effect": "./src/effect.ts"
},
"scripts": {
"generate": "bun run script/build.ts",
@@ -16,10 +15,8 @@
"typecheck": "tsgo --noEmit"
},
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/model": "workspace:*",
"@opencode-ai/protocol": "workspace:*",
"@opencode-ai/server": "workspace:*"
"@opencode-ai/schema": "workspace:*",
"@opencode-ai/protocol": "workspace:*"
},
"peerDependencies": {
"effect": "4.0.0-beta.83"
+10 -9
View File
@@ -1,11 +1,12 @@
// TODO: Keep additional network capabilities inside Model and Protocol as the client grows; /effect must never import
// TODO: Keep additional network capabilities inside Schema and Protocol as the client grows; /effect must never import
// Core or Server. Preserve these datatype exports so internal model reorganizations do not require caller migrations.
export * from "./generated-effect/index"
export { Agent } from "@opencode-ai/model/agent"
export { Location } from "@opencode-ai/model/location"
export { Model } from "@opencode-ai/model/model"
export { AbsolutePath, RelativePath } from "@opencode-ai/model/schema"
export { Session } from "@opencode-ai/model/session"
export { SessionInput } from "@opencode-ai/model/session-input"
export { SessionMessage } from "@opencode-ai/model/session-message"
export { Prompt } from "@opencode-ai/model/prompt"
export { Agent } from "@opencode-ai/schema/agent"
export { Location } from "@opencode-ai/schema/location"
export { Model } from "@opencode-ai/schema/model"
export { Provider } from "@opencode-ai/schema/provider"
export { AbsolutePath, RelativePath } from "@opencode-ai/schema/schema"
export { Session } from "@opencode-ai/schema/session"
export { SessionInput } from "@opencode-ai/schema/session-input"
export { SessionMessage } from "@opencode-ai/schema/session-message"
export { Prompt } from "@opencode-ai/schema/prompt"
@@ -10,7 +10,7 @@ const Api = HttpApi.make("generated").add(SessionGroup)
type RawClient = HttpApiClient.ForApi<typeof Api>
const mapClientError = <E,>(error: E) =>
const mapClientError = <E>(error: E) =>
HttpClientError.isHttpClientError(error) || Schema.isSchemaError(error) || Sse.Retry.is(error)
? new ClientError({ cause: error })
: error
@@ -130,5 +130,7 @@ const adaptGroup0 = (raw: RawClient["server.session"]) => ({
context: Endpoint0_8(raw),
})
const adaptClient = (raw: RawClient) => ({ sessions: adaptGroup0(raw["server.session"]) })
export const make = (options?: { readonly baseUrl?: URL | string }) =>
HttpApiClient.make(Api, options).pipe(Effect.map((raw) => ({ sessions: adaptGroup0(raw["server.session"]) })))
HttpApiClient.make(Api, options).pipe(Effect.map(adaptClient))
+2 -2
View File
@@ -82,7 +82,7 @@ export function make(options: ClientOptions) {
throw new ClientError("UnexpectedStatus", { cause: { status: response.status } })
}
const request = async <A,>(descriptor: RequestDescriptor, requestOptions?: RequestOptions): Promise<A> => {
const request = async <A>(descriptor: RequestDescriptor, requestOptions?: RequestOptions): Promise<A> => {
const response = await execute(descriptor, requestOptions)
if (response.status !== descriptor.successStatus) return responseError(response, descriptor)
if (descriptor.empty) {
@@ -94,7 +94,7 @@ export function make(options: ClientOptions) {
return (await json(response)) as A
}
const sse = <A,>(descriptor: RequestDescriptor, requestOptions?: RequestOptions): AsyncIterable<A> => ({
const sse = <A>(descriptor: RequestDescriptor, requestOptions?: RequestOptions): AsyncIterable<A> => ({
async *[Symbol.asyncIterator]() {
const response = await execute(descriptor, requestOptions)
if (response.status !== descriptor.successStatus) await responseError(response, descriptor)
+25 -11
View File
@@ -1,4 +1,5 @@
import { expect, test } from "bun:test"
import { Schema } from "effect"
import { AgentV2 } from "@opencode-ai/core/agent"
import { Location as CoreLocation } from "@opencode-ai/core/location"
import { ModelV2 } from "@opencode-ai/core/model"
@@ -6,20 +7,20 @@ import { SessionV2 } from "@opencode-ai/core/session"
import { SessionInput as CoreSessionInput } from "@opencode-ai/core/session/input"
import { SessionMessage as CoreSessionMessage } from "@opencode-ai/core/session/message"
import { Prompt as CorePrompt } from "@opencode-ai/core/session/prompt"
import { Agent } from "@opencode-ai/model/agent"
import { Location } from "@opencode-ai/model/location"
import { Model } from "@opencode-ai/model/model"
import { Project } from "@opencode-ai/model/project"
import { Provider } from "@opencode-ai/model/provider"
import { Prompt } from "@opencode-ai/model/prompt"
import { Session } from "@opencode-ai/model/session"
import { SessionInput } from "@opencode-ai/model/session-input"
import { SessionMessage } from "@opencode-ai/model/session-message"
import { Workspace } from "@opencode-ai/model/workspace"
import { Agent } from "@opencode-ai/schema/agent"
import { Location } from "@opencode-ai/schema/location"
import { Model } from "@opencode-ai/schema/model"
import { Project } from "@opencode-ai/schema/project"
import { Provider } from "@opencode-ai/schema/provider"
import { Prompt } from "@opencode-ai/schema/prompt"
import { Session } from "@opencode-ai/schema/session"
import { SessionInput } from "@opencode-ai/schema/session-input"
import { SessionMessage } from "@opencode-ai/schema/session-message"
import { Workspace } from "@opencode-ai/schema/workspace"
import { SessionGroup } from "@opencode-ai/protocol/session"
import { SessionGroup as ServerSessionGroup } from "@opencode-ai/server/groups/session"
test("Core and Server reuse the authoritative Model and Protocol values", () => {
test("Core and Server reuse the authoritative Schema and Protocol values", () => {
expect(AgentV2.ID).toBe(Agent.ID)
expect(CoreLocation.Ref).toBe(Location.Ref)
expect(ModelV2.Ref).toBe(Model.Ref)
@@ -34,3 +35,16 @@ test("Core and Server reuse the authoritative Model and Protocol values", () =>
expect(Provider.ID.anthropic).toBe("anthropic")
expect(Workspace.ID.create()).toStartWith("wrk_")
})
test("shared DTO schemas construct and decode plain objects", () => {
const made = Prompt.make({ text: "hello" })
const decoded = Schema.decodeUnknownSync(Prompt)({ text: "hello" })
const content = Schema.decodeUnknownSync(SessionMessage.AssistantText)({ type: "text", id: "part_1", text: "hi" })
expect(Object.getPrototypeOf(made)).toBe(Object.prototype)
expect(Object.getPrototypeOf(decoded)).toBe(Object.prototype)
expect(Object.getPrototypeOf(content)).toBe(Object.prototype)
expect(Prompt.ast.annotations?.identifier).toBe("Prompt")
expect(SessionMessage.AssistantText.ast.annotations?.identifier).toBe("Session.Message.Assistant.Text")
expect(CoreSessionMessage.AssistantText).toBe(SessionMessage.AssistantText)
})
+6 -6
View File
@@ -1,7 +1,7 @@
import { expect, test } from "bun:test"
import { DateTime, Effect } from "effect"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
import { AbsolutePath, Agent, Location, Model, OpenCode, Prompt, Session, SessionInput } from "../src/effect"
import { AbsolutePath, Agent, Location, Model, OpenCode, Prompt, Session } from "../src/effect"
test("sessions.get returns the decoded Effect projection", async () => {
const httpClient = HttpClient.make((request) =>
@@ -47,7 +47,7 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
})
const admitted = yield* client.sessions.prompt({
sessionID: Session.ID.make("ses_test"),
prompt: new Prompt({ text: "Hello" }),
prompt: Prompt.make({ text: "Hello" }),
resume: false,
})
yield* client.sessions.compact({ sessionID: Session.ID.make("ses_test") })
@@ -57,11 +57,11 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
expect(DateTime.toEpochMillis(result.page.data[0].time.created)).toBe(1_717_171_717_000)
expect(result.page.data[0]).toBeInstanceOf(Session.Info)
expect(result.created).toBeInstanceOf(Session.Info)
expect(Object.getPrototypeOf(result.page.data[0])).toBe(Object.prototype)
expect(Object.getPrototypeOf(result.created)).toBe(Object.prototype)
expect(result.created.id).toBe("ses_test")
expect(result.admitted).toBeInstanceOf(SessionInput.Admitted)
expect(result.admitted.prompt).toBeInstanceOf(Prompt)
expect(Object.getPrototypeOf(result.admitted)).toBe(Object.prototype)
expect(Object.getPrototypeOf(result.admitted.prompt)).toBe(Object.prototype)
expect(DateTime.toEpochMillis(result.admitted.timeCreated)).toBe(1_717_171_717_000)
expect(result.context).toEqual([])
})
@@ -5,7 +5,7 @@ import { join, resolve, sep } from "node:path"
const directory = resolve(import.meta.dir, "..")
const effect = realpathSync(resolve(import.meta.dir, "../node_modules/effect"))
const model = resolve(import.meta.dir, "../../model")
const schema = resolve(import.meta.dir, "../../schema")
const protocol = resolve(import.meta.dir, "../../protocol")
const core = resolve(import.meta.dir, "../../core")
const server = resolve(import.meta.dir, "../../server")
@@ -15,7 +15,7 @@ describe("public import boundaries", () => {
const root = await bundleInputs("@opencode-ai/client", "browser")
expect(within(root, effect)).toEqual([])
expect(within(root, model)).toEqual([])
expect(within(root, schema)).toEqual([])
expect(within(root, protocol)).toEqual([])
expect(within(root, core)).toEqual([])
expect(within(root, server)).toEqual([])
@@ -23,15 +23,10 @@ describe("public import boundaries", () => {
const network = await bundleInputs("@opencode-ai/client/effect", "browser")
expect(within(network, effect).length).toBeGreaterThan(0)
expect(within(network, model).length).toBeGreaterThan(0)
expect(within(network, schema).length).toBeGreaterThan(0)
expect(within(network, protocol).length).toBeGreaterThan(0)
expect(within(network, core)).toEqual([])
expect(within(network, server)).toEqual([])
const embedded = await bundleInputs("@opencode-ai/client/effect/embedded", "bun")
expect(within(embedded, core).length).toBeGreaterThan(0)
expect(within(embedded, server).length).toBeGreaterThan(0)
})
})
+1 -1
View File
@@ -91,7 +91,7 @@
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
"@opencode-ai/effect-sqlite-node": "workspace:*",
"@opencode-ai/llm": "workspace:*",
"@opencode-ai/model": "workspace:*",
"@opencode-ai/schema": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
"@opentelemetry/api": "1.9.0",
"@opentelemetry/context-async-hooks": "2.6.1",
+1 -1
View File
@@ -1,7 +1,7 @@
export * as AgentV2 from "./agent"
import { Array, Context, Effect, Layer, Schema, Types } from "effect"
import { Agent } from "@opencode-ai/model/agent"
import { Agent } from "@opencode-ai/schema/agent"
import { ModelV2 } from "./model"
import { PermissionSchema } from "./permission/schema"
import { ProviderV2 } from "./provider"
+82 -26
View File
@@ -1,4 +1,4 @@
import { Effect, Layer, LayerMap } from "effect"
import { Context, Data, Effect, Layer, LayerMap, Scope } from "effect"
import { Location } from "./location"
import { Policy } from "./policy"
import { Config } from "./config"
@@ -48,12 +48,48 @@ import { SessionRunnerModel } from "./session/runner/model"
import { SystemContextBuiltIns } from "./system-context/builtins"
import { FetchHttpClient } from "effect/unstable/http"
export class LocationServiceMap extends LayerMap.Service<LocationServiceMap>()("@opencode/example/LocationServiceMap", {
lookup: (ref: Location.Ref) => {
class LocationRefKey extends Data.Class<{
readonly directory: Location.Ref["directory"]
readonly workspaceID: Location.Ref["workspaceID"] | null
}> {}
const locationRefKey = (ref: Location.Ref) =>
new LocationRefKey({ directory: ref.directory, workspaceID: ref.workspaceID ?? null })
const locationServiceDependencies = [
Project.defaultLayer,
EventV2.defaultLayer,
Credential.defaultLayer,
Npm.defaultLayer,
ModelsDev.defaultLayer,
FSUtil.defaultLayer,
Git.defaultLayer,
AppProcess.defaultLayer,
Global.defaultLayer,
Ripgrep.defaultLayer,
Database.defaultLayer,
ProjectDirectories.defaultLayer,
SessionStore.layer.pipe(Layer.provide(Database.defaultLayer)),
PermissionSaved.defaultLayer,
RepositoryCache.defaultLayer,
LLMClient.layer.pipe(Layer.provide(RequestExecutor.defaultLayer)),
FetchHttpClient.layer,
ToolOutputStore.defaultCleanupLayer,
] as const
class LocationServiceCache extends LayerMap.Service<LocationServiceCache>()("@opencode/example/LocationServiceCache", {
lookup: (ref: ReturnType<typeof locationRefKey>) => {
const locationRef = Location.Ref.make({
directory: ref.directory,
...(ref.workspaceID === null ? {} : { workspaceID: ref.workspaceID }),
})
const boot = Layer.effectDiscard(
Effect.logInfo("booting location services", { directory: ref.directory, workspaceID: ref.workspaceID }),
Effect.logInfo("booting location services", {
directory: locationRef.directory,
workspaceID: locationRef.workspaceID,
}),
)
const location = Location.layer(ref)
const location = Location.layer(locationRef)
const systemContext = SystemContextBuiltIns.locationLayer
const base = Layer.mergeAll(
location,
@@ -123,25 +159,45 @@ export class LocationServiceMap extends LayerMap.Service<LocationServiceMap>()("
).pipe(Layer.fresh)
},
idleTimeToLive: "60 minutes",
dependencies: [
Project.defaultLayer,
EventV2.defaultLayer,
Credential.defaultLayer,
Npm.defaultLayer,
ModelsDev.defaultLayer,
FSUtil.defaultLayer,
Git.defaultLayer,
AppProcess.defaultLayer,
Global.defaultLayer,
Ripgrep.defaultLayer,
Database.defaultLayer,
ProjectDirectories.defaultLayer,
SessionStore.layer.pipe(Layer.provide(Database.defaultLayer)),
PermissionSaved.defaultLayer,
RepositoryCache.defaultLayer,
LLMClient.layer.pipe(Layer.provide(RequestExecutor.defaultLayer)),
FetchHttpClient.layer,
ToolOutputStore.defaultCleanupLayer,
ApplicationTools.layer,
],
dependencies: [...locationServiceDependencies, ApplicationTools.layer],
}) {}
type LocationServices = Layer.Success<ReturnType<typeof LocationServiceCache.get>>
export interface LocationServiceMapService {
readonly get: (ref: Location.Ref) => Layer.Layer<LocationServices>
readonly contextEffect: (ref: Location.Ref) => Effect.Effect<Context.Context<LocationServices>, never, Scope.Scope>
readonly invalidate: (ref: Location.Ref) => Effect.Effect<void>
}
const locationServiceMapLayer = <E, R>(
service: Context.Service<LocationServiceMap, LocationServiceMapService>,
cache: Layer.Layer<LocationServiceCache, E, R>,
) =>
Layer.effect(
service,
Effect.map(LocationServiceCache, (locations) =>
service.of({
get: (ref) => locations.get(locationRefKey(ref)),
contextEffect: (ref) => locations.contextEffect(locationRefKey(ref)),
invalidate: (ref) => locations.invalidate(locationRefKey(ref)),
}),
),
).pipe(Layer.provide(cache))
export class LocationServiceMap extends Context.Service<LocationServiceMap, LocationServiceMapService>()(
"@opencode/example/LocationServiceMap",
) {
static readonly get = (ref: Location.Ref) =>
Layer.unwrap(Effect.map(LocationServiceMap, (locations) => locations.get(ref)))
static readonly contextEffect = (ref: Location.Ref) =>
Effect.flatMap(LocationServiceMap, (locations) => locations.contextEffect(ref))
static readonly invalidate = (ref: Location.Ref) =>
Effect.flatMap(LocationServiceMap, (locations) => locations.invalidate(ref))
static readonly layer: Layer.Layer<LocationServiceMap> = locationServiceMapLayer(this, LocationServiceCache.layer)
static readonly layerWithApplicationTools: Layer.Layer<LocationServiceMap, never, ApplicationTools.Service> =
locationServiceMapLayer(
this,
LocationServiceCache.layerNoDeps.pipe(Layer.provide(Layer.mergeAll(...locationServiceDependencies))),
)
}
+3 -3
View File
@@ -1,13 +1,13 @@
import { Context, Effect, Layer, Schema } from "effect"
import { Location as ModelLocation } from "@opencode-ai/model/location"
import { Location as SchemaLocation } from "@opencode-ai/schema/location"
import { Project } from "./project"
import { AbsolutePath, optionalOmitUndefined } from "./schema"
import { WorkspaceV2 } from "./workspace"
export * as Location from "./location"
export const Ref = ModelLocation.Ref
export type Ref = ModelLocation.Ref
export const Ref = SchemaLocation.Ref
export type Ref = SchemaLocation.Ref
export class Info extends Schema.Class<Info>("Location.Info")({
directory: AbsolutePath,
+1 -1
View File
@@ -1,5 +1,5 @@
import { Schema, Types } from "effect"
import { Model } from "@opencode-ai/model/model"
import { Model } from "@opencode-ai/schema/model"
import { ProviderV2 } from "./provider"
import { ModelRequest } from "./model-request"
+1 -1
View File
@@ -1,7 +1,7 @@
export * as ProjectSchema from "./schema"
import { Schema } from "effect"
import { Project } from "@opencode-ai/model/project"
import { Project } from "@opencode-ai/schema/project"
import { AbsolutePath } from "../schema"
export const ID = Project.ID
+1 -1
View File
@@ -1,7 +1,7 @@
export * as ProviderV2 from "./provider"
import { Schema, Types } from "effect"
import { Provider } from "@opencode-ai/model/provider"
import { Provider } from "@opencode-ai/schema/provider"
export const ID = Provider.ID
export type ID = typeof ID.Type
+1 -1
View File
@@ -28,7 +28,7 @@ const SessionsLayer = SessionV2.layer.pipe(
Layer.provide(EventV2.layer),
Layer.provide(Database.defaultLayer),
Layer.provide(ProjectV2.defaultLayer),
Layer.provide(LocationServiceMap.layer.pipe(Layer.provide(ApplicationTools.layer))),
Layer.provide(LocationServiceMap.layerWithApplicationTools.pipe(Layer.provide(ApplicationTools.layer))),
Layer.orDie,
)
// TODO: Accept explicit storage so tests and embeddings can select disposable or application-owned persistence.
+17 -36
View File
@@ -1,30 +1,27 @@
import { Schema } from "effect"
import {
AbsolutePath as ModelAbsolutePath,
AbsolutePath,
DateTimeUtcFromMillis,
externalID,
type ExternalID,
NonNegativeInt,
optionalOmitUndefined,
PositiveInt,
RelativePath as ModelRelativePath,
} from "@opencode-ai/model/schema"
import { Hash } from "./util/hash"
RelativePath,
withStatics,
} from "@opencode-ai/schema/schema"
export { NonNegativeInt, optionalOmitUndefined, PositiveInt }
export type ExternalID = {
readonly namespace: string
readonly key: string
export {
AbsolutePath,
DateTimeUtcFromMillis,
externalID,
NonNegativeInt,
optionalOmitUndefined,
PositiveInt,
RelativePath,
withStatics,
}
export const externalID = (prefix: string, input: ExternalID) =>
`${prefix}_${Hash.sha256(JSON.stringify([input.namespace, input.key]))}`
/**
* Integer greater than zero.
*/
export const RelativePath = ModelRelativePath
export type RelativePath = typeof RelativePath.Type
export const AbsolutePath = ModelAbsolutePath
export type AbsolutePath = typeof AbsolutePath.Type
export type { ExternalID }
/**
* Strip `readonly` from a nested type. Stand-in for `effect`'s `Types.DeepMutable`
@@ -54,22 +51,6 @@ export type DeepMutable<T> = T extends string | number | boolean | bigint | symb
? { -readonly [K in keyof T]: DeepMutable<T[K]> }
: T
/**
* Attach static methods to a schema object. Designed to be used with `.pipe()`:
*
* @example
* export const Foo = fooSchema.pipe(
* withStatics((schema) => ({
* zero: schema.make(0),
* from: Schema.decodeUnknownOption(schema),
* }))
* )
*/
export const withStatics =
<S extends object, M extends Record<string, unknown>>(methods: (schema: S) => M) =>
(schema: S): S & M =>
Object.assign(schema, methods(schema))
/**
* Nominal wrapper for scalar types. The class itself is a valid schema
* pass it directly to `Schema.decode`, `Schema.decodeEffect`, etc.
+2 -2
View File
@@ -2,7 +2,7 @@ export * as SessionV2 from "./session"
export * from "./session/schema"
import { DateTime, Effect, Layer, Schema, Context, Stream } from "effect"
import { Session as ModelSession } from "@opencode-ai/model/session"
import { Session as SchemaSession } from "@opencode-ai/schema/session"
import { and, asc, desc, eq, gt, like, lt, or, type SQL } from "drizzle-orm"
import { ProjectV2 } from "./project"
import { WorkspaceV2 } from "./workspace"
@@ -39,7 +39,7 @@ import { SessionInput } from "./session/input"
// - by subpath
// - by workspace (home is special)
export const ListAnchor = ModelSession.ListAnchor
export const ListAnchor = SchemaSession.ListAnchor
export type ListAnchor = typeof ListAnchor.Type
const ListInputBase = {
+2 -4
View File
@@ -2,12 +2,10 @@ import { Schema } from "effect"
import { ProviderMetadata, ToolContent } from "@opencode-ai/llm"
import { EventV2 } from "../event"
import { ModelV2 } from "../model"
import { NonNegativeInt } from "../schema"
import { V2Schema } from "../v2-schema"
import { DateTimeUtcFromMillis, NonNegativeInt, RelativePath } from "../schema"
import { FileAttachment, Prompt } from "./prompt"
import { SessionSchema } from "./schema"
import { Location } from "../location"
import { RelativePath } from "../schema"
import { SessionMessageID } from "./message-id"
export { FileAttachment }
@@ -22,7 +20,7 @@ export const Source = Schema.Struct({
export type Source = typeof Source.Type
const Base = {
timestamp: V2Schema.DateTimeUtcFromMillis,
timestamp: DateTimeUtcFromMillis,
sessionID: SessionSchema.ID,
}
const PromptFields = {
+6 -6
View File
@@ -2,7 +2,7 @@ export * as SessionInput from "./input"
import { and, asc, eq, isNull, lte } from "drizzle-orm"
import { DateTime, Effect, Schema } from "effect"
import { SessionInput as ModelSessionInput } from "@opencode-ai/model/session-input"
import { SessionInput as SchemaSessionInput } from "@opencode-ai/schema/session-input"
import type { Database } from "../database/database"
import type { EventV2 } from "../event"
import { SessionEvent } from "./event"
@@ -13,17 +13,17 @@ import { SessionInputTable, SessionMessageTable } from "./sql"
type DatabaseService = Database.Interface["db"]
export const Delivery = ModelSessionInput.Delivery
export const Delivery = SchemaSessionInput.Delivery
export type Delivery = typeof Delivery.Type
export const Admitted = ModelSessionInput.Admitted
export type Admitted = ModelSessionInput.Admitted
export const Admitted = SchemaSessionInput.Admitted
export type Admitted = SchemaSessionInput.Admitted
const decodePrompt = Schema.decodeUnknownSync(Prompt)
const encodePrompt = Schema.encodeSync(Prompt)
const fromRow = (row: typeof SessionInputTable.$inferSelect): Admitted =>
new Admitted({
Admitted.make({
admittedSeq: row.admitted_seq,
id: SessionMessage.ID.make(row.id),
sessionID: SessionSchema.ID.make(row.session_id),
@@ -68,7 +68,7 @@ export const admit = Effect.fn("SessionInput.admit")(function* (
event.durable === undefined
? Effect.die("Prompt admission event is missing aggregate sequence")
: Effect.succeed(
new Admitted({
Admitted.make({
admittedSeq: event.durable.seq,
id: input.id,
sessionID: input.sessionID,
+1 -1
View File
@@ -1,2 +1,2 @@
export * as SessionMessageID from "./message-id"
export { ID } from "@opencode-ai/model/session-message"
export { ID } from "@opencode-ai/schema/session-message"
+15 -15
View File
@@ -102,7 +102,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
yield* SessionEvent.All.match(event, {
"session.next.agent.switched": (event) => {
return adapter.appendMessage(
new SessionMessage.AgentSwitched({
SessionMessage.AgentSwitched.make({
id: event.data.messageID,
type: "agent-switched",
metadata: event.metadata,
@@ -113,7 +113,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
},
"session.next.model.switched": (event) => {
return adapter.appendMessage(
new SessionMessage.ModelSwitched({
SessionMessage.ModelSwitched.make({
id: event.data.messageID,
type: "model-switched",
metadata: event.metadata,
@@ -125,7 +125,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
"session.next.moved": () => Effect.void,
"session.next.prompted": (event) => {
return adapter.appendMessage(
new SessionMessage.User({
SessionMessage.User.make({
id: event.data.messageID,
type: "user",
metadata: event.metadata,
@@ -139,7 +139,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
"session.next.prompt.admitted": () => Effect.void,
"session.next.context.updated": (event) =>
adapter.appendMessage(
new SessionMessage.System({
SessionMessage.System.make({
id: event.data.messageID,
type: "system",
text: event.data.text,
@@ -148,7 +148,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
),
"session.next.synthetic": (event) => {
return adapter.appendMessage(
new SessionMessage.Synthetic({
SessionMessage.Synthetic.make({
sessionID: event.data.sessionID,
text: event.data.text,
id: event.data.messageID,
@@ -159,7 +159,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
},
"session.next.shell.started": (event) => {
return adapter.appendMessage(
new SessionMessage.Shell({
SessionMessage.Shell.make({
id: event.data.messageID,
type: "shell",
metadata: event.metadata,
@@ -194,7 +194,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
)
}
yield* adapter.appendMessage(
new SessionMessage.Assistant({
SessionMessage.Assistant.make({
id: event.data.assistantMessageID,
type: "assistant",
agent: event.data.agent,
@@ -225,7 +225,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
"session.next.text.started": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
draft.content.push(
castDraft(new SessionMessage.AssistantText({ type: "text", id: event.data.textID, text: "" })),
castDraft(SessionMessage.AssistantText.make({ type: "text", id: event.data.textID, text: "" })),
)
})
},
@@ -245,12 +245,12 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
draft.content.push(
castDraft(
new SessionMessage.AssistantTool({
SessionMessage.AssistantTool.make({
type: "tool",
id: event.data.callID,
name: event.data.name,
time: { created: event.data.timestamp },
state: new SessionMessage.ToolStatePending({ status: "pending", input: "" }),
state: SessionMessage.ToolStatePending.make({ status: "pending", input: "" }),
}),
),
)
@@ -270,7 +270,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
match.provider = event.data.provider
match.time.ran = event.data.timestamp
match.state = castDraft(
new SessionMessage.ToolStateRunning({
SessionMessage.ToolStateRunning.make({
status: "running",
input: event.data.input,
structured: {},
@@ -300,7 +300,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
}
match.time.completed = event.data.timestamp
match.state = castDraft(
new SessionMessage.ToolStateCompleted({
SessionMessage.ToolStateCompleted.make({
status: "completed",
input: match.state.input,
structured: event.data.structured,
@@ -323,7 +323,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
}
match.time.completed = event.data.timestamp
match.state = castDraft(
new SessionMessage.ToolStateError({
SessionMessage.ToolStateError.make({
status: "error",
error: event.data.error,
input: typeof match.state.input === "string" ? {} : match.state.input,
@@ -339,7 +339,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
draft.content.push(
castDraft(
new SessionMessage.AssistantReasoning({
SessionMessage.AssistantReasoning.make({
type: "reasoning",
id: event.data.reasoningID,
text: "",
@@ -369,7 +369,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
"session.next.compaction.delta": () => Effect.void,
"session.next.compaction.ended": (event) => {
return adapter.appendMessage(
new SessionMessage.Compaction({
SessionMessage.Compaction.make({
id: event.data.messageID,
type: "compaction",
metadata: event.metadata,
+1 -1
View File
@@ -1,2 +1,2 @@
export * as SessionMessage from "./message"
export * from "@opencode-ai/model/session-message"
export * from "@opencode-ai/schema/session-message"
+1 -1
View File
@@ -1 +1 @@
export { AgentAttachment, FileAttachment, Prompt, Source } from "@opencode-ai/model/prompt"
export { AgentAttachment, FileAttachment, Prompt, Source } from "@opencode-ai/schema/prompt"
+1 -1
View File
@@ -1,6 +1,6 @@
export * as SessionSchema from "./schema"
import { Session } from "@opencode-ai/model/session"
import { Session } from "@opencode-ai/schema/session"
import type { ExternalID } from "../schema"
export const ID = Session.ID
-3
View File
@@ -1,3 +0,0 @@
export { DateTimeUtcFromMillis } from "@opencode-ai/model/schema"
export * as V2Schema from "./v2-schema"
+1 -1
View File
@@ -1,6 +1,6 @@
export * as WorkspaceV2 from "./workspace"
import { Workspace } from "@opencode-ai/model/workspace"
import { Workspace } from "@opencode-ai/schema/workspace"
export const ID = Workspace.ID
export type ID = typeof ID.Type
+2 -3
View File
@@ -4,9 +4,8 @@ import { EventV2 } from "@opencode-ai/core/event"
import { Database } from "@opencode-ai/core/database/database"
import { EventSequenceTable, EventTable } from "@opencode-ai/core/event/sql"
import { Location } from "@opencode-ai/core/location"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { AbsolutePath, DateTimeUtcFromMillis } from "@opencode-ai/core/schema"
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
import { V2Schema } from "@opencode-ai/core/v2-schema"
import { eq } from "drizzle-orm"
import { location } from "./fixture/location"
import { testEffect } from "./lib/effect"
@@ -79,7 +78,7 @@ const SyncTimestamp = EventV2.define({
},
schema: {
id: Schema.String,
timestamp: V2Schema.DateTimeUtcFromMillis,
timestamp: DateTimeUtcFromMillis,
},
})
+20 -10
View File
@@ -1,7 +1,7 @@
import fs from "fs/promises"
import path from "path"
import { describe, expect } from "bun:test"
import { DateTime, Effect, Equal, Hash, Layer, Schema } from "effect"
import { DateTime, Effect, Layer, Schema } from "effect"
import { Tool } from "@opencode-ai/core/public"
import { define } from "@opencode-ai/plugin/v2/effect"
import { AgentV2 } from "@opencode-ai/core/agent"
@@ -34,7 +34,7 @@ const applicationTools = ApplicationTools.layer
const it = testEffect(
Layer.merge(
Layer.mergeAll(applicationTools, Database.defaultLayer, EventV2.defaultLayer),
LocationServiceMap.layer.pipe(
LocationServiceMap.layerWithApplicationTools.pipe(
Layer.provide(applicationTools),
Layer.provide(
Layer.mergeAll(
@@ -52,14 +52,24 @@ const it = testEffect(
)
describe("LocationServiceMap", () => {
it.effect("compares equivalent location refs by value", () =>
Effect.sync(() => {
const directory = AbsolutePath.make("/project")
expect(Equal.equals(Location.Ref.make({ directory }), Location.Ref.make({ directory }))).toBe(true)
expect(Hash.hash(Location.Ref.make({ directory }))).toBe(
Hash.hash(Location.Ref.make({ directory, workspaceID: undefined })),
)
}),
it.live("reuses cached services for equivalent location refs", () =>
Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
).pipe(
Effect.flatMap((dir) =>
Effect.scoped(
Effect.gen(function* () {
const locations = yield* LocationServiceMap
const directory = AbsolutePath.make(dir.path)
const first = yield* locations.contextEffect(Location.Ref.make({ directory }))
const second = yield* locations.contextEffect(Location.Ref.make({ directory, workspaceID: undefined }))
expect(first).toBe(second)
}),
),
),
),
)
it.live("isolates location state while sharing location policy with catalog", () =>
+2 -2
View File
@@ -218,7 +218,7 @@ describe("SessionV2.create", () => {
const events = yield* EventV2.Service
const { db } = yield* Database.Service
const created = yield* session.create({ location })
yield* session.prompt({ sessionID: created.id, prompt: new Prompt({ text: "Hello" }), resume: false })
yield* session.prompt({ sessionID: created.id, prompt: Prompt.make({ text: "Hello" }), resume: false })
yield* SessionInput.promoteSteers(db, events, created.id, Number.MAX_SAFE_INTEGER)
expect(
@@ -238,7 +238,7 @@ describe("SessionV2.create", () => {
const created = yield* session.create({ id: SessionV2.ID.make("ses_fresh_target_replay"), location })
const admitted = yield* session.prompt({
sessionID: created.id,
prompt: new Prompt({ text: "Replay lifecycle" }),
prompt: Prompt.make({ text: "Replay lifecycle" }),
resume: false,
})
yield* SessionInput.promoteSteers(sourceDb, sourceEvents, created.id, Number.MAX_SAFE_INTEGER)
+10 -10
View File
@@ -36,7 +36,7 @@ const assistantRow = (
id: _,
type,
...data
} = encodeMessage(new SessionMessage.Assistant({ id, type: "assistant", agent: "build", model, content: [], time }))
} = encodeMessage(SessionMessage.Assistant.make({ id, type: "assistant", agent: "build", model, content: [], time }))
return { id, session_id: sessionID, type, seq, time_created: DateTime.toEpochMillis(time.created), data }
}
@@ -69,7 +69,7 @@ describe("SessionProjector", () => {
sessionID,
messageID: SessionMessage.ID.make("msg_first"),
timestamp: created,
prompt: new Prompt({ text: "first" }),
prompt: Prompt.make({ text: "first" }),
delivery: "steer",
},
{ id: EventV2.ID.make("evt_z") },
@@ -80,7 +80,7 @@ describe("SessionProjector", () => {
sessionID,
messageID: SessionMessage.ID.make("msg_second"),
timestamp: created,
prompt: new Prompt({ text: "second" }),
prompt: Prompt.make({ text: "second" }),
delivery: "steer",
},
{ id: EventV2.ID.make("evt_a") },
@@ -145,7 +145,7 @@ describe("SessionProjector", () => {
const admitted = yield* SessionInput.admit(db, events, {
id,
sessionID,
prompt: new Prompt({ text: "promote me" }),
prompt: Prompt.make({ text: "promote me" }),
delivery: "steer",
})
if (!admitted) return yield* Effect.die("Prompt admission failed")
@@ -154,7 +154,7 @@ describe("SessionProjector", () => {
sessionID,
timestamp: admitted.timeCreated,
messageID: id,
prompt: new Prompt({ text: "promote me" }),
prompt: Prompt.make({ text: "promote me" }),
delivery: "steer",
})
@@ -334,7 +334,7 @@ describe("SessionProjector", () => {
it.effect("does not revive a stale incomplete in-memory assistant projection", () =>
Effect.gen(function* () {
const stale = new SessionMessage.Assistant({
const stale = SessionMessage.Assistant.make({
id: SessionMessage.ID.make("msg_assistant_stale"),
type: "assistant",
agent: "build",
@@ -342,7 +342,7 @@ describe("SessionProjector", () => {
content: [],
time: { created },
})
const completed = new SessionMessage.Assistant({
const completed = SessionMessage.Assistant.make({
id: SessionMessage.ID.make("msg_assistant_completed"),
type: "assistant",
agent: "build",
@@ -466,15 +466,15 @@ describe("SessionProjector", () => {
Schema.decodeUnknownSync(SessionMessage.Message)({ ...row.data, id: row.id, type: row.type }),
)
expect(messages).toEqual([
new SessionMessage.Assistant({
SessionMessage.Assistant.make({
id: SessionMessage.ID.make("msg_assistant_completed"),
type: "assistant",
agent: "build",
model,
content: [new SessionMessage.AssistantText({ type: "text", id: "text-stale", text: "" })],
content: [SessionMessage.AssistantText.make({ type: "text", id: "text-stale", text: "" })],
time: { created: DateTime.makeUnsafe(1), completed: DateTime.makeUnsafe(2) },
}),
new SessionMessage.Assistant({
SessionMessage.Assistant.make({
id: SessionMessage.ID.make("msg_assistant_stale"),
type: "assistant",
agent: "build",
+28 -23
View File
@@ -147,7 +147,7 @@ describe("SessionV2.prompt", () => {
const message = yield* session.prompt({
sessionID,
prompt: new Prompt({ text: "Fix the failing tests" }),
prompt: Prompt.make({ text: "Fix the failing tests" }),
resume: false,
})
@@ -171,8 +171,8 @@ describe("SessionV2.prompt", () => {
const fiber = yield* session.events({ sessionID }).pipe(Stream.take(4), Stream.runCollect, Effect.forkScoped)
yield* Effect.yieldNow
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false })
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
yield* SessionInput.promoteSteers(db, events, sessionID, Number.MAX_SAFE_INTEGER)
const streamed = Array.from(yield* Fiber.join(fiber))
@@ -198,7 +198,7 @@ describe("SessionV2.prompt", () => {
const session = yield* SessionV2.Service
const message = yield* session.prompt({
sessionID,
prompt: new Prompt({ text: "Fix the failing tests" }),
prompt: Prompt.make({ text: "Fix the failing tests" }),
resume: false,
})
@@ -217,7 +217,7 @@ describe("SessionV2.prompt", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
const input = { sessionID, prompt: new Prompt({ text: "Fix the failing tests" }), resume: false }
const input = { sessionID, prompt: Prompt.make({ text: "Fix the failing tests" }), resume: false }
const first = yield* session.prompt(input)
const second = yield* session.prompt(input)
@@ -235,7 +235,7 @@ describe("SessionV2.prompt", () => {
const input = {
sessionID,
id: messageID,
prompt: new Prompt({ text: "Fix the failing tests" }),
prompt: Prompt.make({ text: "Fix the failing tests" }),
resume: false,
}
@@ -255,7 +255,7 @@ describe("SessionV2.prompt", () => {
const input = {
sessionID,
id: messageID,
prompt: new Prompt({ text: "Recover committed prompt" }),
prompt: Prompt.make({ text: "Recover committed prompt" }),
resume: false,
}
const first = yield* session.prompt(input)
@@ -276,13 +276,13 @@ describe("SessionV2.prompt", () => {
yield* session.prompt({
sessionID,
id: messageID,
prompt: new Prompt({ text: "Fix the failing tests" }),
prompt: Prompt.make({ text: "Fix the failing tests" }),
})
const failure = yield* session
.prompt({
sessionID,
id: messageID,
prompt: new Prompt({ text: "Delete the failing tests" }),
prompt: Prompt.make({ text: "Delete the failing tests" }),
resume: false,
})
.pipe(Effect.flip)
@@ -301,14 +301,14 @@ describe("SessionV2.prompt", () => {
yield* session.prompt({
id: messageID,
sessionID,
prompt: new Prompt({ text: "Fix the failing tests" }),
prompt: Prompt.make({ text: "Fix the failing tests" }),
resume: false,
})
const failure = yield* session
.prompt({
id: messageID,
sessionID,
prompt: new Prompt({ text: "Fix the failing tests" }),
prompt: Prompt.make({ text: "Fix the failing tests" }),
delivery: "queue",
resume: false,
})
@@ -325,7 +325,7 @@ describe("SessionV2.prompt", () => {
const input = {
sessionID,
id: messageID,
prompt: new Prompt({ text: "Fix the failing tests" }),
prompt: Prompt.make({ text: "Fix the failing tests" }),
resume: false,
}
@@ -344,7 +344,7 @@ describe("SessionV2.prompt", () => {
const { db } = yield* Database.Service
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
yield* session.prompt({ id: messageID, sessionID, prompt: new Prompt({ text: "Promote once" }), resume: false })
yield* session.prompt({ id: messageID, sessionID, prompt: Prompt.make({ text: "Promote once" }), resume: false })
yield* Effect.all(
[
@@ -368,9 +368,9 @@ describe("SessionV2.prompt", () => {
const { db } = yield* Database.Service
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
const first = yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Before cutoff" }), resume: false })
const first = yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Before cutoff" }), resume: false })
const cutoff = first.admittedSeq
const second = yield* session.prompt({ sessionID, prompt: new Prompt({ text: "After cutoff" }), resume: false })
const second = yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "After cutoff" }), resume: false })
yield* SessionInput.promoteSteers(db, events, sessionID, cutoff)
@@ -386,7 +386,12 @@ describe("SessionV2.prompt", () => {
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
wakeCalls.length = 0
yield* session.prompt({ id: messageID, sessionID, prompt: new Prompt({ text: "Replay pending" }), resume: false })
yield* session.prompt({
id: messageID,
sessionID,
prompt: Prompt.make({ text: "Replay pending" }),
resume: false,
})
const recorded = yield* db
.select()
.from(EventTable)
@@ -422,7 +427,7 @@ describe("SessionV2.prompt", () => {
yield* setup
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
const prompt = new Prompt({ text: "Historical prompt" })
const prompt = Prompt.make({ text: "Historical prompt" })
yield* events.publish(SessionEvent.Prompted, {
sessionID,
messageID,
@@ -443,7 +448,7 @@ describe("SessionV2.prompt", () => {
yield* setup
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
const prompt = new Prompt({ text: "Historical queued prompt" })
const prompt = Prompt.make({ text: "Historical queued prompt" })
yield* events.publish(SessionEvent.Prompted, {
sessionID,
messageID,
@@ -478,7 +483,7 @@ describe("SessionV2.prompt", () => {
.onConflictDoNothing()
.run()
.pipe(Effect.orDie)
const prompt = new Prompt({ text: "Fix the failing tests" })
const prompt = Prompt.make({ text: "Fix the failing tests" })
yield* session.prompt({ id: messageID, sessionID, prompt, resume: false })
const failure = yield* session
@@ -502,7 +507,7 @@ describe("SessionV2.prompt", () => {
})
const failure = yield* session
.prompt({ id: messageID, sessionID, prompt: new Prompt({ text: "Conflicting prompt" }), resume: false })
.prompt({ id: messageID, sessionID, prompt: Prompt.make({ text: "Conflicting prompt" }), resume: false })
.pipe(Effect.flip)
expect(failure).toMatchObject({ _tag: "Session.PromptConflictError", sessionID, messageID })
@@ -517,7 +522,7 @@ describe("SessionV2.prompt", () => {
executionCalls.length = 0
wakeCalls.length = 0
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Run by default" }) })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Run by default" }) })
expect(executionCalls).toEqual([])
expect(wakeCalls).toEqual([sessionID])
@@ -533,7 +538,7 @@ describe("SessionV2.prompt", () => {
yield* session.prompt({
sessionID,
prompt: new Prompt({ text: "Run explicitly" }),
prompt: Prompt.make({ text: "Run explicitly" }),
resume: true,
})
@@ -549,7 +554,7 @@ describe("SessionV2.prompt", () => {
executionCalls.length = 0
wakeCalls.length = 0
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Do not run" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Do not run" }), resume: false })
expect(executionCalls).toEqual([])
expect(wakeCalls).toEqual([])
@@ -16,7 +16,7 @@ const model = Model.make({ id: "model", provider: "provider", route: OpenAIChat.
describe("toLLMMessages", () => {
test("omits empty assistant turns", () => {
const assistant = (value: string, content: SessionMessage.Assistant["content"]) =>
new SessionMessage.Assistant({
SessionMessage.Assistant.make({
id: id(value),
type: "assistant",
agent: "build",
@@ -27,13 +27,13 @@ describe("toLLMMessages", () => {
const messages = toLLMMessages(
[
assistant("empty", []),
assistant("empty-text", [new SessionMessage.AssistantText({ type: "text", id: "empty", text: "" })]),
assistant("empty-text", [SessionMessage.AssistantText.make({ type: "text", id: "empty", text: "" })]),
assistant("empty-reasoning", [
new SessionMessage.AssistantReasoning({ type: "reasoning", id: "empty-reasoning", text: "" }),
SessionMessage.AssistantReasoning.make({ type: "reasoning", id: "empty-reasoning", text: "" }),
]),
assistant("text", [new SessionMessage.AssistantText({ type: "text", id: "text", text: "Partial" })]),
assistant("text", [SessionMessage.AssistantText.make({ type: "text", id: "text", text: "Partial" })]),
assistant("reasoning", [
new SessionMessage.AssistantReasoning({
SessionMessage.AssistantReasoning.make({
type: "reasoning",
id: "reasoning",
text: "",
@@ -48,43 +48,43 @@ describe("toLLMMessages", () => {
})
test("maps every top-level V2 Session message type", () => {
const file = new FileAttachment({ uri: "data:image/png;base64,aGVsbG8=", mime: "image/png", name: "hello.png" })
const file = FileAttachment.make({ uri: "data:image/png;base64,aGVsbG8=", mime: "image/png", name: "hello.png" })
const messages = toLLMMessages(
[
new SessionMessage.AgentSwitched({
SessionMessage.AgentSwitched.make({
id: id("agent"),
type: "agent-switched",
agent: "build",
time: { created },
}),
new SessionMessage.ModelSwitched({
SessionMessage.ModelSwitched.make({
id: id("model"),
type: "model-switched",
model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") },
time: { created },
}),
new SessionMessage.System({
SessionMessage.System.make({
id: id("system"),
type: "system",
text: "Updated context\n\nOther context",
time: { created },
}),
new SessionMessage.User({
SessionMessage.User.make({
id: id("user"),
type: "user",
text: "Inspect this image",
files: [file],
agents: [new AgentAttachment({ name: "build" })],
agents: [AgentAttachment.make({ name: "build" })],
time: { created },
}),
new SessionMessage.Synthetic({
SessionMessage.Synthetic.make({
id: id("synthetic"),
type: "synthetic",
sessionID: SessionV2.ID.make("ses_translate"),
text: "Synthetic context",
time: { created },
}),
new SessionMessage.Shell({
SessionMessage.Shell.make({
id: id("shell"),
type: "shell",
callID: "shell-1",
@@ -92,7 +92,7 @@ describe("toLLMMessages", () => {
output: "/project",
time: { created, completed: created },
}),
new SessionMessage.Compaction({
SessionMessage.Compaction.make({
id: id("compaction"),
type: "compaction",
reason: "auto",
@@ -142,31 +142,31 @@ Recent work
test("replays durable tool media into canonical tool messages without structured base64", () => {
const messages = toLLMMessages(
[
new SessionMessage.Assistant({
SessionMessage.Assistant.make({
id: id("assistant"),
type: "assistant",
agent: "build",
model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") },
content: [
new SessionMessage.AssistantText({ type: "text", id: "text-1", text: "Checking" }),
new SessionMessage.AssistantReasoning({
SessionMessage.AssistantText.make({ type: "text", id: "text-1", text: "Checking" }),
SessionMessage.AssistantReasoning.make({
type: "reasoning",
id: "reasoning-1",
text: "Think",
providerMetadata: { anthropic: { signature: "sig_1" } },
}),
new SessionMessage.AssistantTool({
SessionMessage.AssistantTool.make({
type: "tool",
id: "pending",
name: "read",
state: new SessionMessage.ToolStatePending({ status: "pending", input: '{"path":"README.md"}' }),
state: SessionMessage.ToolStatePending.make({ status: "pending", input: '{"path":"README.md"}' }),
time: { created },
}),
new SessionMessage.AssistantTool({
SessionMessage.AssistantTool.make({
type: "tool",
id: "running",
name: "read",
state: new SessionMessage.ToolStateRunning({
state: SessionMessage.ToolStateRunning.make({
status: "running",
input: { path: "README.md" },
content: [],
@@ -174,11 +174,11 @@ Recent work
}),
time: { created },
}),
new SessionMessage.AssistantTool({
SessionMessage.AssistantTool.make({
type: "tool",
id: "completed",
name: "read",
state: new SessionMessage.ToolStateCompleted({
state: SessionMessage.ToolStateCompleted.make({
status: "completed",
input: { path: "README.md" },
content: [
@@ -194,7 +194,7 @@ Recent work
}),
time: { created, completed: created },
}),
new SessionMessage.AssistantTool({
SessionMessage.AssistantTool.make({
type: "tool",
id: "hosted",
name: "web_search",
@@ -203,7 +203,7 @@ Recent work
metadata: { fake: { continuation: "hosted-call" } },
resultMetadata: { fake: { continuation: "hosted-result" } },
},
state: new SessionMessage.ToolStateCompleted({
state: SessionMessage.ToolStateCompleted.make({
status: "completed",
input: { query: "Effect" },
content: [{ type: "text", text: "Found it" }],
@@ -211,12 +211,12 @@ Recent work
}),
time: { created, completed: created },
}),
new SessionMessage.AssistantTool({
SessionMessage.AssistantTool.make({
type: "tool",
id: "hosted-failed",
name: "write",
provider: { executed: true, metadata: { fake: { continuation: "failed" } } },
state: new SessionMessage.ToolStateError({
state: SessionMessage.ToolStateError.make({
status: "error",
input: { path: "README.md" },
content: [],
@@ -299,13 +299,13 @@ Recent work
test("restores OpenAI encrypted reasoning metadata", () => {
const messages = toLLMMessages(
[
new SessionMessage.Assistant({
SessionMessage.Assistant.make({
id: id("assistant-openai-reasoning"),
type: "assistant",
agent: "build",
model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") },
content: [
new SessionMessage.AssistantReasoning({
SessionMessage.AssistantReasoning.make({
type: "reasoning",
id: "reasoning-openai",
text: "Think",
@@ -330,19 +330,19 @@ Recent work
test("drops provider-native continuation metadata after a model switch", () => {
const messages = toLLMMessages(
[
new SessionMessage.Assistant({
SessionMessage.Assistant.make({
id: id("assistant-old-model"),
type: "assistant",
agent: "build",
model: { id: ModelV2.ID.make("old-model"), providerID: ProviderV2.ID.make("provider") },
content: [
new SessionMessage.AssistantReasoning({
SessionMessage.AssistantReasoning.make({
type: "reasoning",
id: "reasoning-old-model",
text: "Visible thought",
providerMetadata: { anthropic: { signature: "sig_old" } },
}),
new SessionMessage.AssistantTool({
SessionMessage.AssistantTool.make({
type: "tool",
id: "hosted-old-model",
name: "web_search",
@@ -351,7 +351,7 @@ Recent work
metadata: { openai: { itemId: "hosted-old-model" } },
resultMetadata: { openai: { itemId: "hosted-old-model" } },
},
state: new SessionMessage.ToolStateCompleted({
state: SessionMessage.ToolStateCompleted.make({
status: "completed",
input: { query: "Effect" },
content: [],
@@ -360,7 +360,7 @@ Recent work
}),
time: { created, completed: created },
}),
new SessionMessage.AssistantTool({
SessionMessage.AssistantTool.make({
type: "tool",
id: "local-old-model",
name: "read",
@@ -369,7 +369,7 @@ Recent work
metadata: { fake: { call: "old" } },
resultMetadata: { fake: { result: "old" } },
},
state: new SessionMessage.ToolStateCompleted({
state: SessionMessage.ToolStateCompleted.make({
status: "completed",
input: { path: "README.md" },
content: [],
@@ -154,7 +154,7 @@ describe("SessionRunnerLLM recorded", () => {
const session = yield* SessionV2.Service
const prompt = yield* session.prompt({
sessionID,
prompt: new Prompt({ text: "Say hello in one short sentence." }),
prompt: Prompt.make({ text: "Say hello in one short sentence." }),
resume: false,
})
+110 -111
View File
@@ -26,7 +26,6 @@ import { SessionMessage } from "@opencode-ai/core/session/message"
import { Prompt } from "@opencode-ai/core/session/prompt"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { SessionExecution } from "@opencode-ai/core/session/execution"
import { SessionContextEpoch } from "@opencode-ai/core/session/context-epoch"
import { SessionRunCoordinator } from "@opencode-ai/core/session/run-coordinator"
import { SessionRunner } from "@opencode-ai/core/session/runner"
import * as SessionRunnerLLM from "@opencode-ai/core/session/runner/llm"
@@ -351,7 +350,7 @@ const setupOverflowRecovery = Effect.gen(function* () {
response = fragmentFixture("text", "text-earlier", ["Earlier answer"]).completeEvents
yield* session.prompt({
sessionID,
prompt: new Prompt({ text: "Earlier question ".repeat(700) }),
prompt: Prompt.make({ text: "Earlier question ".repeat(700) }),
resume: false,
})
yield* session.resume(sessionID)
@@ -476,7 +475,7 @@ const verifyEphemeralDeltas = (kind: FragmentKind) =>
const chunks = Array.from({ length: 32 }, (_, index) => `${index},`)
const fixture = fragmentFixture(kind, fragmentID(kind, "many"), chunks)
const expectedContext = [{ type: "user", text: prompt }, fixture.expectedAssistant]
yield* session.prompt({ sessionID, prompt: new Prompt({ text: prompt }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: prompt }), resume: false })
const events = yield* EventV2.Service
const live = yield* events.subscribe(fixture.delta).pipe(Stream.take(32), Stream.runCollect, Effect.forkScoped)
yield* Effect.yieldNow
@@ -507,7 +506,7 @@ const verifyPartialFlushOnFailure = (kind: FragmentKind) =>
const prompt = `Fail after ${kind}`
const fixture = fragmentFixture(kind, fragmentID(kind, "partial"), ["Partial"])
const failure = providerUnavailable()
yield* session.prompt({ sessionID, prompt: new Prompt({ text: prompt }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: prompt }), resume: false })
responseStream = Stream.concat(Stream.fromIterable(fixture.partialEvents), Stream.fail(failure))
expect(yield* session.resume(sessionID).pipe(Effect.flip)).toBe(failure)
@@ -529,7 +528,7 @@ const verifyPartialFlushOnInterruption = (kind: FragmentKind) =>
const prompt = `Interrupt after ${kind}`
const fixture = fragmentFixture(kind, fragmentID(kind, "interrupted"), ["Partial"])
const streamed = yield* Deferred.make<void>()
yield* session.prompt({ sessionID, prompt: new Prompt({ text: prompt }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: prompt }), resume: false })
responseStream = Stream.concat(
Stream.fromIterable(fixture.partialEvents),
Stream.fromEffect(Deferred.succeed(streamed, undefined)).pipe(Stream.flatMap(() => Stream.never)),
@@ -573,7 +572,7 @@ describe("SessionRunnerLLM", () => {
}),
}),
})
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Use application context" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Use application context" }), resume: false })
responses = [
[
LLMEvent.stepStart({ index: 0 }),
@@ -621,7 +620,7 @@ describe("SessionRunnerLLM", () => {
streamStarted = undefined
response = []
const message = yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Run automatically" }) })
const message = yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Run automatically" }) })
expect(requests).toHaveLength(1)
expect(yield* session.messages({ sessionID })).toMatchObject([
@@ -634,8 +633,8 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false })
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
requests.length = 0
responses = undefined
@@ -662,7 +661,7 @@ describe("SessionRunnerLLM", () => {
const { db } = yield* Database.Service
const messageID = SessionMessage.ID.create()
systemUnavailable = true
yield* session.prompt({ id: messageID, sessionID, prompt: new Prompt({ text: "First" }), resume: false })
yield* session.prompt({ id: messageID, sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
requests.length = 0
const exit = yield* session.resume(sessionID).pipe(Effect.exit)
@@ -680,7 +679,7 @@ describe("SessionRunnerLLM", () => {
).toBeUndefined()
systemUnavailable = false
yield* session.prompt({ id: messageID, sessionID, prompt: new Prompt({ text: "First" }) })
yield* session.prompt({ id: messageID, sessionID, prompt: Prompt.make({ text: "First" }) })
expect(requests).toHaveLength(1)
expect(requests[0]?.messages.map((message) => message.role)).toEqual(["user"])
@@ -693,7 +692,7 @@ describe("SessionRunnerLLM", () => {
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
const { db } = yield* Database.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
requests.length = 0
response = []
yield* session.resume(sessionID)
@@ -711,7 +710,7 @@ describe("SessionRunnerLLM", () => {
.get(),
).toBeUndefined()
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
const exit = yield* session.resume(sessionID).pipe(Effect.exit)
expect(Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)).toBe(true)
@@ -725,7 +724,7 @@ describe("SessionRunnerLLM", () => {
yield* setup
const session = yield* SessionV2.Service
const { db } = yield* Database.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
response = []
yield* session.resume(sessionID)
yield* db
@@ -734,7 +733,7 @@ describe("SessionRunnerLLM", () => {
.where(eq(SessionContextEpochTable.session_id, sessionID))
.run()
.pipe(Effect.orDie)
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
requests.length = 0
const exit = yield* session.resume(sessionID).pipe(Effect.exit)
@@ -749,13 +748,13 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
requests.length = 0
response = []
yield* session.resume(sessionID)
systemBaseline = "Changed context"
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
yield* session.resume(sessionID)
expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([
@@ -790,7 +789,7 @@ describe("SessionRunnerLLM", () => {
}),
)
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
requests.length = 0
response = fragmentFixture("text", "text-build", ["Done"]).completeEvents
@@ -816,7 +815,7 @@ describe("SessionRunnerLLM", () => {
editor.default(AgentV2.ID.make("reviewer"))
})
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
requests.length = 0
response = fragmentFixture("text", "text-reviewer", ["Done"]).completeEvents
@@ -845,7 +844,7 @@ describe("SessionRunnerLLM", () => {
.run()
.pipe(Effect.orDie)
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
requests.length = 0
response = fragmentFixture("text", "text-selected", ["Done"]).completeEvents
@@ -862,7 +861,7 @@ describe("SessionRunnerLLM", () => {
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
skillBaselines.set(AgentV2.ID.make("build"), "Build skills")
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
requests.length = 0
response = []
@@ -874,7 +873,7 @@ describe("SessionRunnerLLM", () => {
timestamp: DateTime.makeUnsafe(1),
agent: "reviewer",
})
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
yield* session.resume(sessionID)
expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([
@@ -905,7 +904,7 @@ describe("SessionRunnerLLM", () => {
})
.pipe(Effect.asVoid)
})
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
requests.length = 0
response = []
@@ -935,7 +934,7 @@ describe("SessionRunnerLLM", () => {
})
.pipe(Effect.asVoid)
})
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
requests.length = 0
response = []
@@ -949,13 +948,13 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
requests.length = 0
response = []
yield* session.resume(sessionID)
systemRemoved = true
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
yield* session.resume(sessionID)
expect(requests[1]?.messages.map((message) => message.role)).toEqual(["user", "user", "system"])
@@ -971,13 +970,13 @@ describe("SessionRunnerLLM", () => {
yield* setup
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
requests.length = 0
response = []
yield* session.resume(sessionID)
systemBaseline = "Changed context"
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
yield* session.resume(sessionID)
yield* events.publish(SessionEvent.ModelSwitched, {
sessionID,
@@ -986,7 +985,7 @@ describe("SessionRunnerLLM", () => {
model: { id: ModelV2.ID.make("replacement"), providerID: ProviderV2.ID.make("fake") },
})
systemBaseline = "Replacement context"
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Third" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Third" }), resume: false })
yield* session.resume(sessionID)
expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([
@@ -1006,7 +1005,7 @@ describe("SessionRunnerLLM", () => {
])
yield* replaySessionProjection(sessionID)
expect(yield* session.messages({ sessionID })).toHaveLength(6)
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Fourth" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Fourth" }), resume: false })
yield* session.resume(sessionID)
}),
)
@@ -1016,7 +1015,7 @@ describe("SessionRunnerLLM", () => {
yield* setup
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
requests.length = 0
response = []
@@ -1028,11 +1027,11 @@ describe("SessionRunnerLLM", () => {
model: { id: ModelV2.ID.make("replacement"), providerID: ProviderV2.ID.make("fake") },
})
systemUnavailable = true
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
yield* session.resume(sessionID)
systemUnavailable = false
systemBaseline = "Replacement context"
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Third" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Third" }), resume: false })
yield* session.resume(sessionID)
expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([
@@ -1048,7 +1047,7 @@ describe("SessionRunnerLLM", () => {
yield* setup
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
requests.length = 0
response = []
@@ -1069,7 +1068,7 @@ describe("SessionRunnerLLM", () => {
recent: "",
})
systemBaseline = "Replacement context"
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
yield* session.resume(sessionID)
expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([
@@ -1077,7 +1076,7 @@ describe("SessionRunnerLLM", () => {
["Replacement context"],
])
yield* replaySessionProjection(sessionID)
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Third" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Third" }), resume: false })
yield* session.resume(sessionID)
}),
)
@@ -1089,7 +1088,7 @@ describe("SessionRunnerLLM", () => {
response = fragmentFixture("text", "text-first", ["Earlier answer"]).completeEvents
yield* session.prompt({
sessionID,
prompt: new Prompt({ text: "Earlier question ".repeat(180) }),
prompt: Prompt.make({ text: "Earlier question ".repeat(180) }),
resume: false,
})
yield* session.resume(sessionID)
@@ -1102,7 +1101,7 @@ describe("SessionRunnerLLM", () => {
]
yield* session.prompt({
sessionID,
prompt: new Prompt({ text: "Recent exact request ".repeat(180) }),
prompt: Prompt.make({ text: "Recent exact request ".repeat(180) }),
resume: false,
})
yield* session.resume(sessionID)
@@ -1128,7 +1127,7 @@ describe("SessionRunnerLLM", () => {
]
yield* session.prompt({
sessionID,
prompt: new Prompt({ text: "Newest exact request ".repeat(180) }),
prompt: Prompt.make({ text: "Newest exact request ".repeat(180) }),
resume: false,
})
yield* session.resume(sessionID)
@@ -1156,7 +1155,7 @@ describe("SessionRunnerLLM", () => {
fragmentFixture("text", "text-summary", ["## Goal\n- Recover overflow"]).completeEvents,
fragmentFixture("text", "text-final", ["Recovered"]).completeEvents,
]
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Continue" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Continue" }), resume: false })
yield* session.resume(sessionID)
expect(requests).toHaveLength(3)
@@ -1186,7 +1185,7 @@ describe("SessionRunnerLLM", () => {
fragmentFixture("text", "text-summary", ["## Goal\n- Recover once"]).completeEvents,
overflow(),
]
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Continue" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Continue" }), resume: false })
yield* session.resume(sessionID)
expect(requests).toHaveLength(3)
@@ -1214,7 +1213,7 @@ describe("SessionRunnerLLM", () => {
fragmentFixture("text", "text-summary", ["## Goal\n- Recover raw overflow"]).completeEvents,
fragmentFixture("text", "text-final", ["Recovered"]).completeEvents,
]
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Continue" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Continue" }), resume: false })
yield* session.resume(sessionID)
expect(requests).toHaveLength(3)
@@ -1232,7 +1231,7 @@ describe("SessionRunnerLLM", () => {
[LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" })],
[LLMEvent.providerError({ message: "summary unavailable" })],
]
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Continue" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Continue" }), resume: false })
yield* session.resume(sessionID)
expect(requests).toHaveLength(2)
@@ -1255,7 +1254,7 @@ describe("SessionRunnerLLM", () => {
const firstGate = yield* Deferred.make<void>()
const summaryGate = yield* Deferred.make<void>()
streamGate = firstGate
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Continue" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Continue" }), resume: false })
const run = yield* session.resume(sessionID).pipe(Effect.forkChild)
while (requests.length < 1) yield* Effect.yieldNow
streamGate = summaryGate
@@ -1275,13 +1274,13 @@ describe("SessionRunnerLLM", () => {
yield* setup
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
requests.length = 0
response = []
yield* session.resume(sessionID)
systemBaseline = "Changed context"
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
yield* session.resume(sessionID)
const compactionID = SessionMessage.ID.create()
yield* events.publish(SessionEvent.Compaction.Started, {
@@ -1299,7 +1298,7 @@ describe("SessionRunnerLLM", () => {
recent: "",
})
systemUnavailable = true
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Third" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Third" }), resume: false })
yield* session.resume(sessionID)
expect(requests.at(-1)?.system.map((part) => part.text)).toEqual(["Initial context"])
@@ -1311,7 +1310,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Use tools" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Use tools" }), resume: false })
requests.length = 0
responses = undefined
@@ -1409,7 +1408,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Echo this" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Echo this" }), resume: false })
requests.length = 0
authorizations.length = 0
@@ -1468,7 +1467,7 @@ describe("SessionRunnerLLM", () => {
yield* setup
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Echo this" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Echo this" }), resume: false })
requests.length = 0
responses = [
@@ -1512,7 +1511,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Think first" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Think first" }), resume: false })
requests.length = 0
response = [
@@ -1550,7 +1549,7 @@ describe("SessionRunnerLLM", () => {
},
])
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Continue" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Continue" }), resume: false })
response = []
yield* session.resume(sessionID)
@@ -1569,7 +1568,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Search first" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Search first" }), resume: false })
requests.length = 0
response = [
@@ -1594,7 +1593,7 @@ describe("SessionRunnerLLM", () => {
yield* session.resume(sessionID)
yield* replaySessionProjection(sessionID)
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Continue" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Continue" }), resume: false })
response = []
yield* session.resume(sessionID)
@@ -1624,7 +1623,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Echo five times" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Echo five times" }), resume: false })
requests.length = 0
executions.length = 0
@@ -1685,7 +1684,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Echo twice" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Echo twice" }), resume: false })
requests.length = 0
executions.length = 0
@@ -1773,7 +1772,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Run once" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Run once" }), resume: false })
requests.length = 0
responses = undefined
@@ -1812,7 +1811,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Start working" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Start working" }), resume: false })
requests.length = 0
responses = [
@@ -1832,7 +1831,7 @@ describe("SessionRunnerLLM", () => {
const first = yield* session.resume(sessionID).pipe(Effect.forkChild)
yield* Deferred.await(streamStarted)
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Change direction" }) })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Change direction" }) })
yield* Deferred.succeed(streamGate, undefined)
yield* Fiber.join(first)
streamGate = undefined
@@ -1855,7 +1854,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Start working" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Start working" }), resume: false })
requests.length = 0
responses = [
@@ -1883,7 +1882,7 @@ describe("SessionRunnerLLM", () => {
yield* Deferred.await(streamStarted)
yield* session.prompt({
sessionID,
prompt: new Prompt({ text: "Wait until continuation ends" }),
prompt: Prompt.make({ text: "Wait until continuation ends" }),
delivery: "queue",
})
yield* Deferred.succeed(streamGate, undefined)
@@ -1903,7 +1902,7 @@ describe("SessionRunnerLLM", () => {
yield* setup
const session = yield* SessionV2.Service
const { db } = yield* Database.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Interrupt current work" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Interrupt current work" }), resume: false })
requests.length = 0
responses = [
@@ -1921,7 +1920,7 @@ describe("SessionRunnerLLM", () => {
yield* Deferred.await(streamStarted)
yield* session.prompt({
sessionID,
prompt: new Prompt({ text: "Run after interrupt" }),
prompt: Prompt.make({ text: "Run after interrupt" }),
delivery: "queue",
})
yield* session.interrupt(sessionID)
@@ -1946,7 +1945,7 @@ describe("SessionRunnerLLM", () => {
yield* setup
const session = yield* SessionV2.Service
const { db } = yield* Database.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Interrupt current work" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Interrupt current work" }), resume: false })
requests.length = 0
responses = [
@@ -1964,7 +1963,7 @@ describe("SessionRunnerLLM", () => {
yield* Deferred.await(streamStarted)
yield* session.prompt({
sessionID,
prompt: new Prompt({ text: "Steer after interrupt" }),
prompt: Prompt.make({ text: "Steer after interrupt" }),
})
yield* session.interrupt(sessionID)
expect(yield* Fiber.await(run)).toMatchObject({ _tag: "Failure" })
@@ -1988,7 +1987,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Start working" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Start working" }), resume: false })
requests.length = 0
responses = [
@@ -2013,8 +2012,8 @@ describe("SessionRunnerLLM", () => {
const first = yield* session.resume(sessionID).pipe(Effect.forkChild)
yield* Deferred.await(streamStarted)
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Queue first" }), delivery: "queue" })
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Queue second" }), delivery: "queue" })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Queue first" }), delivery: "queue" })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Queue second" }), delivery: "queue" })
yield* Deferred.succeed(streamGate, undefined)
yield* Fiber.join(first)
streamGate = undefined
@@ -2031,10 +2030,10 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Start steering" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Start steering" }), resume: false })
yield* session.prompt({
sessionID,
prompt: new Prompt({ text: "Queue for later" }),
prompt: Prompt.make({ text: "Queue for later" }),
delivery: "queue",
resume: false,
})
@@ -2065,7 +2064,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Start working" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Start working" }), resume: false })
requests.length = 0
responses = [
@@ -2096,13 +2095,13 @@ describe("SessionRunnerLLM", () => {
const first = yield* session.resume(sessionID).pipe(Effect.forkChild)
while (requests.length < 1) yield* Effect.yieldNow
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Queue first" }), delivery: "queue" })
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Queue second" }), delivery: "queue" })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Queue first" }), delivery: "queue" })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Queue second" }), delivery: "queue" })
streamGate = secondGate
yield* Deferred.succeed(firstGate, undefined)
while (requests.length < 2) yield* Effect.yieldNow
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Steer before next queued input" }) })
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Also steer before next queued input" }) })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Steer before next queued input" }) })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Also steer before next queued input" }) })
yield* Deferred.succeed(secondGate, undefined)
yield* Fiber.join(first)
streamGate = undefined
@@ -2130,7 +2129,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Start working" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Start working" }), resume: false })
requests.length = 0
responses = [
@@ -2150,8 +2149,8 @@ describe("SessionRunnerLLM", () => {
const first = yield* session.resume(sessionID).pipe(Effect.forkChild)
yield* Deferred.await(streamStarted)
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First steer" }) })
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second steer" }) })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First steer" }) })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second steer" }) })
yield* Deferred.succeed(streamGate, undefined)
yield* Fiber.join(first)
streamGate = undefined
@@ -2170,7 +2169,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Start working" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Start working" }), resume: false })
requests.length = 0
responses = undefined
@@ -2181,7 +2180,7 @@ describe("SessionRunnerLLM", () => {
const first = yield* session.resume(sessionID).pipe(Effect.forkChild)
yield* Deferred.await(streamStarted)
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Recover with this" }) })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Recover with this" }) })
yield* Deferred.succeed(streamGate, undefined)
expect(yield* Fiber.join(first).pipe(Effect.flip)).toBe(streamFailure)
@@ -2200,7 +2199,7 @@ describe("SessionRunnerLLM", () => {
yield* setup
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Recover interrupted tool" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Recover interrupted tool" }), resume: false })
yield* SessionInput.promoteSteers((yield* Database.Service).db, events, sessionID, Number.MAX_SAFE_INTEGER)
const assistantMessageID = SessionMessage.ID.create()
yield* events.publish(SessionEvent.Step.Started, {
@@ -2262,7 +2261,7 @@ describe("SessionRunnerLLM", () => {
const events = yield* EventV2.Service
yield* session.prompt({
sessionID,
prompt: new Prompt({ text: "Recover interrupted hosted tool" }),
prompt: Prompt.make({ text: "Recover interrupted hosted tool" }),
resume: false,
})
yield* SessionInput.promoteSteers((yield* Database.Service).db, events, sessionID, Number.MAX_SAFE_INTEGER)
@@ -2322,7 +2321,7 @@ describe("SessionRunnerLLM", () => {
const events = yield* EventV2.Service
yield* session.prompt({
sessionID,
prompt: new Prompt({ text: "Recover interrupted tool input" }),
prompt: Prompt.make({ text: "Recover interrupted tool input" }),
resume: false,
})
yield* SessionInput.promoteSteers((yield* Database.Service).db, events, sessionID, Number.MAX_SAFE_INTEGER)
@@ -2360,7 +2359,7 @@ describe("SessionRunnerLLM", () => {
const session = yield* SessionV2.Service
yield* session.prompt({
sessionID,
prompt: new Prompt({ text: "Wait in queue" }),
prompt: Prompt.make({ text: "Wait in queue" }),
delivery: "queue",
resume: false,
})
@@ -2382,7 +2381,7 @@ describe("SessionRunnerLLM", () => {
const defect = new Error("fail after prompt promotion")
let fail = true
yield* events.project(SessionEvent.Prompted, () => (fail ? Effect.die(defect) : Effect.void))
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Recover promoted input" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Recover promoted input" }), resume: false })
expect(yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))).toBe(defect)
fail = false
@@ -2410,7 +2409,7 @@ describe("SessionRunnerLLM", () => {
)
yield* session.prompt({
sessionID,
prompt: new Prompt({ text: "Run committed promotion" }),
prompt: Prompt.make({ text: "Run committed promotion" }),
resume: false,
})
@@ -2427,8 +2426,8 @@ describe("SessionRunnerLLM", () => {
yield* setup
yield* insertSession(otherSessionID)
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Run first" }), resume: false })
yield* session.prompt({ sessionID: otherSessionID, prompt: new Prompt({ text: "Run second" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Run first" }), resume: false })
yield* session.prompt({ sessionID: otherSessionID, prompt: Prompt.make({ text: "Run second" }), resume: false })
requests.length = 0
responses = undefined
@@ -2470,12 +2469,12 @@ describe("SessionRunnerLLM", () => {
const session = yield* SessionV2.Service
yield* session.prompt({
sessionID: externalSessionID,
prompt: new Prompt({ text: "Run external session" }),
prompt: Prompt.make({ text: "Run external session" }),
resume: false,
})
yield* session.prompt({
sessionID: otherExternalSessionID,
prompt: new Prompt({ text: "Run other external session" }),
prompt: Prompt.make({ text: "Run other external session" }),
resume: false,
})
@@ -2494,7 +2493,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Retry after failure" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Retry after failure" }), resume: false })
requests.length = 0
responses = undefined
@@ -2525,7 +2524,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Call missing" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Call missing" }), resume: false })
requests.length = 0
responses = [
@@ -2571,7 +2570,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Call defect" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Call defect" }), resume: false })
requests.length = 0
responses = [
@@ -2620,7 +2619,7 @@ describe("SessionRunnerLLM", () => {
questions.ask({ sessionID: context.sessionID, questions: [] }).pipe(Effect.as({}), Effect.orDie),
}),
})
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Ask then stop" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Ask then stop" }), resume: false })
requests.length = 0
responses = [
@@ -2665,7 +2664,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Settle before failing" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Settle before failing" }), resume: false })
const failure = providerUnavailable()
toolExecutionGate = yield* Deferred.make<void>()
responseStream = Stream.concat(
@@ -2699,7 +2698,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Interrupt blocked tool" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Interrupt blocked tool" }), resume: false })
executions.length = 0
toolExecutionGate = yield* Deferred.make<void>()
responseStream = Stream.concat(
@@ -2749,7 +2748,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Interrupt provider" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Interrupt provider" }), resume: false })
requests.length = 0
response = []
streamGate = yield* Deferred.make<void>()
@@ -2772,7 +2771,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Interrupt tool settlement" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Interrupt tool settlement" }), resume: false })
executions.length = 0
toolExecutionGate = yield* Deferred.make<void>()
response = [
@@ -2815,7 +2814,7 @@ describe("SessionRunnerLLM", () => {
}),
)
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Finish at the limit" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Finish at the limit" }), resume: false })
requests.length = 0
executions.length = 0
@@ -2863,7 +2862,7 @@ describe("SessionRunnerLLM", () => {
}),
)
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Start work" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Start work" }), resume: false })
requests.length = 0
executions.length = 0
@@ -2891,7 +2890,7 @@ describe("SessionRunnerLLM", () => {
const run = yield* session.resume(sessionID).pipe(Effect.forkChild)
yield* Deferred.await(streamStarted)
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Change direction" }) })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Change direction" }) })
yield* Deferred.succeed(streamGate, undefined)
yield* Fiber.join(run)
streamGate = undefined
@@ -2909,7 +2908,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Fail durably" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Fail durably" }), resume: false })
requests.length = 0
responses = undefined
@@ -2931,7 +2930,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Fail before step" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Fail before step" }), resume: false })
requests.length = 0
response = [LLMEvent.providerError({ message: "Provider unavailable" })]
@@ -2950,7 +2949,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Fail after output" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Fail after output" }), resume: false })
requests.length = 0
response = [
@@ -2979,7 +2978,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Fail raw stream durably" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Fail raw stream durably" }), resume: false })
const failure = providerUnavailable()
responseStream = Stream.fail(failure)
@@ -2998,7 +2997,7 @@ describe("SessionRunnerLLM", () => {
const session = yield* SessionV2.Service
yield* session.prompt({
sessionID,
prompt: new Prompt({ text: "Do not continue failed provider" }),
prompt: Prompt.make({ text: "Do not continue failed provider" }),
resume: false,
})
@@ -3021,7 +3020,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Fail hosted tool durably" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Fail hosted tool durably" }), resume: false })
requests.length = 0
response = [
@@ -3052,7 +3051,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Fail hosted tool at EOF" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Fail hosted tool at EOF" }), resume: false })
response = [
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({
@@ -3079,7 +3078,7 @@ describe("SessionRunnerLLM", () => {
const session = yield* SessionV2.Service
yield* session.prompt({
sessionID,
prompt: new Prompt({ text: "Fail hosted tool on raw failure" }),
prompt: Prompt.make({ text: "Fail hosted tool on raw failure" }),
resume: false,
})
const failure = providerUnavailable()
@@ -3114,7 +3113,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Two blocks" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Two blocks" }), resume: false })
responses = undefined
streamGate = undefined
@@ -3177,7 +3176,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Call provider tool" }), resume: false })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Call provider tool" }), resume: false })
responses = undefined
streamGate = undefined
+3 -3
View File
@@ -363,7 +363,7 @@ function renderImportedEffectFiles(
? `import { ${api} } from ${JSON.stringify(options.module)}`
: `import { HttpApi, HttpApiClient${"endpoints" in options ? ", HttpApiGroup" : ""} } from "effect/unstable/httpapi"\nimport { ${projection.imports.join(", ")} } from ${JSON.stringify(options.module)}`
const httpApiImport = projection === undefined ? 'import { HttpApiClient } from "effect/unstable/httpapi"\n' : ""
const client = `// Generated by @opencode-ai/httpapi-codegen. Do not edit.\nimport { Effect${usesStream ? ", Stream" : ""}, Schema } from "effect"\nimport { Sse } from "effect/unstable/encoding"\nimport { HttpClientError } from "effect/unstable/http"\n${httpApiImport}${imports}\nimport { ClientError } from "./client-error"\n\n${projection?.source ?? ""}type RawClient = HttpApiClient.ForApi<typeof ${api}>\n\nconst mapClientError = <E>(error: E) => HttpClientError.isHttpClientError(error) || Schema.isSchemaError(error) || Sse.Retry.is(error) ? new ClientError({ cause: error }) : error\n\n${adapters.join("\n\n")}\n\nexport const make = (options?: { readonly baseUrl?: URL | string }) => HttpApiClient.make(${api}, options).pipe(Effect.map((raw) => ({ ${fields.join(", ")} })))\n`
const client = `// Generated by @opencode-ai/httpapi-codegen. Do not edit.\nimport { Effect${usesStream ? ", Stream" : ""}, Schema } from "effect"\nimport { Sse } from "effect/unstable/encoding"\nimport { HttpClientError } from "effect/unstable/http"\n${httpApiImport}${imports}\nimport { ClientError } from "./client-error"\n\n${projection?.source ?? ""}type RawClient = HttpApiClient.ForApi<typeof ${api}>\n\nconst mapClientError = <E>(error: E) => HttpClientError.isHttpClientError(error) || Schema.isSchemaError(error) || Sse.Retry.is(error) ? new ClientError({ cause: error }) : error\n\n${adapters.join("\n\n")}\n\nconst adaptClient = (raw: RawClient) => ({ ${fields.join(", ")} })\n\nexport const make = (options?: { readonly baseUrl?: URL | string }) => HttpApiClient.make(${api}, options).pipe(Effect.map(adaptClient))\n`
return [
{
path: "client-error.ts",
@@ -736,7 +736,7 @@ export function write(
output.files,
(file) =>
Effect.tryPromise({
try: () => format(file.content, { parser: "typescript", semi: false, printWidth: 120 }),
try: () => format(file.content, { filepath: file.path, parser: "typescript", semi: false, printWidth: 120 }),
catch: (error) => new GenerationError({ reason: `Failed to format ${file.path}: ${String(error)}` }),
}).pipe(Effect.flatMap((content) => fs.writeFileString(join(directory, file.path), content))),
{ concurrency: 8, discard: true },
@@ -1143,5 +1143,5 @@ function renderClient(groups: ReadonlyArray<Group>) {
const raw = `{ ${group.endpoints.map((item) => `${JSON.stringify(item.endpoint.name)}: raw[${JSON.stringify(item.endpoint.name)}]`).join(", ")} }`
return [`...adaptGroup${index}(${raw})`]
})
return `// Generated by @opencode-ai/httpapi-codegen. Do not edit.\nimport { Effect } from "effect"\nimport { HttpApi, HttpApiClient } from "effect/unstable/httpapi"\n${imports}\n\nconst Api = ${api}\n\nexport const make = (options?: { readonly baseUrl?: URL | string }) =>\n HttpApiClient.make(Api, options).pipe(Effect.map((raw) => ({ ${fields.join(", ")} })))\n`
return `// Generated by @opencode-ai/httpapi-codegen. Do not edit.\nimport { Effect } from "effect"\nimport { HttpApi, HttpApiClient } from "effect/unstable/httpapi"\n${imports}\n\nconst Api = ${api}\nconst adaptClient = (raw: HttpApiClient.ForApi<typeof Api>) => ({ ${fields.join(", ")} })\n\nexport const make = (options?: { readonly baseUrl?: URL | string }) =>\n HttpApiClient.make(Api, options).pipe(Effect.map(adaptClient))\n`
}
@@ -6,12 +6,11 @@ import { adaptGroup1, Group1 } from "./event"
import { adaptGroup2, Group2 } from "./system"
const Api = HttpApi.make("generated").add(Group0).add(Group1).add(Group2)
const adaptClient = (raw: HttpApiClient.ForApi<typeof Api>) => ({
session: adaptGroup0(raw["session"]),
event: adaptGroup1(raw["event"]),
...adaptGroup2({ status: raw["status"] }),
})
export const make = (options?: { readonly baseUrl?: URL | string }) =>
HttpApiClient.make(Api, options).pipe(
Effect.map((raw) => ({
session: adaptGroup0(raw["session"]),
event: adaptGroup1(raw["event"]),
...adaptGroup2({ status: raw["status"] }),
})),
)
HttpApiClient.make(Api, options).pipe(Effect.map(adaptClient))
-46
View File
@@ -1,46 +0,0 @@
import { Schema } from "effect"
export class Source extends Schema.Class<Source>("Prompt.Source")({
start: Schema.Finite,
end: Schema.Finite,
text: Schema.String,
}) {}
export class FileAttachment extends Schema.Class<FileAttachment>("Prompt.FileAttachment")({
uri: Schema.String,
mime: Schema.String,
name: Schema.String.pipe(Schema.optional),
description: Schema.String.pipe(Schema.optional),
source: Source.pipe(Schema.optional),
}) {
static create(input: FileAttachment) {
return new FileAttachment({
uri: input.uri,
mime: input.mime,
name: input.name,
description: input.description,
source: input.source,
})
}
}
export class AgentAttachment extends Schema.Class<AgentAttachment>("Prompt.AgentAttachment")({
name: Schema.String,
source: Source.pipe(Schema.optional),
}) {}
export class Prompt extends Schema.Class<Prompt>("Prompt")({
text: Schema.String,
files: Schema.Array(FileAttachment).pipe(Schema.optional),
agents: Schema.Array(AgentAttachment).pipe(Schema.optional),
}) {
static readonly equivalence = Schema.toEquivalence(Prompt)
static fromUserMessage(input: Pick<Prompt, "text" | "files" | "agents">) {
return new Prompt({
text: input.text,
...(input.files === undefined ? {} : { files: input.files }),
...(input.agents === undefined ? {} : { agents: input.agents }),
})
}
}
@@ -54,6 +54,7 @@ import { Database } from "@opencode-ai/core/database/database"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
import { EventV2 } from "@opencode-ai/core/event"
import { ApplicationTools } from "@opencode-ai/core/tool/application-tools"
import { ModelsDev } from "@opencode-ai/core/models-dev"
import { Npm } from "@opencode-ai/core/npm"
import { ProjectV2 } from "@opencode-ai/core/project"
@@ -280,6 +281,7 @@ export function createRoutes(
HttpServer.layerServices,
]),
Layer.provide(LayerNode.buildLayer(app)),
Layer.provide(ApplicationTools.layer),
Layer.provide(Layer.succeed(CorsConfig)(corsOptions)),
Layer.provideMerge(Observability.layer),
)
+5 -5
View File
@@ -1086,12 +1086,12 @@ export const layer = Layer.effect(
}
if (part.type === "file") {
result.files.push(
new FileAttachment({
FileAttachment.make({
uri: part.url,
mime: part.mime,
name: part.filename,
source: part.source
? new Source({
? Source.make({
start: part.source.text.start,
end: part.source.text.end,
text: part.source.text.value,
@@ -1102,10 +1102,10 @@ export const layer = Layer.effect(
}
if (part.type === "agent") {
result.agents.push(
new AgentAttachment({
AgentAttachment.make({
name: part.name,
source: part.source
? new Source({
? Source.make({
start: part.source.start,
end: part.source.end,
text: part.source.value,
@@ -1130,7 +1130,7 @@ export const layer = Layer.effect(
messageID: SessionMessage.ID.create(),
timestamp: DateTime.makeUnsafe(info.time.created),
delivery: "steer",
prompt: new Prompt({
prompt: Prompt.make({
text: nextPrompt.text.join("\n"),
files: nextPrompt.files,
agents: nextPrompt.agents,
@@ -23,7 +23,6 @@ import * as HttpSessionError from "../../src/server/routes/instance/httpapi/hand
import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session"
import { Session } from "@/session/session"
import { MessageID, PartID, SessionID, type SessionID as SessionIDType } from "../../src/session/schema"
import { MessageV2 } from "../../src/session/message-v2"
import { Database } from "@opencode-ai/core/database/database"
import { SessionInputTable, SessionMessageTable, SessionTable } from "@opencode-ai/core/session/sql"
import { SessionMessage } from "@opencode-ai/core/session/message"
@@ -129,7 +128,7 @@ const createLocalWorkspace = (input: { projectID: Project.Info["id"]; type: stri
const insertLegacyAssistantMessage = (sessionID: SessionIDType, seq = 1, time = seq) =>
Effect.gen(function* () {
const message = new SessionMessage.Assistant({
const message = SessionMessage.Assistant.make({
id: SessionMessage.ID.create(),
type: "assistant",
agent: "build",
+1 -1
View File
@@ -14,7 +14,7 @@
"typecheck": "tsgo --noEmit"
},
"dependencies": {
"@opencode-ai/model": "workspace:*",
"@opencode-ai/schema": "workspace:*",
"effect": "catalog:"
},
"devDependencies": {
+4 -4
View File
@@ -1,7 +1,7 @@
import { AbsolutePath, PositiveInt, RelativePath } from "@opencode-ai/model/schema"
import { Project } from "@opencode-ai/model/project"
import { Session } from "@opencode-ai/model/session"
import { Workspace } from "@opencode-ai/model/workspace"
import { AbsolutePath, PositiveInt, RelativePath } from "@opencode-ai/schema/schema"
import { Project } from "@opencode-ai/schema/project"
import { Session } from "@opencode-ai/schema/session"
import { Workspace } from "@opencode-ai/schema/workspace"
import { Effect, Encoding, Schema, Struct } from "effect"
const fields = {
+10 -10
View File
@@ -1,13 +1,13 @@
import { Agent } from "@opencode-ai/model/agent"
import { Location } from "@opencode-ai/model/location"
import { Model } from "@opencode-ai/model/model"
import { Project } from "@opencode-ai/model/project"
import { AbsolutePath, PositiveInt, RelativePath } from "@opencode-ai/model/schema"
import { Session } from "@opencode-ai/model/session"
import { SessionInput } from "@opencode-ai/model/session-input"
import { SessionMessage } from "@opencode-ai/model/session-message"
import { Prompt } from "@opencode-ai/model/prompt"
import { Workspace } from "@opencode-ai/model/workspace"
import { Agent } from "@opencode-ai/schema/agent"
import { Location } from "@opencode-ai/schema/location"
import { Model } from "@opencode-ai/schema/model"
import { Project } from "@opencode-ai/schema/project"
import { AbsolutePath, PositiveInt, RelativePath } from "@opencode-ai/schema/schema"
import { Session } from "@opencode-ai/schema/session"
import { SessionInput } from "@opencode-ai/schema/session-input"
import { SessionMessage } from "@opencode-ai/schema/session-message"
import { Prompt } from "@opencode-ai/schema/prompt"
import { Workspace } from "@opencode-ai/schema/workspace"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import {
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/model",
"name": "@opencode-ai/schema",
"private": true,
"type": "module",
"license": "MIT",
@@ -4,7 +4,8 @@ import { Effect, Schema } from "effect"
import { AbsolutePath } from "./schema"
import { Workspace } from "./workspace"
export class Ref extends Schema.Class<Ref>("Location.Ref")({
export interface Ref extends Schema.Schema.Type<typeof Ref> {}
export const Ref = Schema.Struct({
directory: AbsolutePath,
workspaceID: Schema.optional(Workspace.ID).pipe(Schema.withConstructorDefault(Effect.succeed(undefined))),
}) {}
}).annotate({ identifier: "Location.Ref" })
+30
View File
@@ -0,0 +1,30 @@
import { Schema } from "effect"
export interface Source extends Schema.Schema.Type<typeof Source> {}
export const Source = Schema.Struct({
start: Schema.Finite,
end: Schema.Finite,
text: Schema.String,
}).annotate({ identifier: "Prompt.Source" })
export interface FileAttachment extends Schema.Schema.Type<typeof FileAttachment> {}
export const FileAttachment = Schema.Struct({
uri: Schema.String,
mime: Schema.String,
name: Schema.String.pipe(Schema.optional),
description: Schema.String.pipe(Schema.optional),
source: Source.pipe(Schema.optional),
}).annotate({ identifier: "Prompt.FileAttachment" })
export interface AgentAttachment extends Schema.Schema.Type<typeof AgentAttachment> {}
export const AgentAttachment = Schema.Struct({
name: Schema.String,
source: Source.pipe(Schema.optional),
}).annotate({ identifier: "Prompt.AgentAttachment" })
export interface Prompt extends Schema.Schema.Type<typeof Prompt> {}
export const Prompt = Schema.Struct({
text: Schema.String,
files: Schema.Array(FileAttachment).pipe(Schema.optional),
agents: Schema.Array(AgentAttachment).pipe(Schema.optional),
}).annotate({ identifier: "Prompt" })
@@ -1,4 +1,6 @@
import { DateTime, Option, Schema, SchemaGetter } from "effect"
import { sha256 } from "@noble/hashes/sha2.js"
import { bytesToHex } from "@noble/hashes/utils.js"
export const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0))
export const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
@@ -22,6 +24,14 @@ export const withStatics =
(schema: S): S & M =>
Object.assign(schema, methods(schema))
export interface ExternalID {
readonly namespace: string
readonly key: string
}
export const externalID = (prefix: string, input: ExternalID) =>
`${prefix}_${bytesToHex(sha256(new TextEncoder().encode(JSON.stringify([input.namespace, input.key]))))}`
export const DateTimeUtcFromMillis = Schema.Finite.pipe(
Schema.decodeTo(Schema.DateTimeUtc, {
decode: SchemaGetter.transform((value) => DateTime.makeUnsafe(value)),
@@ -9,7 +9,8 @@ import { SessionMessage } from "./session-message"
export const Delivery = Schema.Literals(["steer", "queue"])
export type Delivery = typeof Delivery.Type
export class Admitted extends Schema.Class<Admitted>("SessionInput.Admitted")({
export interface Admitted extends Schema.Schema.Type<typeof Admitted> {}
export const Admitted = Schema.Struct({
admittedSeq: NonNegativeInt,
id: SessionMessage.ID,
sessionID: Session.ID,
@@ -17,4 +18,4 @@ export class Admitted extends Schema.Class<Admitted>("SessionInput.Admitted")({
delivery: Delivery,
timeCreated: DateTimeUtcFromMillis,
promotedSeq: NonNegativeInt.pipe(Schema.optional),
}) {}
}).annotate({ identifier: "SessionInput.Admitted" })
@@ -36,41 +36,47 @@ const Base = {
time: Schema.Struct({ created: DateTimeUtcFromMillis }),
}
export class AgentSwitched extends Schema.Class<AgentSwitched>("Session.Message.AgentSwitched")({
export interface AgentSwitched extends Schema.Schema.Type<typeof AgentSwitched> {}
export const AgentSwitched = Schema.Struct({
...Base,
type: Schema.Literal("agent-switched"),
agent: Schema.String,
}) {}
}).annotate({ identifier: "Session.Message.AgentSwitched" })
export class ModelSwitched extends Schema.Class<ModelSwitched>("Session.Message.ModelSwitched")({
export interface ModelSwitched extends Schema.Schema.Type<typeof ModelSwitched> {}
export const ModelSwitched = Schema.Struct({
...Base,
type: Schema.Literal("model-switched"),
model: Model.Ref,
}) {}
}).annotate({ identifier: "Session.Message.ModelSwitched" })
export class User extends Schema.Class<User>("Session.Message.User")({
export interface User extends Schema.Schema.Type<typeof User> {}
export const User = Schema.Struct({
...Base,
text: Prompt.fields.text,
files: Prompt.fields.files,
agents: Prompt.fields.agents,
type: Schema.Literal("user"),
time: Schema.Struct({ created: DateTimeUtcFromMillis }),
}) {}
}).annotate({ identifier: "Session.Message.User" })
export class Synthetic extends Schema.Class<Synthetic>("Session.Message.Synthetic")({
export interface Synthetic extends Schema.Schema.Type<typeof Synthetic> {}
export const Synthetic = Schema.Struct({
...Base,
sessionID: Session.ID,
text: Schema.String,
type: Schema.Literal("synthetic"),
}) {}
}).annotate({ identifier: "Session.Message.Synthetic" })
export class System extends Schema.Class<System>("Session.Message.System")({
export interface System extends Schema.Schema.Type<typeof System> {}
export const System = Schema.Struct({
...Base,
type: Schema.Literal("system"),
text: Schema.String,
}) {}
}).annotate({ identifier: "Session.Message.System" })
export class Shell extends Schema.Class<Shell>("Session.Message.Shell")({
export interface Shell extends Schema.Schema.Type<typeof Shell> {}
export const Shell = Schema.Struct({
...Base,
type: Schema.Literal("shell"),
callID: Schema.String,
@@ -80,21 +86,24 @@ export class Shell extends Schema.Class<Shell>("Session.Message.Shell")({
created: DateTimeUtcFromMillis,
completed: DateTimeUtcFromMillis.pipe(Schema.optional),
}),
}) {}
}).annotate({ identifier: "Session.Message.Shell" })
export class ToolStatePending extends Schema.Class<ToolStatePending>("Session.Message.ToolState.Pending")({
export interface ToolStatePending extends Schema.Schema.Type<typeof ToolStatePending> {}
export const ToolStatePending = Schema.Struct({
status: Schema.Literal("pending"),
input: Schema.String,
}) {}
}).annotate({ identifier: "Session.Message.ToolState.Pending" })
export class ToolStateRunning extends Schema.Class<ToolStateRunning>("Session.Message.ToolState.Running")({
export interface ToolStateRunning extends Schema.Schema.Type<typeof ToolStateRunning> {}
export const ToolStateRunning = Schema.Struct({
status: Schema.Literal("running"),
input: Schema.Record(Schema.String, Schema.Unknown),
structured: Schema.Record(Schema.String, Schema.Any),
content: ToolContent.pipe(Schema.Array),
}) {}
}).annotate({ identifier: "Session.Message.ToolState.Running" })
export class ToolStateCompleted extends Schema.Class<ToolStateCompleted>("Session.Message.ToolState.Completed")({
export interface ToolStateCompleted extends Schema.Schema.Type<typeof ToolStateCompleted> {}
export const ToolStateCompleted = Schema.Struct({
status: Schema.Literal("completed"),
input: Schema.Record(Schema.String, Schema.Unknown),
attachments: FileAttachment.pipe(Schema.Array, Schema.optional),
@@ -102,23 +111,25 @@ export class ToolStateCompleted extends Schema.Class<ToolStateCompleted>("Sessio
outputPaths: Schema.Array(Schema.String).pipe(Schema.optional),
structured: Schema.Record(Schema.String, Schema.Any),
result: Schema.Unknown.pipe(Schema.optional),
}) {}
}).annotate({ identifier: "Session.Message.ToolState.Completed" })
export class ToolStateError extends Schema.Class<ToolStateError>("Session.Message.ToolState.Error")({
export interface ToolStateError extends Schema.Schema.Type<typeof ToolStateError> {}
export const ToolStateError = Schema.Struct({
status: Schema.Literal("error"),
input: Schema.Record(Schema.String, Schema.Unknown),
content: ToolContent.pipe(Schema.Array),
structured: Schema.Record(Schema.String, Schema.Any),
error: UnknownError,
result: Schema.Unknown.pipe(Schema.optional),
}) {}
}).annotate({ identifier: "Session.Message.ToolState.Error" })
export const ToolState = Schema.Union([ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]).pipe(
Schema.toTaggedUnion("status"),
)
export type ToolState = typeof ToolState.Type
export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError
export class AssistantTool extends Schema.Class<AssistantTool>("Session.Message.Assistant.Tool")({
export interface AssistantTool extends Schema.Schema.Type<typeof AssistantTool> {}
export const AssistantTool = Schema.Struct({
type: Schema.Literal("tool"),
id: Schema.String,
name: Schema.String,
@@ -134,27 +145,30 @@ export class AssistantTool extends Schema.Class<AssistantTool>("Session.Message.
completed: DateTimeUtcFromMillis.pipe(Schema.optional),
pruned: DateTimeUtcFromMillis.pipe(Schema.optional),
}),
}) {}
}).annotate({ identifier: "Session.Message.Assistant.Tool" })
export class AssistantText extends Schema.Class<AssistantText>("Session.Message.Assistant.Text")({
export interface AssistantText extends Schema.Schema.Type<typeof AssistantText> {}
export const AssistantText = Schema.Struct({
type: Schema.Literal("text"),
id: Schema.String,
text: Schema.String,
}) {}
}).annotate({ identifier: "Session.Message.Assistant.Text" })
export class AssistantReasoning extends Schema.Class<AssistantReasoning>("Session.Message.Assistant.Reasoning")({
export interface AssistantReasoning extends Schema.Schema.Type<typeof AssistantReasoning> {}
export const AssistantReasoning = Schema.Struct({
type: Schema.Literal("reasoning"),
id: Schema.String,
text: Schema.String,
providerMetadata: ProviderMetadata.pipe(Schema.optional),
}) {}
}).annotate({ identifier: "Session.Message.Assistant.Reasoning" })
export const AssistantContent = Schema.Union([AssistantText, AssistantReasoning, AssistantTool]).pipe(
Schema.toTaggedUnion("type"),
)
export type AssistantContent = typeof AssistantContent.Type
export type AssistantContent = AssistantText | AssistantReasoning | AssistantTool
export class Assistant extends Schema.Class<Assistant>("Session.Message.Assistant")({
export interface Assistant extends Schema.Schema.Type<typeof Assistant> {}
export const Assistant = Schema.Struct({
...Base,
type: Schema.Literal("assistant"),
agent: Schema.String,
@@ -177,15 +191,16 @@ export class Assistant extends Schema.Class<Assistant>("Session.Message.Assistan
created: DateTimeUtcFromMillis,
completed: DateTimeUtcFromMillis.pipe(Schema.optional),
}),
}) {}
}).annotate({ identifier: "Session.Message.Assistant" })
export class Compaction extends Schema.Class<Compaction>("Session.Message.Compaction")({
export interface Compaction extends Schema.Schema.Type<typeof Compaction> {}
export const Compaction = Schema.Struct({
type: Schema.Literal("compaction"),
reason: Schema.Literals(["auto", "manual"]),
summary: Schema.String,
recent: Schema.String,
...Base,
}) {}
}).annotate({ identifier: "Session.Message.Compaction" })
export const Message = Schema.Union([
AgentSwitched,
@@ -199,5 +214,5 @@ export const Message = Schema.Union([
])
.pipe(Schema.toTaggedUnion("type"))
.annotate({ identifier: "Session.Message" })
export type Message = typeof Message.Type
export type Message = AgentSwitched | ModelSwitched | User | Synthetic | System | Shell | Assistant | Compaction
export type Type = Message["type"]
@@ -1,21 +1,14 @@
export * as Session from "./session"
import { Schema } from "effect"
import { sha256 } from "@noble/hashes/sha2.js"
import { bytesToHex } from "@noble/hashes/utils.js"
import { Agent } from "./agent"
import { Location } from "./location"
import { Model } from "./model"
import { Project } from "./project"
import { DateTimeUtcFromMillis, optionalOmitUndefined, RelativePath } from "./schema"
import { DateTimeUtcFromMillis, externalID, type ExternalID, optionalOmitUndefined, RelativePath } from "./schema"
import { withStatics } from "./schema"
import { descending } from "./identifier"
export interface ExternalID {
readonly namespace: string
readonly key: string
}
export const ID = Schema.String.check(Schema.isStartsWith("ses")).pipe(
Schema.brand("SessionID"),
withStatics((schema) => {
@@ -23,16 +16,14 @@ export const ID = Schema.String.check(Schema.isStartsWith("ses")).pipe(
return {
create,
descending: (id?: string) => (id === undefined ? create() : schema.make(id)),
fromExternal: (input: ExternalID) =>
schema.make(
"ses_" + bytesToHex(sha256(new TextEncoder().encode(JSON.stringify([input.namespace, input.key])))),
),
fromExternal: (input: ExternalID) => schema.make(externalID("ses", input)),
}
}),
)
export type ID = typeof ID.Type
export class Info extends Schema.Class<Info>("SessionV2.Info")({
export interface Info extends Schema.Schema.Type<typeof Info> {}
export const Info = Schema.Struct({
id: ID,
parentID: ID.pipe(optionalOmitUndefined),
projectID: Project.ID,
@@ -56,7 +47,7 @@ export class Info extends Schema.Class<Info>("SessionV2.Info")({
title: Schema.String,
location: Location.Ref,
subpath: RelativePath.pipe(Schema.optional),
}) {}
}).annotate({ identifier: "SessionV2.Info" })
export const ListAnchor = Schema.Struct({
id: ID,
+27
View File
@@ -0,0 +1,27 @@
# @opencode-ai/sdk-next
Effect-native scoped OpenCode host for in-process applications. This transitional package will replace the existing generated `@opencode-ai/sdk` after its consumers migrate.
The SDK executes Server's assembled HTTP router in memory. It opens no listener and performs no network I/O, while preserving the same routing, middleware, handlers, codecs, and errors as the network client.
```ts
import { OpenCode } from "@opencode-ai/sdk-next"
const opencode = yield * OpenCode.create()
const session = yield * opencode.sessions.get({ sessionID })
```
It also exposes local-only `tools.register(...)`. Closing the owning Effect Scope releases router resources, location services, fibers, and scoped tool registrations.
The same constructor is available as a service Layer:
```ts
const program = Effect.gen(function* () {
const opencode = yield* OpenCode.Service
return yield* opencode.sessions.get({ sessionID })
})
yield * program.pipe(Effect.provide(OpenCode.layer))
```
`OpenCode.layer` adapts `OpenCode.create()` for dependency injection; it does not define another host implementation.
+25
View File
@@ -0,0 +1,25 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk-next",
"private": true,
"type": "module",
"license": "MIT",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"test": "bun test --timeout 5000",
"typecheck": "tsgo --noEmit"
},
"dependencies": {
"@opencode-ai/client": "workspace:*",
"@opencode-ai/core": "workspace:*",
"@opencode-ai/server": "workspace:*",
"effect": "catalog:"
},
"devDependencies": {
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:"
}
}
+16
View File
@@ -0,0 +1,16 @@
export * as OpenCode from "./opencode"
export { ClientError } from "@opencode-ai/client/effect"
export { Tool } from "@opencode-ai/core/public/tool"
export {
AbsolutePath,
Agent,
Location,
Model,
Prompt,
Provider,
RelativePath,
Session,
SessionInput,
SessionMessage,
} from "@opencode-ai/client/effect"
@@ -1,5 +1,5 @@
export * as OpenCode from "./effect-embedded"
import { OpenCode } from "@opencode-ai/client/effect"
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
import { ApplicationTools } from "@opencode-ai/core/tool/application-tools"
import { createEmbeddedRoutes } from "@opencode-ai/server/routes"
import { Cause, Context, Effect, Layer } from "effect"
@@ -11,20 +11,19 @@ import {
HttpServerRequest,
HttpServerResponse,
} from "effect/unstable/http"
import { OpenCode as Generated } from "./generated-effect/index"
export const create = Effect.fn("OpenCode.create")(function* () {
const context = yield* Layer.build(
Layer.merge(
createEmbeddedRoutes().pipe(Layer.provide(HttpServer.layerServices), Layer.provideMerge(HttpRouter.layer)),
ApplicationTools.layer,
),
)
const handler = Context.get(context, HttpRouter.HttpRouter).asHttpEffect()
const { handler, permissions, tools } = yield* Effect.all({
handler: HttpRouter.toHttpEffect(createEmbeddedRoutes().pipe(Layer.provide(HttpServer.layerServices))),
permissions: PermissionSaved.Service,
tools: ApplicationTools.Service,
}).pipe(Effect.provide(Layer.merge(ApplicationTools.layer, PermissionSaved.defaultLayer)))
const httpClient = HttpClient.make(
Effect.fnUntraced(function* (request) {
const response = yield* handler.pipe(
Effect.provideService(HttpServerRequest.HttpServerRequest, HttpServerRequest.fromClientRequest(request)),
Effect.provideService(ApplicationTools.Service, tools),
Effect.provideService(PermissionSaved.Service, permissions),
Effect.catchCause((cause) =>
Cause.hasInterruptsOnly(cause)
? Effect.interrupt
@@ -34,10 +33,9 @@ export const create = Effect.fn("OpenCode.create")(function* () {
return HttpServerResponse.toClientResponse(response, { request })
}, Effect.scoped),
)
const client = yield* Generated.make({ baseUrl: "http://opencode.local" }).pipe(
const client = yield* OpenCode.make({ baseUrl: "http://opencode.local" }).pipe(
Effect.provideService(HttpClient.HttpClient, httpClient),
)
const tools = Context.get(context, ApplicationTools.Service)
return {
...client,
tools: { register: tools.register },
@@ -46,20 +44,6 @@ export const create = Effect.fn("OpenCode.create")(function* () {
export type Interface = Effect.Success<ReturnType<typeof create>>
export class Service extends Context.Service<Service, Interface>()("@opencode-ai/client/OpenCode") {}
export class Service extends Context.Service<Service, Interface>()("@opencode-ai/sdk-next/OpenCode") {}
export const layer = Layer.effect(Service, create())
export { ClientError } from "./generated-effect/index"
export { Tool } from "@opencode-ai/core/public/tool"
export {
Agent,
Location,
Model,
AbsolutePath,
RelativePath,
Session,
SessionInput,
SessionMessage,
Prompt,
} from "./effect"
@@ -9,11 +9,9 @@ test("embedded client uses the real router and handlers", async () => {
const directory = await mkdtemp(join(tmpdir(), "opencode-embedded-"))
const database = Flag.OPENCODE_DB
Flag.OPENCODE_DB = join(directory, "opencode.sqlite")
const { AbsolutePath, Agent, Location, Model, OpenCode, Prompt, Session, Tool } = await import(
"../src/effect-embedded"
)
const { AbsolutePath, Agent, Location, Model, OpenCode, Prompt, Provider, Session, Tool } = await import("../src")
const sessionID = Session.ID.make(`ses_embedded_${crypto.randomUUID()}`)
const model = Model.Ref.make({ id: "embedded", providerID: "test" })
const model = Model.Ref.make({ id: Model.ID.make("embedded"), providerID: Provider.ID.make("test") })
try {
const program = Effect.gen(function* () {
@@ -37,7 +35,7 @@ test("embedded client uses the real router and handlers", async () => {
const page = yield* opencode.sessions.list({ directory: AbsolutePath.make(directory) })
const admitted = yield* opencode.sessions.prompt({
sessionID,
prompt: new Prompt({ text: "Do not run" }),
prompt: Prompt.make({ text: "Do not run" }),
resume: false,
})
const context = yield* opencode.sessions.context({ sessionID })
@@ -64,7 +62,7 @@ test("embedded client is available as a Layer service", async () => {
const directory = await mkdtemp(join(tmpdir(), "opencode-embedded-layer-"))
const database = Flag.OPENCODE_DB
Flag.OPENCODE_DB = join(directory, "opencode.sqlite")
const { AbsolutePath, Location, OpenCode, Session } = await import("../src/effect-embedded")
const { AbsolutePath, Location, OpenCode, Session } = await import("../src")
const sessionID = Session.ID.make(`ses_embedded_${crypto.randomUUID()}`)
try {
@@ -0,0 +1,53 @@
import { expect, test } from "bun:test"
import { mkdtemp, rm } from "node:fs/promises"
import { join, resolve, sep } from "node:path"
const directory = resolve(import.meta.dir, "..")
const client = resolve(import.meta.dir, "../../client")
const core = resolve(import.meta.dir, "../../core")
const server = resolve(import.meta.dir, "../../server")
test("bundles the client and in-memory host", async () => {
const inputs = await bundleInputs()
expect(within(inputs, client).length).toBeGreaterThan(0)
expect(within(inputs, core).length).toBeGreaterThan(0)
expect(within(inputs, server).length).toBeGreaterThan(0)
})
async function bundleInputs() {
const temporary = await mkdtemp(join(import.meta.dir, ".import-boundary-"))
const entrypoint = join(temporary, "index.ts")
const metafile = join(temporary, "meta.json")
try {
await Bun.write(entrypoint, 'export * from "@opencode-ai/sdk-next"')
const child = Bun.spawn(
[
process.execPath,
"build",
entrypoint,
"--target=bun",
"--format=esm",
"--packages=bundle",
`--metafile=${metafile}`,
`--outdir=${join(temporary, "out")}`,
],
{ cwd: directory, stdout: "pipe", stderr: "pipe" },
)
const [exitCode, stdout, stderr] = await Promise.all([
child.exited,
new Response(child.stdout).text(),
new Response(child.stderr).text(),
])
if (exitCode !== 0) throw new Error(stdout + stderr)
const metadata = await Bun.file(metafile).json()
return Object.keys(metadata.inputs).map((input) => resolve(directory, input))
} finally {
await rm(temporary, { recursive: true, force: true })
}
}
function within(inputs: ReadonlyArray<string>, directory: string) {
const prefix = directory.endsWith(sep) ? directory : directory + sep
return inputs.filter((input) => input === directory || input.startsWith(prefix))
}
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/bun/tsconfig.json",
"compilerOptions": {
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"noUncheckedIndexedAccess": false
}
}
+1 -1
View File
@@ -53,6 +53,6 @@ export const handlers = Layer.mergeAll(
Layer.provide(SessionExecutionLocal.defaultLayer),
Layer.provide(PermissionSaved.defaultLayer),
Layer.provide(PtyTicket.defaultLayer),
Layer.provide(LocationServiceMap.layer),
Layer.provide(LocationServiceMap.layerWithApplicationTools),
Layer.provide(Credential.defaultLayer),
)
+2 -1
View File
@@ -1,5 +1,6 @@
import { Database } from "@opencode-ai/core/database/database"
import { EventV2 } from "@opencode-ai/core/event"
import { ApplicationTools } from "@opencode-ai/core/tool/application-tools"
import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { Layer, Option } from "effect"
@@ -15,7 +16,7 @@ export function createRoutes(password?: string) {
password
? ServerAuth.Config.layer({ username: "opencode", password: Option.some(password) })
: ServerAuth.Config.defaultLayer,
)
).pipe(Layer.provide(ApplicationTools.layer))
}
export function createEmbeddedRoutes() {