Merge remote-tracking branch 'origin/v2' into search-integration
# Conflicts: # packages/client/test/promise.test.ts # packages/core/schema.json # packages/core/src/database/migration.gen.ts # packages/core/src/tool/websearch.ts # packages/sdk-next/src/index.ts # packages/sdk/js/src/v2/gen/types.gen.ts
This commit is contained in:
commit
7b8d8b8861
666 changed files with 46671 additions and 20220 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/server",
|
||||
"version": "1.17.13",
|
||||
"version": "1.17.14",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
|
|
|||
|
|
@ -20,41 +20,36 @@ function eventData(data: unknown): Sse.Event {
|
|||
export const EventHandler = HttpApiBuilder.group(Api, "server.event", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
return handlers
|
||||
.handle(
|
||||
"event.changes",
|
||||
Effect.fn(() => Effect.succeed(events.changes())),
|
||||
)
|
||||
.handleRaw("event.subscribe", () =>
|
||||
Effect.gen(function* () {
|
||||
const connected = {
|
||||
id: EventV2.ID.create(),
|
||||
type: "server.connected",
|
||||
data: {},
|
||||
}
|
||||
const output = Stream.unwrap(
|
||||
Effect.gen(function* () {
|
||||
// Acquiring the bounded stream installs its listener before readiness is observable.
|
||||
const live = yield* EventV2.liveBounded(events, {
|
||||
capacity: subscriberCapacity,
|
||||
accept: isOpenCodeEvent,
|
||||
})
|
||||
return Stream.make(connected).pipe(Stream.concat(live))
|
||||
}),
|
||||
).pipe(Stream.map(eventData), Stream.pipeThroughChannel(Sse.encode()))
|
||||
const heartbeat = Stream.tick("15 seconds").pipe(Stream.map(() => ": heartbeat\n\n"))
|
||||
return HttpServerResponse.stream(
|
||||
output.pipe(Stream.merge(heartbeat, { haltStrategy: "left" }), Stream.encodeText),
|
||||
{
|
||||
contentType: "text/event-stream",
|
||||
headers: {
|
||||
"Cache-Control": "no-cache, no-transform",
|
||||
"X-Accel-Buffering": "no",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
},
|
||||
return handlers.handleRaw("event.subscribe", () =>
|
||||
Effect.gen(function* () {
|
||||
const connected = {
|
||||
id: EventV2.ID.create(),
|
||||
type: "server.connected",
|
||||
data: {},
|
||||
}
|
||||
const output = Stream.unwrap(
|
||||
Effect.gen(function* () {
|
||||
// Acquiring the bounded stream installs its listener before readiness is observable.
|
||||
const live = yield* EventV2.liveBounded(events, {
|
||||
capacity: subscriberCapacity,
|
||||
accept: isOpenCodeEvent,
|
||||
})
|
||||
return Stream.make(connected).pipe(Stream.concat(live))
|
||||
}),
|
||||
).pipe(Stream.map(eventData), Stream.pipeThroughChannel(Sse.encode()))
|
||||
const heartbeat = Stream.tick("15 seconds").pipe(Stream.map(() => ": heartbeat\n\n"))
|
||||
return HttpServerResponse.stream(
|
||||
output.pipe(Stream.merge(heartbeat, { haltStrategy: "left" }), Stream.encodeText),
|
||||
{
|
||||
contentType: "text/event-stream",
|
||||
headers: {
|
||||
"Cache-Control": "no-cache, no-transform",
|
||||
"X-Accel-Buffering": "no",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
},
|
||||
)
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -38,10 +38,6 @@ export const MessageHandler = HttpApiBuilder.group(Api, "server.message", (handl
|
|||
catch: () => new InvalidCursorError({ message: "Invalid cursor" }),
|
||||
})
|
||||
const order = decoded?.order ?? ctx.query.order ?? "desc"
|
||||
// Read the watermark before the snapshot: an understated watermark only
|
||||
// redelivers already-reflected events, while an overstated one would let
|
||||
// an attached tail skip events missing from the snapshot.
|
||||
const watermark = (yield* session.watermarks([ctx.params.sessionID])).get(ctx.params.sessionID)
|
||||
const messages = yield* session
|
||||
.messages({
|
||||
sessionID: ctx.params.sessionID,
|
||||
|
|
@ -74,7 +70,6 @@ export const MessageHandler = HttpApiBuilder.group(Api, "server.message", (handl
|
|||
const last = messages.at(-1)
|
||||
return {
|
||||
data: messages,
|
||||
watermark,
|
||||
cursor: {
|
||||
previous: first ? cursor.encode(first, order, "previous") : undefined,
|
||||
next: last ? cursor.encode(last, order, "next") : undefined,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { SessionContextEntry } from "@opencode-ai/core/session/context-entry"
|
||||
import { InstructionEntry } from "@opencode-ai/core/session/instruction-entry"
|
||||
import { DateTime, Effect, Stream } from "effect"
|
||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
|
|
@ -9,6 +9,7 @@ import {
|
|||
CommandEvaluationError,
|
||||
CommandNotFoundError,
|
||||
InvalidCursorError,
|
||||
InvalidRequestError,
|
||||
MessageNotFoundError,
|
||||
ServiceUnavailableError,
|
||||
SessionBusyError,
|
||||
|
|
@ -44,7 +45,6 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
|||
const last = sessions.at(-1)
|
||||
return {
|
||||
data: sessions,
|
||||
watermarks: Object.fromEntries(page.watermarks),
|
||||
cursor: {
|
||||
previous: first
|
||||
? SessionsCursor.make({
|
||||
|
|
@ -89,10 +89,8 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
|||
"session.active",
|
||||
Effect.fn(function* () {
|
||||
const active = yield* session.active
|
||||
const watermarks = yield* session.watermarks(Array.from(active))
|
||||
return {
|
||||
data: Object.fromEntries(Array.from(active, (sessionID) => [sessionID, { type: "running" as const }])),
|
||||
watermarks: Object.fromEntries(watermarks),
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
|
@ -113,6 +111,22 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
|||
}
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.remove",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* session.remove(ctx.params.sessionID).pipe(
|
||||
Effect.catchTag(
|
||||
"Session.NotFoundError",
|
||||
(error) =>
|
||||
new SessionNotFoundError({
|
||||
sessionID: error.sessionID,
|
||||
message: `Session not found: ${error.sessionID}`,
|
||||
}),
|
||||
),
|
||||
)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.fork",
|
||||
Effect.fn(function* (ctx) {
|
||||
|
|
@ -216,6 +230,9 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
|||
}),
|
||||
),
|
||||
),
|
||||
Effect.catchTag("Session.AttachmentError", (error) =>
|
||||
Effect.fail(new InvalidRequestError({ message: error.message, field: "prompt.files" })),
|
||||
),
|
||||
),
|
||||
}
|
||||
}),
|
||||
|
|
@ -270,6 +287,9 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
|||
}),
|
||||
),
|
||||
),
|
||||
Effect.catchTag("Session.AttachmentError", (error) =>
|
||||
Effect.fail(new InvalidRequestError({ message: error.message, field: "files" })),
|
||||
),
|
||||
),
|
||||
}
|
||||
}),
|
||||
|
|
@ -344,44 +364,26 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
|||
.handle(
|
||||
"session.compact",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* session.compact({ sessionID: ctx.params.sessionID }).pipe(
|
||||
Effect.catchTag("Session.NotFoundError", (error) =>
|
||||
Effect.fail(
|
||||
new SessionNotFoundError({
|
||||
sessionID: error.sessionID,
|
||||
message: `Session not found: ${error.sessionID}`,
|
||||
}),
|
||||
),
|
||||
),
|
||||
Effect.catchTag("Session.OperationUnavailableError", (error) =>
|
||||
Effect.fail(
|
||||
new ServiceUnavailableError({
|
||||
message: `Session ${error.operation} is not available yet`,
|
||||
service: `session.${error.operation}`,
|
||||
}),
|
||||
),
|
||||
),
|
||||
Effect.catchTag(
|
||||
"Session.BusyError",
|
||||
(error) =>
|
||||
new SessionBusyError({
|
||||
sessionID: error.sessionID,
|
||||
message: `Session is busy: ${error.sessionID}`,
|
||||
}),
|
||||
),
|
||||
Effect.catchTag("Session.MessageDecodeError", (error) => {
|
||||
const ref = `err_${crypto.randomUUID().slice(0, 8)}`
|
||||
return Effect.logError("failed to decode session message during compaction").pipe(
|
||||
Effect.annotateLogs({ ref, sessionID: error.sessionID, messageID: error.messageID }),
|
||||
Effect.andThen(
|
||||
Effect.fail(
|
||||
new UnknownError({ message: "Unexpected server error. Check server logs for details.", ref }),
|
||||
),
|
||||
return {
|
||||
data: yield* session.compact({ sessionID: ctx.params.sessionID, id: ctx.payload.id }).pipe(
|
||||
Effect.catchTag("Session.NotFoundError", (error) =>
|
||||
Effect.fail(
|
||||
new SessionNotFoundError({
|
||||
sessionID: error.sessionID,
|
||||
message: `Session not found: ${error.sessionID}`,
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
),
|
||||
Effect.catchTag("Session.CompactionConflictError", (error) =>
|
||||
Effect.fail(
|
||||
new ConflictError({
|
||||
message: `Compaction input ID conflicts with an existing durable record: ${error.inputID}`,
|
||||
resource: error.inputID,
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
|
|
@ -544,25 +546,25 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
|||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.context.entry.list",
|
||||
"session.instructions.entry.list",
|
||||
Effect.fn(function* (ctx) {
|
||||
const contextEntries = yield* SessionContextEntry.Service
|
||||
return { data: yield* contextEntries.list(ctx.params.sessionID) }
|
||||
const instructions = yield* InstructionEntry.Service
|
||||
return { data: yield* instructions.list(ctx.params.sessionID) }
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.context.entry.put",
|
||||
"session.instructions.entry.put",
|
||||
Effect.fn(function* (ctx) {
|
||||
const contextEntries = yield* SessionContextEntry.Service
|
||||
yield* contextEntries.put({ sessionID: ctx.params.sessionID, key: ctx.params.key, value: ctx.payload.value })
|
||||
const instructions = yield* InstructionEntry.Service
|
||||
yield* instructions.put({ sessionID: ctx.params.sessionID, key: ctx.params.key, value: ctx.payload.value })
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.context.entry.remove",
|
||||
"session.instructions.entry.remove",
|
||||
Effect.fn(function* (ctx) {
|
||||
const contextEntries = yield* SessionContextEntry.Service
|
||||
yield* contextEntries.remove({ sessionID: ctx.params.sessionID, key: ctx.params.key })
|
||||
const instructions = yield* InstructionEntry.Service
|
||||
yield* instructions.remove({ sessionID: ctx.params.sessionID, key: ctx.params.key })
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,20 @@ export const ShellHandler = HttpApiBuilder.group(Api, "server.shell", (handlers)
|
|||
)
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"shell.timeout",
|
||||
Effect.fn(function* (ctx) {
|
||||
const shell = yield* Shell.Service
|
||||
return yield* response(
|
||||
shell.timeout(ctx.params.id, ctx.payload.timeout).pipe(
|
||||
Effect.catchTag(
|
||||
"Shell.NotFoundError",
|
||||
() => new ShellNotFoundError({ id: ctx.params.id, message: `Shell command not found: ${ctx.params.id}` }),
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"shell.output",
|
||||
Effect.fn(function* (ctx) {
|
||||
|
|
|
|||
|
|
@ -70,15 +70,17 @@ function makeRoutes<AuthError, AuthServices>(
|
|||
[PluginRuntime.providerNode, PluginRuntime.providerNodeWithCell(pluginRuntimeCell)],
|
||||
...(sdkPlugins ? [[SdkPlugins.node, SdkPlugins.layerWithStore(sdkPlugins)] as const] : []),
|
||||
]
|
||||
// Simulation replacements are loaded via dynamic import so the simulation
|
||||
// module is never eagerly loaded. Layer.unwrap defers both the import and
|
||||
// the app-node build to layer-build time; when simulation is off the branch
|
||||
// is byte-for-byte identical to a plain AppNodeBuilder.build call.
|
||||
const serviceLayer = simulationEnabled()
|
||||
const serviceLayer = simulateEnabled()
|
||||
? Layer.unwrap(
|
||||
Effect.gen(function* () {
|
||||
const { simulationReplacements } = yield* Effect.promise(() => import("@opencode-ai/simulation/backend"))
|
||||
return AppNodeBuilder.build(applicationServices, [...replacements, ...simulationReplacements])
|
||||
const { simulationReplacements, startDriveServer } = yield* Effect.promise(() =>
|
||||
import("@opencode-ai/simulation/backend"),
|
||||
)
|
||||
if (driveEnabled()) startDriveServer()
|
||||
return AppNodeBuilder.build(applicationServices, [
|
||||
...replacements,
|
||||
...(simulateEnabled() ? simulationReplacements : []),
|
||||
])
|
||||
}),
|
||||
)
|
||||
: AppNodeBuilder.build(applicationServices, replacements)
|
||||
|
|
@ -96,8 +98,12 @@ function makeRoutes<AuthError, AuthServices>(
|
|||
)
|
||||
}
|
||||
|
||||
function simulationEnabled() {
|
||||
return !!process.env.OPENCODE_SIMULATION
|
||||
function simulateEnabled() {
|
||||
return !!process.env.OPENCODE_SIMULATE
|
||||
}
|
||||
|
||||
function driveEnabled() {
|
||||
return !!process.env.OPENCODE_DRIVE
|
||||
}
|
||||
|
||||
export const routes = createRoutes()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue