refactor(sdk): extract shared schema and embedded host
This commit is contained in:
+9
-5
@@ -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.
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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))),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,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,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,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
|
||||
|
||||
@@ -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
@@ -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,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,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 = {
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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,2 +1,2 @@
|
||||
export * as SessionMessage from "./message"
|
||||
export * from "@opencode-ai/model/session-message"
|
||||
export * from "@opencode-ai/schema/session-message"
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { AgentAttachment, FileAttachment, Prompt, Source } from "@opencode-ai/model/prompt"
|
||||
export { AgentAttachment, FileAttachment, Prompt, Source } from "@opencode-ai/schema/prompt"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export { DateTimeUtcFromMillis } from "@opencode-ai/model/schema"
|
||||
|
||||
export * as V2Schema from "./v2-schema"
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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", () =>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"typecheck": "tsgo --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opencode-ai/model": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"effect": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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" })
|
||||
@@ -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,
|
||||
@@ -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.
|
||||
@@ -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:"
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/bun/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"noUncheckedIndexedAccess": false
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user