mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-29 21:16:10 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
264071f901 | ||
|
|
283ba56335 | ||
|
|
c7b551fbe1 | ||
|
|
85e931ff44 | ||
|
|
c7f93304b6 |
@@ -0,0 +1,9 @@
|
||||
---
|
||||
"@opencode-ai/protocol": minor
|
||||
"@opencode-ai/client": minor
|
||||
"@opencode-ai/plugin": minor
|
||||
"@opencode-ai/schema": minor
|
||||
"@opencode-ai/sdk": minor
|
||||
---
|
||||
|
||||
Expose process-local execution status on session get responses.
|
||||
@@ -29,7 +29,10 @@ afterEach(() => mock.restore())
|
||||
describe("session target resolver", () => {
|
||||
test("adopts an explicit Session location and model", async () => {
|
||||
const client = OpenCode.make({ baseUrl: "https://opencode.test" })
|
||||
const selected = session("ses_resume", "/session", "work_1", { providerID: "openai", id: "gpt-5" })
|
||||
const selected = {
|
||||
...session("ses_resume", "/session", "work_1", { providerID: "openai", id: "gpt-5" }),
|
||||
executing: false,
|
||||
}
|
||||
spyOn(client.session, "get").mockResolvedValue(selected)
|
||||
spyOn(client.location, "get").mockResolvedValue(location("/session", "work_1"))
|
||||
|
||||
@@ -57,7 +60,7 @@ describe("session target resolver", () => {
|
||||
|
||||
test("attaches the terminal environment to the resolved local Session", async () => {
|
||||
const client = OpenCode.make({ baseUrl: "https://opencode.test" })
|
||||
const selected = session("ses_resume", "/session")
|
||||
const selected = { ...session("ses_resume", "/session"), executing: false }
|
||||
spyOn(client.session, "get").mockResolvedValue(selected)
|
||||
spyOn(client.location, "get").mockResolvedValue(location("/session"))
|
||||
const environment = spyOn(client.session, "environment").mockResolvedValue()
|
||||
|
||||
@@ -203,7 +203,7 @@ export type SessionActiveOutput = { readonly [x: Session.ID]: { readonly type: "
|
||||
export type SessionActiveOperation<E = never> = () => Effect.Effect<SessionActiveOutput, E>
|
||||
|
||||
export type SessionGetInput = { readonly sessionID: Session.ID }
|
||||
export type SessionGetOutput = Session.Info
|
||||
export type SessionGetOutput = Session.GetInfo
|
||||
export type SessionGetOperation<E = never> = (input: SessionGetInput) => Effect.Effect<SessionGetOutput, E>
|
||||
|
||||
export type SessionRemoveInput = { readonly sessionID: Session.ID }
|
||||
|
||||
@@ -1545,6 +1545,24 @@ export type SessionInfo = {
|
||||
revert?: SessionRevert
|
||||
}
|
||||
|
||||
export type SessionGetInfo = {
|
||||
id: string
|
||||
parentID?: string
|
||||
fork?: { sessionID: string; boundary: SessionForkBoundary }
|
||||
projectID: string
|
||||
agent?: string
|
||||
model?: ModelRef
|
||||
cost: MoneyUSD
|
||||
tokens: TokenUsageInfo
|
||||
outcome?: "succeeded" | "failed" | "interrupted"
|
||||
time: { created: number; updated: number; idle?: number; viewed?: number; archived?: number }
|
||||
title?: string
|
||||
location: LocationRef
|
||||
subpath?: string
|
||||
revert?: SessionRevert
|
||||
executing: boolean
|
||||
}
|
||||
|
||||
export type SessionRevertStaged = {
|
||||
id: string
|
||||
created: number
|
||||
@@ -3399,7 +3417,7 @@ export type SessionActiveOutput = { data: { [x: string]: SessionActive } }["data
|
||||
|
||||
export type SessionGetInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
||||
|
||||
export type SessionGetOutput = { data: SessionInfo }["data"]
|
||||
export type SessionGetOutput = { data: SessionGetInfo }["data"]
|
||||
|
||||
export type SessionRemoveInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ test("effect entrypoint exposes canonical Schema contracts", () => {
|
||||
test("generated Effect API names canonical and composed outputs", async () => {
|
||||
const source = await Bun.file(new URL("../src/effect/api/api.ts", import.meta.url)).text()
|
||||
|
||||
expect(source).toContain("export type SessionGetOutput = Session.Info")
|
||||
expect(source).toContain("export type SessionGetOutput = Session.GetInfo")
|
||||
expect(source).toContain("export type EventSubscribeOutput = OpenCodeEvent")
|
||||
expect(source).not.toContain("HttpApiClient.ForApi")
|
||||
})
|
||||
|
||||
@@ -37,6 +37,7 @@ test("session.get returns the decoded Effect projection", async () => {
|
||||
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
|
||||
|
||||
expect(DateTime.toEpochMillis(result.time.created)).toBe(1_717_171_717_000)
|
||||
expect(result.executing).toBe(true)
|
||||
})
|
||||
|
||||
test("session instructions methods use the public HTTP contract", async () => {
|
||||
@@ -279,6 +280,7 @@ const session = {
|
||||
},
|
||||
title: "Test",
|
||||
location: { directory: "/tmp/project" },
|
||||
executing: true,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -360,6 +360,7 @@ test("session.get returns the wire projection", async () => {
|
||||
const result = await client.session.get({ sessionID: "ses_test" })
|
||||
|
||||
expect(result.time.created).toBe(1_717_171_717_000)
|
||||
expect(result.executing).toBe(true)
|
||||
})
|
||||
|
||||
test("session instructions methods use the public HTTP contract", async () => {
|
||||
@@ -680,6 +681,7 @@ const session = {
|
||||
},
|
||||
title: "Test",
|
||||
location: { directory: "/tmp/project" },
|
||||
executing: true,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { IntegrationMethodRegistration } from "@opencode-ai/plugin/effect/i
|
||||
import { EventManifest } from "@opencode-ai/schema/event-manifest"
|
||||
import { Mcp } from "@opencode-ai/schema/mcp"
|
||||
import { App } from "../app.js"
|
||||
import { Effect, Schema, Stream } from "effect"
|
||||
import { Effect, Schema, Stream, Struct } from "effect"
|
||||
import { Agent } from "../agent.js"
|
||||
import { AISDK } from "../aisdk.js"
|
||||
import { Catalog } from "../catalog.js"
|
||||
@@ -394,7 +394,11 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: Interface, p
|
||||
location:
|
||||
input?.location ?? Location.Ref.make({ directory: location.directory, workspaceID: location.workspaceID }),
|
||||
}),
|
||||
get: (input) => runtime.session.get(input.sessionID),
|
||||
get: (input) =>
|
||||
Effect.gen(function* () {
|
||||
const info = yield* runtime.session.get(input.sessionID)
|
||||
return Struct.assign(info, { executing: (yield* runtime.session.active).has(info.id) })
|
||||
}),
|
||||
switchAgent: runtime.session.switchAgent,
|
||||
switchModel: runtime.session.switchModel,
|
||||
prompt: runtime.session.prompt,
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface Interface {
|
||||
readonly session: Pick<
|
||||
Session.Interface,
|
||||
| "get"
|
||||
| "active"
|
||||
| "create"
|
||||
| "messages"
|
||||
| "prompt"
|
||||
@@ -69,6 +70,7 @@ export const layerWithCell = (cell: Cell) =>
|
||||
Service.of({
|
||||
session: {
|
||||
get: (sessionID) => require(cell, (runtime) => runtime.session.get(sessionID)),
|
||||
active: require(cell, (runtime) => runtime.session.active),
|
||||
create: (input) => require(cell, (runtime) => runtime.session.create(input)),
|
||||
messages: (input) => require(cell, (runtime) => runtime.session.messages(input)),
|
||||
prompt: (input) => require(cell, (runtime) => runtime.session.prompt(input)),
|
||||
|
||||
@@ -229,13 +229,13 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
.add(
|
||||
HttpApiEndpoint.get("session.get", "/api/session/:sessionID", {
|
||||
params: { sessionID: Session.ID },
|
||||
success: Schema.Struct({ data: Session.Info }),
|
||||
success: Schema.Struct({ data: Session.GetInfo }),
|
||||
error: SessionNotFoundError,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.get",
|
||||
summary: "Get session",
|
||||
description: "Retrieve a session by ID.",
|
||||
description: "Retrieve a session by ID with its process-local runtime execution status.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -52,6 +52,15 @@ export const Info = Schema.Struct({
|
||||
revert: Revert.pipe(optional),
|
||||
}).annotate({ identifier: "Session.Info" })
|
||||
|
||||
/** Session projection enriched with process-local runtime liveness. */
|
||||
export interface GetInfo extends Schema.Schema.Type<typeof GetInfo> {}
|
||||
export const GetInfo = Schema.Struct({
|
||||
...Info.fields,
|
||||
executing: Schema.Boolean.annotate({
|
||||
description: "Whether this OpenCode process is currently executing the session.",
|
||||
}),
|
||||
}).annotate({ identifier: "Session.GetInfo" })
|
||||
|
||||
export const ListAnchor = Schema.Struct({
|
||||
id: ID,
|
||||
time: Schema.Finite,
|
||||
|
||||
@@ -301,6 +301,7 @@ it.live(
|
||||
expect(created.id).toBe(id)
|
||||
expect(selected.model?.id).toBe(model.id)
|
||||
expect(selected.model?.providerID).toBe(model.providerID)
|
||||
expect(selected.executing).toBe(false)
|
||||
expect(page.data.some((session) => session.id === id)).toBe(true)
|
||||
expect(active).toEqual({})
|
||||
expect(admitted.sessionID).toBe(id)
|
||||
|
||||
@@ -43,6 +43,7 @@ test("Promise host uses the embedded router and releases plugins", async () => {
|
||||
const events = Array.fromAsync(opencode.sessions.log({ sessionID: session.id }))
|
||||
|
||||
expect(selected.id).toBe(session.id)
|
||||
expect(selected.executing).toBe(false)
|
||||
expect(page.data.some((item) => item.id === session.id)).toBe(true)
|
||||
expect((await events).some((event) => event.type === "session.created")).toBe(true)
|
||||
expect(setup).toBe(true)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Session } from "@opencode-ai/core/session"
|
||||
import { SessionStats } from "@opencode-ai/core/session/stats"
|
||||
import { SessionTransfer } from "@opencode-ai/core/session/transfer"
|
||||
import { InstructionEntry } from "@opencode-ai/core/session/instruction-entry"
|
||||
import { DateTime, Effect, Stream } from "effect"
|
||||
import { DateTime, Effect, Stream, Struct } from "effect"
|
||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { SessionsCursor } from "@opencode-ai/protocol/groups/session"
|
||||
@@ -171,10 +171,11 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||
.handle(
|
||||
"session.get",
|
||||
Effect.fn(function* (ctx) {
|
||||
const info = yield* session
|
||||
.get(ctx.params.sessionID)
|
||||
.pipe(Effect.catchTag("Session.NotFoundError", missingSession))
|
||||
return {
|
||||
data: yield* session
|
||||
.get(ctx.params.sessionID)
|
||||
.pipe(Effect.catchTag("Session.NotFoundError", missingSession)),
|
||||
data: Struct.assign(info, { executing: (yield* session.active).has(info.id) }),
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { expect } from "bun:test"
|
||||
import { createServer, type Server } from "node:http"
|
||||
import { makeMemoryDriver } from "@opencode-ai/core/environment/index"
|
||||
import { Session } from "@opencode-ai/core/session"
|
||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
import { Workspace } from "@opencode-ai/core/workspace"
|
||||
import { WorkspaceDriver } from "@opencode-ai/core/workspace/driver"
|
||||
import { Effect } from "effect"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { it } from "../../core/test/lib/effect"
|
||||
import { ServerFetch } from "../src/fetch"
|
||||
|
||||
@@ -253,6 +255,52 @@ it.live("serves the session view operation and missing-session error", () =>
|
||||
}).pipe(Effect.scoped),
|
||||
)
|
||||
|
||||
it.live("reports per-session runtime execution status", () =>
|
||||
Effect.gen(function* () {
|
||||
const executing = Session.ID.make("ses_executing")
|
||||
const idle = Session.ID.make("ses_idle")
|
||||
const handler = yield* ServerFetch.make(options, {
|
||||
overrides: [
|
||||
[
|
||||
SessionExecution.node,
|
||||
Layer.succeed(
|
||||
SessionExecution.Service,
|
||||
SessionExecution.Service.of({
|
||||
active: Effect.succeed(new Set([executing])),
|
||||
resume: () => Effect.void,
|
||||
wake: () => Effect.void,
|
||||
interrupt: () => Effect.succeed(false),
|
||||
awaitIdle: () => Effect.void,
|
||||
}),
|
||||
),
|
||||
],
|
||||
],
|
||||
})
|
||||
|
||||
yield* Effect.forEach([executing, idle], (id) =>
|
||||
Effect.promise(() =>
|
||||
handler(
|
||||
new Request("http://opencode.local/api/session", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ id }),
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
const responses = yield* Effect.forEach([executing, idle], (id) =>
|
||||
Effect.promise(() =>
|
||||
handler(new Request(`http://opencode.local/api/session/${id}`)).then((response) => response.json()),
|
||||
),
|
||||
)
|
||||
|
||||
expect(responses).toMatchObject([
|
||||
{ data: { id: executing, executing: true } },
|
||||
{ data: { id: idle, executing: false } },
|
||||
])
|
||||
}).pipe(Effect.scoped),
|
||||
)
|
||||
|
||||
// Pins the eager-boot guarantee: the application layer is built before the handler returns, so
|
||||
// an aborted first request cannot interrupt layer construction and wedge every later request
|
||||
// (the Effect-TS/effect#6319 failure class that lazy first-request builds are prone to).
|
||||
|
||||
@@ -190,6 +190,7 @@ describe("run session shared", () => {
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
model: { providerID: "openai", id: "gpt-5", variant: "high" },
|
||||
executing: false,
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user