mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 02:24:10 +08:00
feat(plugin): support parent session creation
Co-authored-by: dbpolito <347400+dbpolito@users.noreply.github.com>
This commit is contained in:
committed by
opencode-agent[bot]
parent
b020f1018b
commit
d3a156097d
@@ -198,6 +198,7 @@ export type SessionStatsOperation<E = never> = (input?: SessionStatsInput) => Ef
|
||||
|
||||
export type SessionCreateInput = {
|
||||
readonly id?: Session.ID | undefined
|
||||
readonly parentID?: Session.ID | undefined
|
||||
readonly title?: string | undefined
|
||||
readonly agent?: Agent.ID | undefined
|
||||
readonly model?: Model.Ref | undefined
|
||||
|
||||
@@ -393,6 +393,7 @@ const EndpointSessionCreate = (raw: RawClient["server.session"]) => (input?: Ses
|
||||
raw["session.create"]({
|
||||
payload: {
|
||||
id: input?.["id"],
|
||||
parentID: input?.["parentID"],
|
||||
title: input?.["title"],
|
||||
agent: input?.["agent"],
|
||||
model: input?.["model"],
|
||||
|
||||
@@ -577,6 +577,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/session`,
|
||||
body: {
|
||||
id: input?.["id"],
|
||||
parentID: input?.["parentID"],
|
||||
title: input?.["title"],
|
||||
agent: input?.["agent"],
|
||||
model: input?.["model"],
|
||||
@@ -585,7 +586,7 @@ export function make(options: ClientOptions) {
|
||||
permissions: input?.["permissions"],
|
||||
},
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
|
||||
@@ -2914,6 +2914,7 @@ export type SessionStatsOutput = { data: SessionStatsInfo }["data"]
|
||||
export type SessionCreateInput = {
|
||||
readonly id?: {
|
||||
readonly id?: string | null
|
||||
readonly parentID?: string | null
|
||||
readonly title?: string | null
|
||||
readonly agent?: string | null
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||
@@ -2925,8 +2926,23 @@ export type SessionCreateInput = {
|
||||
readonly effect: "allow" | "deny" | "ask"
|
||||
}> | null
|
||||
}["id"]
|
||||
readonly parentID?: {
|
||||
readonly id?: string | null
|
||||
readonly parentID?: string | null
|
||||
readonly title?: string | null
|
||||
readonly agent?: string | null
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||
readonly location?: { readonly directory: string } | null
|
||||
readonly metadata?: { readonly [x: string]: JsonValue } | null
|
||||
readonly permissions?: ReadonlyArray<{
|
||||
readonly action: string
|
||||
readonly resource: string
|
||||
readonly effect: "allow" | "deny" | "ask"
|
||||
}> | null
|
||||
}["parentID"]
|
||||
readonly title?: {
|
||||
readonly id?: string | null
|
||||
readonly parentID?: string | null
|
||||
readonly title?: string | null
|
||||
readonly agent?: string | null
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||
@@ -2940,6 +2956,7 @@ export type SessionCreateInput = {
|
||||
}["title"]
|
||||
readonly agent?: {
|
||||
readonly id?: string | null
|
||||
readonly parentID?: string | null
|
||||
readonly title?: string | null
|
||||
readonly agent?: string | null
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||
@@ -2953,6 +2970,7 @@ export type SessionCreateInput = {
|
||||
}["agent"]
|
||||
readonly model?: {
|
||||
readonly id?: string | null
|
||||
readonly parentID?: string | null
|
||||
readonly title?: string | null
|
||||
readonly agent?: string | null
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||
@@ -2966,6 +2984,7 @@ export type SessionCreateInput = {
|
||||
}["model"]
|
||||
readonly location?: {
|
||||
readonly id?: string | null
|
||||
readonly parentID?: string | null
|
||||
readonly title?: string | null
|
||||
readonly agent?: string | null
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||
@@ -2979,6 +2998,7 @@ export type SessionCreateInput = {
|
||||
}["location"]
|
||||
readonly metadata?: {
|
||||
readonly id?: string | null
|
||||
readonly parentID?: string | null
|
||||
readonly title?: string | null
|
||||
readonly agent?: string | null
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||
@@ -2992,6 +3012,7 @@ export type SessionCreateInput = {
|
||||
}["metadata"]
|
||||
readonly permissions?: {
|
||||
readonly id?: string | null
|
||||
readonly parentID?: string | null
|
||||
readonly title?: string | null
|
||||
readonly agent?: string | null
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { OpenCode } from "../src/promise/index"
|
||||
|
||||
test("session.create forwards parentID from the Promise client", async () => {
|
||||
let body: unknown
|
||||
const client = OpenCode.make({
|
||||
baseUrl: "http://localhost:3000",
|
||||
fetch: async (_input, init) => {
|
||||
body = JSON.parse(String(init?.body))
|
||||
return Response.json({ data: { id: "ses_child" } })
|
||||
},
|
||||
})
|
||||
|
||||
const child = await client.session.create({ parentID: "ses_parent", title: "Child" })
|
||||
|
||||
expect(child.id).toBe("ses_child")
|
||||
expect(body).toEqual({ parentID: "ses_parent", title: "Child" })
|
||||
})
|
||||
@@ -534,8 +534,13 @@ export const make = Effect.fn("PluginHost.make")(function* (
|
||||
model: input?.model,
|
||||
metadata: input?.metadata,
|
||||
permissions: input?.permissions,
|
||||
location:
|
||||
input?.location ?? Location.Ref.make({ directory: location.directory, workspaceID: location.workspaceID }),
|
||||
...(input?.parentID === undefined
|
||||
? {
|
||||
location:
|
||||
input?.location ??
|
||||
Location.Ref.make({ directory: location.directory, workspaceID: location.workspaceID }),
|
||||
}
|
||||
: { parentID: input.parentID }),
|
||||
}),
|
||||
get: (input) => sessions.get(input.sessionID),
|
||||
switchAgent: sessions.switchAgent,
|
||||
|
||||
@@ -8,9 +8,10 @@ import { Credential } from "@opencode/core/credential"
|
||||
import { Integration } from "@opencode/core/integration"
|
||||
import { Plugin } from "@opencode/core/plugin"
|
||||
import { PluginModule } from "@opencode/core/plugin/module"
|
||||
import { Session } from "@opencode/core/session"
|
||||
import { SessionSchema } from "@opencode/core/session/schema"
|
||||
import { Watcher } from "@opencode/core/filesystem/watcher"
|
||||
import { fromPromise } from "@opencode/plugin/promise/adapter"
|
||||
import { Session } from "@opencode/schema/session"
|
||||
import { testEffect } from "./lib/effect"
|
||||
import { PluginTestLayer } from "./plugin/fixture"
|
||||
|
||||
@@ -471,6 +472,34 @@ it.effect("normalizes Promise plugin API inputs through JSON", () =>
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("creates child sessions from a plugin at the parent's location", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* Plugin.Service
|
||||
const sessions = yield* Session.Service
|
||||
const created = yield* Deferred.make<{ parentID: SessionSchema.ID; childID: SessionSchema.ID }>()
|
||||
yield* plugins.activate([
|
||||
{
|
||||
id: "child-session",
|
||||
revision: "1",
|
||||
effect: (ctx) =>
|
||||
Effect.gen(function* () {
|
||||
const parent = yield* ctx.session
|
||||
.create({ title: "Parent", metadata: { source: "parent" } })
|
||||
.pipe(Effect.orDie)
|
||||
const child = yield* ctx.session.create({ parentID: parent.id, title: "Child" }).pipe(Effect.orDie)
|
||||
yield* Deferred.succeed(created, { parentID: parent.id, childID: child.id })
|
||||
}),
|
||||
},
|
||||
])
|
||||
yield* plugins.awaitActivation
|
||||
const ids = yield* Deferred.await(created)
|
||||
const parent = yield* sessions.get(ids.parentID)
|
||||
const child = yield* sessions.get(ids.childID)
|
||||
|
||||
expect(child).toMatchObject({ parentID: parent.id, location: parent.location, metadata: parent.metadata })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("reloading a plugin replaces its command implementation", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* Plugin.Service
|
||||
@@ -497,7 +526,7 @@ it.effect("reloading a plugin replaces its command implementation", () =>
|
||||
])
|
||||
const request = {
|
||||
name: "greet",
|
||||
invocation: { sessionID: Session.ID.make("ses_plugin"), prompt: { text: "" }, delivery: "steer" as const },
|
||||
invocation: { sessionID: SessionSchema.ID.make("ses_plugin"), prompt: { text: "" }, delivery: "steer" as const },
|
||||
}
|
||||
|
||||
yield* load("1", "before")
|
||||
|
||||
@@ -220,6 +220,7 @@ export const makeSessionGroup = <
|
||||
HttpApiEndpoint.post("session.create", "/api/session", {
|
||||
payload: Schema.Struct({
|
||||
id: Session.ID.pipe(Schema.optional),
|
||||
parentID: Session.ID.pipe(Schema.optional),
|
||||
title: Schema.String.pipe(Schema.optional),
|
||||
agent: Agent.ID.pipe(Schema.optional),
|
||||
model: Model.Ref.pipe(Schema.optional),
|
||||
@@ -228,11 +229,13 @@ export const makeSessionGroup = <
|
||||
permissions: Permission.Ruleset.pipe(Schema.optional),
|
||||
}),
|
||||
success: Schema.Struct({ data: PublicSessionInfo }),
|
||||
error: SessionNotFoundError,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.create",
|
||||
summary: "Create session",
|
||||
description: "Create a session at the requested location.",
|
||||
description:
|
||||
"Create a session at the requested location. A parentID creates a linked child session at its parent's location.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -133,9 +133,11 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||
model: ctx.payload.model,
|
||||
metadata: ctx.payload.metadata,
|
||||
permissions: ctx.payload.permissions,
|
||||
location: ctx.payload.location ?? { directory: AbsolutePath.make(process.cwd()) },
|
||||
...(ctx.payload.parentID === undefined
|
||||
? { location: ctx.payload.location ?? { directory: AbsolutePath.make(process.cwd()) } }
|
||||
: { parentID: ctx.payload.parentID }),
|
||||
})
|
||||
.pipe(Effect.orDie),
|
||||
.pipe(Effect.catchTag("Session.NotFoundError", missingSession)),
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { expect } from "bun:test"
|
||||
import { Session } from "@opencode/schema/session"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { it } from "../../core/test/lib/effect"
|
||||
import { ServerFetch } from "../src/fetch"
|
||||
|
||||
const SessionResponse = Schema.Struct({ data: Schema.toEncoded(Session.Info) })
|
||||
const SessionsResponse = Schema.Struct({ data: Schema.Array(Schema.toEncoded(Session.Info)) })
|
||||
|
||||
it.live("creates a child at its parent's location and lists it under the parent", () =>
|
||||
Effect.gen(function* () {
|
||||
const handler = yield* ServerFetch.make({
|
||||
app: { version: "test" },
|
||||
database: { path: ":memory:" },
|
||||
fs: { filewatcher: false },
|
||||
})
|
||||
const create = (body: unknown) =>
|
||||
Effect.promise(async () => {
|
||||
const response = await handler(
|
||||
new Request("http://opencode.local/api/session", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
)
|
||||
expect(response.status).toBe(200)
|
||||
return Schema.decodeUnknownSync(SessionResponse)(await response.json()).data
|
||||
})
|
||||
const parent = yield* create({ title: "Parent" })
|
||||
const child = yield* create({ parentID: parent.id, title: "Child", location: { directory: "/unused" } })
|
||||
const response = yield* Effect.promise(() =>
|
||||
handler(new Request(`http://opencode.local/api/session?parentID=${parent.id}`)),
|
||||
)
|
||||
expect(response.status).toBe(200)
|
||||
const children = Schema.decodeUnknownSync(SessionsResponse)(yield* Effect.promise(() => response.json()))
|
||||
|
||||
expect(child).toMatchObject({ parentID: parent.id, location: parent.location })
|
||||
expect(children.data.map((session) => session.id)).toEqual([child.id])
|
||||
}).pipe(Effect.scoped),
|
||||
)
|
||||
|
||||
it.live("returns not found when creating a child of a missing session", () =>
|
||||
Effect.gen(function* () {
|
||||
const handler = yield* ServerFetch.make({
|
||||
app: { version: "test" },
|
||||
database: { path: ":memory:" },
|
||||
fs: { filewatcher: false },
|
||||
})
|
||||
const parentID = Session.ID.create()
|
||||
const response = yield* Effect.promise(() =>
|
||||
handler(
|
||||
new Request("http://opencode.local/api/session", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ parentID, title: "Child" }),
|
||||
}),
|
||||
),
|
||||
)
|
||||
expect(response.status).toBe(404)
|
||||
expect(yield* Effect.promise(() => response.json())).toMatchObject({
|
||||
_tag: "SessionNotFoundError",
|
||||
sessionID: parentID,
|
||||
})
|
||||
}).pipe(Effect.scoped),
|
||||
)
|
||||
Reference in New Issue
Block a user