feat(plugin): support plugin-provided tools (#34619)
This commit is contained in:
@@ -680,6 +680,7 @@
|
|||||||
"version": "1.17.11",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "3.0.8",
|
"@ai-sdk/provider": "3.0.8",
|
||||||
|
"@opencode-ai/protocol": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
@@ -688,6 +689,7 @@
|
|||||||
"@opentui/core": "catalog:",
|
"@opentui/core": "catalog:",
|
||||||
"@opentui/keymap": "catalog:",
|
"@opentui/keymap": "catalog:",
|
||||||
"@opentui/solid": "catalog:",
|
"@opentui/solid": "catalog:",
|
||||||
|
"@tsconfig/bun": "catalog:",
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { NodeFileSystem } from "@effect/platform-node"
|
import { NodeFileSystem } from "@effect/platform-node"
|
||||||
import { compile, emitEffectImported, emitPromise, write } from "@opencode-ai/httpapi-codegen"
|
import { compile, emitEffectImported, emitEffectShape, emitPromise, write } from "@opencode-ai/httpapi-codegen"
|
||||||
import { ClientApi, effectOmitEndpoints, endpointNames, groupNames, promiseOmitEndpoints } from "../src/contract"
|
import {
|
||||||
|
ClientApi,
|
||||||
|
effectOmitEndpoints,
|
||||||
|
endpointNames,
|
||||||
|
groupNames,
|
||||||
|
promiseOmitEndpoints,
|
||||||
|
} from "@opencode-ai/protocol/client"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { fileURLToPath } from "url"
|
import { fileURLToPath } from "url"
|
||||||
|
|
||||||
@@ -25,7 +31,11 @@ await Effect.runPromise(
|
|||||||
emitEffectImported(effectContract, { module: "../contract", api: "ClientApi" }),
|
emitEffectImported(effectContract, { module: "../contract", api: "ClientApi" }),
|
||||||
fileURLToPath(new URL("../src/generated-effect", import.meta.url)),
|
fileURLToPath(new URL("../src/generated-effect", import.meta.url)),
|
||||||
),
|
),
|
||||||
|
write(
|
||||||
|
emitEffectShape(effectContract, { module: "@opencode-ai/protocol/client", api: "ClientApi" }),
|
||||||
|
fileURLToPath(new URL("../../plugin/src/v2/effect/generated", import.meta.url)),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
{ concurrency: 2, discard: true },
|
{ concurrency: 3, discard: true },
|
||||||
).pipe(Effect.provide(NodeFileSystem.layer)),
|
).pipe(Effect.provide(NodeFileSystem.layer)),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,57 +1,7 @@
|
|||||||
import { makeDefaultApi } from "@opencode-ai/protocol/api"
|
export {
|
||||||
import { InvalidRequestError, SessionNotFoundError } from "@opencode-ai/protocol/errors"
|
ClientApi,
|
||||||
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
effectOmitEndpoints,
|
||||||
|
endpointNames,
|
||||||
class LocationMiddleware extends HttpApiMiddleware.Service<LocationMiddleware>()(
|
groupNames,
|
||||||
"@opencode-ai/client/LocationMiddleware",
|
promiseOmitEndpoints,
|
||||||
) {}
|
} from "@opencode-ai/protocol/client"
|
||||||
|
|
||||||
class SessionLocationMiddleware extends HttpApiMiddleware.Service<SessionLocationMiddleware>()(
|
|
||||||
"@opencode-ai/client/SessionLocationMiddleware",
|
|
||||||
{ error: [InvalidRequestError, SessionNotFoundError] },
|
|
||||||
) {}
|
|
||||||
|
|
||||||
export const ClientApi = makeDefaultApi({
|
|
||||||
locationMiddleware: LocationMiddleware,
|
|
||||||
sessionLocationMiddleware: SessionLocationMiddleware,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const groupNames = {
|
|
||||||
"server.health": "health",
|
|
||||||
"server.location": "location",
|
|
||||||
"server.agent": "agent",
|
|
||||||
"server.session": "session",
|
|
||||||
"server.message": "message",
|
|
||||||
"server.model": "model",
|
|
||||||
"server.generate": "generate",
|
|
||||||
"server.provider": "provider",
|
|
||||||
"server.integration": "integration",
|
|
||||||
"server.credential": "credential",
|
|
||||||
"server.permission": "permission",
|
|
||||||
"server.fs": "file",
|
|
||||||
"server.command": "command",
|
|
||||||
"server.skill": "skill",
|
|
||||||
"server.event": "event",
|
|
||||||
"server.pty": "pty",
|
|
||||||
"server.shell": "shell",
|
|
||||||
"server.question": "question",
|
|
||||||
"server.reference": "reference",
|
|
||||||
"server.project": "project",
|
|
||||||
"server.projectCopy": "projectCopy",
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export const endpointNames = {
|
|
||||||
"session.messages": "list",
|
|
||||||
"integration.connect.key": "connectKey",
|
|
||||||
"integration.connect.oauth": "connectOauth",
|
|
||||||
"integration.attempt.status": "attemptStatus",
|
|
||||||
"integration.attempt.complete": "attemptComplete",
|
|
||||||
"integration.attempt.cancel": "attemptCancel",
|
|
||||||
"permission.request.list": "listRequests",
|
|
||||||
"permission.saved.list": "listSaved",
|
|
||||||
"permission.saved.remove": "removeSaved",
|
|
||||||
"question.request.list": "listRequests",
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export const promiseOmitEndpoints = new Set(["pty.connect", "pty.connectToken"])
|
|
||||||
export const effectOmitEndpoints = new Set(["fs.read", "pty.connect", "pty.connectToken"])
|
|
||||||
|
|||||||
@@ -254,9 +254,9 @@ const adaptGroup3 = (raw: RawClient["server.session"]) => ({
|
|||||||
skill: Endpoint3_9(raw),
|
skill: Endpoint3_9(raw),
|
||||||
compact: Endpoint3_10(raw),
|
compact: Endpoint3_10(raw),
|
||||||
wait: Endpoint3_11(raw),
|
wait: Endpoint3_11(raw),
|
||||||
stage: Endpoint3_12(raw),
|
revertStage: Endpoint3_12(raw),
|
||||||
clear: Endpoint3_13(raw),
|
revertClear: Endpoint3_13(raw),
|
||||||
commit: Endpoint3_14(raw),
|
revertCommit: Endpoint3_14(raw),
|
||||||
context: Endpoint3_15(raw),
|
context: Endpoint3_15(raw),
|
||||||
history: Endpoint3_16(raw),
|
history: Endpoint3_16(raw),
|
||||||
events: Endpoint3_17(raw),
|
events: Endpoint3_17(raw),
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ import type {
|
|||||||
SessionCompactOutput,
|
SessionCompactOutput,
|
||||||
SessionWaitInput,
|
SessionWaitInput,
|
||||||
SessionWaitOutput,
|
SessionWaitOutput,
|
||||||
SessionStageInput,
|
SessionRevertStageInput,
|
||||||
SessionStageOutput,
|
SessionRevertStageOutput,
|
||||||
SessionClearInput,
|
SessionRevertClearInput,
|
||||||
SessionClearOutput,
|
SessionRevertClearOutput,
|
||||||
SessionCommitInput,
|
SessionRevertCommitInput,
|
||||||
SessionCommitOutput,
|
SessionRevertCommitOutput,
|
||||||
SessionContextInput,
|
SessionContextInput,
|
||||||
SessionContextOutput,
|
SessionContextOutput,
|
||||||
SessionHistoryInput,
|
SessionHistoryInput,
|
||||||
@@ -465,8 +465,8 @@ export function make(options: ClientOptions) {
|
|||||||
},
|
},
|
||||||
requestOptions,
|
requestOptions,
|
||||||
),
|
),
|
||||||
stage: (input: SessionStageInput, requestOptions?: RequestOptions) =>
|
revertStage: (input: SessionRevertStageInput, requestOptions?: RequestOptions) =>
|
||||||
request<{ readonly data: SessionStageOutput }>(
|
request<{ readonly data: SessionRevertStageOutput }>(
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`,
|
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`,
|
||||||
@@ -477,8 +477,8 @@ export function make(options: ClientOptions) {
|
|||||||
},
|
},
|
||||||
requestOptions,
|
requestOptions,
|
||||||
).then((value) => value.data),
|
).then((value) => value.data),
|
||||||
clear: (input: SessionClearInput, requestOptions?: RequestOptions) =>
|
revertClear: (input: SessionRevertClearInput, requestOptions?: RequestOptions) =>
|
||||||
request<SessionClearOutput>(
|
request<SessionRevertClearOutput>(
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/clear`,
|
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/clear`,
|
||||||
@@ -488,8 +488,8 @@ export function make(options: ClientOptions) {
|
|||||||
},
|
},
|
||||||
requestOptions,
|
requestOptions,
|
||||||
),
|
),
|
||||||
commit: (input: SessionCommitInput, requestOptions?: RequestOptions) =>
|
revertCommit: (input: SessionRevertCommitInput, requestOptions?: RequestOptions) =>
|
||||||
request<SessionCommitOutput>(
|
request<SessionRevertCommitOutput>(
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`,
|
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`,
|
||||||
|
|||||||
@@ -577,13 +577,13 @@ export type SessionWaitInput = { readonly sessionID: { readonly sessionID: strin
|
|||||||
|
|
||||||
export type SessionWaitOutput = void
|
export type SessionWaitOutput = void
|
||||||
|
|
||||||
export type SessionStageInput = {
|
export type SessionRevertStageInput = {
|
||||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||||
readonly messageID: { readonly messageID: string; readonly files?: boolean | undefined }["messageID"]
|
readonly messageID: { readonly messageID: string; readonly files?: boolean | undefined }["messageID"]
|
||||||
readonly files?: { readonly messageID: string; readonly files?: boolean | undefined }["files"]
|
readonly files?: { readonly messageID: string; readonly files?: boolean | undefined }["files"]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionStageOutput = {
|
export type SessionRevertStageOutput = {
|
||||||
readonly data: {
|
readonly data: {
|
||||||
readonly messageID: string
|
readonly messageID: string
|
||||||
readonly partID?: string
|
readonly partID?: string
|
||||||
@@ -599,13 +599,13 @@ export type SessionStageOutput = {
|
|||||||
}
|
}
|
||||||
}["data"]
|
}["data"]
|
||||||
|
|
||||||
export type SessionClearInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
export type SessionRevertClearInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
||||||
|
|
||||||
export type SessionClearOutput = void
|
export type SessionRevertClearOutput = void
|
||||||
|
|
||||||
export type SessionCommitInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
export type SessionRevertCommitInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
||||||
|
|
||||||
export type SessionCommitOutput = void
|
export type SessionRevertCommitOutput = void
|
||||||
|
|
||||||
export type SessionContextInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
export type SessionContextInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export interface Interface extends State.Transformable<Draft> {
|
|||||||
readonly default: () => Effect.Effect<Info | undefined>
|
readonly default: () => Effect.Effect<Info | undefined>
|
||||||
readonly resolve: (id?: ID | string) => Effect.Effect<Info | undefined>
|
readonly resolve: (id?: ID | string) => Effect.Effect<Info | undefined>
|
||||||
readonly select: (id?: ID | string) => Effect.Effect<Selection>
|
readonly select: (id?: ID | string) => Effect.Effect<Selection>
|
||||||
readonly all: () => Effect.Effect<Info[]>
|
readonly list: () => Effect.Effect<Info[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Agent") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Agent") {}
|
||||||
@@ -104,7 +104,7 @@ export const layer = Layer.effect(
|
|||||||
const info = selectedDefault()
|
const info = selectedDefault()
|
||||||
return { id: info?.id ?? defaultID, info }
|
return { id: info?.id ?? defaultID, info }
|
||||||
}),
|
}),
|
||||||
all: Effect.fn("AgentV2.all")(function* () {
|
list: Effect.fn("AgentV2.list")(function* () {
|
||||||
return Array.fromIterable(state.get().agents.values())
|
return Array.fromIterable(state.get().agents.values())
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ type CheckReplacementErrors<SourceError, ReplacementError> = [Exclude<Replacemen
|
|||||||
|
|
||||||
export function replace<A, E, R, E2>(
|
export function replace<A, E, R, E2>(
|
||||||
source: Layer.Layer<A, E, R>,
|
source: Layer.Layer<A, E, R>,
|
||||||
replacement: Layer.Layer<NoInfer<A>, E2, never> & CheckReplacementErrors<E, NoInfer<E2>>,
|
replacement: Layer.Layer<NoInfer<A>, E2, NoInfer<R>> & CheckReplacementErrors<E, NoInfer<E2>>,
|
||||||
): Replacement {
|
): Replacement {
|
||||||
return { source, replacement }
|
return { source, replacement }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export * as PluginV2 from "./plugin"
|
|||||||
|
|
||||||
import { makeLocationNode } from "./effect/app-node"
|
import { makeLocationNode } from "./effect/app-node"
|
||||||
import { Context, Deferred, Effect, Exit, Layer, Scope } from "effect"
|
import { Context, Deferred, Effect, Exit, Layer, Scope } from "effect"
|
||||||
import type { Plugin as PluginRuntime } from "@opencode-ai/plugin/v2/effect"
|
import type { Plugin as PluginDefinition } from "@opencode-ai/plugin/v2/effect"
|
||||||
import { Plugin } from "@opencode-ai/schema/plugin"
|
import { Plugin } from "@opencode-ai/schema/plugin"
|
||||||
import { AgentV2 } from "./agent"
|
import { AgentV2 } from "./agent"
|
||||||
import { AISDK } from "./aisdk"
|
import { AISDK } from "./aisdk"
|
||||||
@@ -11,17 +11,20 @@ import { CommandV2 } from "./command"
|
|||||||
import { EventV2 } from "./event"
|
import { EventV2 } from "./event"
|
||||||
import { Integration } from "./integration"
|
import { Integration } from "./integration"
|
||||||
import { KeyedMutex } from "./effect/keyed-mutex"
|
import { KeyedMutex } from "./effect/keyed-mutex"
|
||||||
|
import { Location } from "./location"
|
||||||
import { PluginHost } from "./plugin/host"
|
import { PluginHost } from "./plugin/host"
|
||||||
|
import { PluginRuntime } from "./plugin/runtime"
|
||||||
import { Reference } from "./reference"
|
import { Reference } from "./reference"
|
||||||
import { SkillV2 } from "./skill"
|
import { SkillV2 } from "./skill"
|
||||||
import { State } from "./state"
|
import { State } from "./state"
|
||||||
|
import { ToolRegistry } from "./tool/registry"
|
||||||
|
|
||||||
export const ID = Plugin.ID
|
export const ID = Plugin.ID
|
||||||
export type ID = typeof ID.Type
|
export type ID = typeof ID.Type
|
||||||
export const Event = Plugin.Event
|
export const Event = Plugin.Event
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly add: (id: ID, effect: PluginRuntime["effect"]) => Effect.Effect<void>
|
readonly add: (id: ID, effect: PluginDefinition["effect"]) => Effect.Effect<void>
|
||||||
readonly remove: (id: ID) => Effect.Effect<void>
|
readonly remove: (id: ID) => Effect.Effect<void>
|
||||||
readonly wait: (id: ID) => Effect.Effect<void>
|
readonly wait: (id: ID) => Effect.Effect<void>
|
||||||
}
|
}
|
||||||
@@ -38,9 +41,9 @@ export const layer = Layer.effect(
|
|||||||
const loading = new Set<ID>()
|
const loading = new Set<ID>()
|
||||||
const waiters = new Map<ID, Set<Deferred.Deferred<void>>>()
|
const waiters = new Map<ID, Set<Deferred.Deferred<void>>>()
|
||||||
const failures = new Map<ID, Exit.Exit<void, never>>()
|
const failures = new Map<ID, Exit.Exit<void, never>>()
|
||||||
let host: Parameters<PluginRuntime["effect"]>[0]
|
let host: Parameters<PluginDefinition["effect"]>[0]
|
||||||
|
|
||||||
const add = Effect.fn("Plugin.add")(function* (id: ID, effect: PluginRuntime["effect"]) {
|
const add = Effect.fn("Plugin.add")(function* (id: ID, effect: PluginDefinition["effect"]) {
|
||||||
if (loading.has(id)) return yield* Effect.die(`Plugin load cycle detected for ${id}`)
|
if (loading.has(id)) return yield* Effect.die(`Plugin load cycle detected for ${id}`)
|
||||||
|
|
||||||
yield* locks.withLock(id)(
|
yield* locks.withLock(id)(
|
||||||
@@ -150,6 +153,8 @@ export const locationLayer = layer.pipe(
|
|||||||
Layer.provideMerge(Integration.locationLayer),
|
Layer.provideMerge(Integration.locationLayer),
|
||||||
Layer.provideMerge(Reference.locationLayer),
|
Layer.provideMerge(Reference.locationLayer),
|
||||||
Layer.provideMerge(SkillV2.locationLayer),
|
Layer.provideMerge(SkillV2.locationLayer),
|
||||||
|
Layer.provideMerge(ToolRegistry.defaultLayer),
|
||||||
|
Layer.provideMerge(PluginRuntime.layer),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const node = makeLocationNode({
|
export const node = makeLocationNode({
|
||||||
@@ -162,7 +167,10 @@ export const node = makeLocationNode({
|
|||||||
Catalog.node,
|
Catalog.node,
|
||||||
CommandV2.node,
|
CommandV2.node,
|
||||||
Integration.node,
|
Integration.node,
|
||||||
|
Location.node,
|
||||||
Reference.node,
|
Reference.node,
|
||||||
SkillV2.node,
|
SkillV2.node,
|
||||||
|
ToolRegistry.toolsNode,
|
||||||
|
PluginRuntime.node,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,12 +8,17 @@ import { Catalog } from "../catalog"
|
|||||||
import { CommandV2 } from "../command"
|
import { CommandV2 } from "../command"
|
||||||
import { Credential } from "../credential"
|
import { Credential } from "../credential"
|
||||||
import { Integration } from "../integration"
|
import { Integration } from "../integration"
|
||||||
|
import { Location } from "../location"
|
||||||
import { ModelV2 } from "../model"
|
import { ModelV2 } from "../model"
|
||||||
import { PluginV2 } from "../plugin"
|
import { PluginV2 } from "../plugin"
|
||||||
|
import { PluginRuntime } from "./runtime"
|
||||||
import { ProviderV2 } from "../provider"
|
import { ProviderV2 } from "../provider"
|
||||||
import { Reference } from "../reference"
|
import { Reference } from "../reference"
|
||||||
import type { DeepMutable } from "../schema"
|
import { AbsolutePath, type DeepMutable } from "../schema"
|
||||||
import { SkillV2 } from "../skill"
|
import { SkillV2 } from "../skill"
|
||||||
|
import { Tool } from "../tool/tool"
|
||||||
|
import { Tools } from "../tool/tools"
|
||||||
|
import { WorkspaceV2 } from "../workspace"
|
||||||
|
|
||||||
const mutable = <T>(value: T) => value as DeepMutable<T>
|
const mutable = <T>(value: T) => value as DeepMutable<T>
|
||||||
|
|
||||||
@@ -23,12 +28,38 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
|||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
const commands = yield* CommandV2.Service
|
const commands = yield* CommandV2.Service
|
||||||
const integration = yield* Integration.Service
|
const integration = yield* Integration.Service
|
||||||
|
const location = yield* Location.Service
|
||||||
const reference = yield* Reference.Service
|
const reference = yield* Reference.Service
|
||||||
const skill = yield* SkillV2.Service
|
const skill = yield* SkillV2.Service
|
||||||
|
const tools = yield* Tools.Service
|
||||||
|
const runtime = yield* PluginRuntime.Service
|
||||||
|
const locationInfo = () =>
|
||||||
|
new Location.Info({
|
||||||
|
directory: location.directory,
|
||||||
|
workspaceID: location.workspaceID,
|
||||||
|
project: location.project,
|
||||||
|
})
|
||||||
|
const locationRef = (input?: Parameters<Interface["agent"]["list"]>[0]) =>
|
||||||
|
input?.location === undefined
|
||||||
|
? undefined
|
||||||
|
: Location.Ref.make({
|
||||||
|
directory: AbsolutePath.make(input.location.directory ?? location.directory),
|
||||||
|
workspaceID:
|
||||||
|
input.location.workspace === undefined
|
||||||
|
? location.workspaceID
|
||||||
|
: WorkspaceV2.ID.make(input.location.workspace),
|
||||||
|
})
|
||||||
|
const isCurrentLocation = (ref: Location.Ref) =>
|
||||||
|
ref.directory === location.directory && ref.workspaceID === location.workspaceID
|
||||||
|
|
||||||
return {
|
return {
|
||||||
options: {},
|
options: {},
|
||||||
agent: {
|
agent: {
|
||||||
|
list: (input) => {
|
||||||
|
const ref = locationRef(input)
|
||||||
|
if (ref && !isCurrentLocation(ref)) return runtime.location.agent.list(ref)
|
||||||
|
return agents.list().pipe(Effect.map((data) => ({ location: locationInfo(), data })))
|
||||||
|
},
|
||||||
reload: agents.reload,
|
reload: agents.reload,
|
||||||
transform: (callback) =>
|
transform: (callback) =>
|
||||||
agents.transform((draft) =>
|
agents.transform((draft) =>
|
||||||
@@ -215,5 +246,21 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
tool: {
|
||||||
|
register: (input) => tools.register(input as Readonly<Record<string, Tool.AnyTool>>),
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
create: (input) =>
|
||||||
|
runtime.session.create({
|
||||||
|
id: input?.id,
|
||||||
|
agent: input?.agent,
|
||||||
|
model: input?.model,
|
||||||
|
location:
|
||||||
|
input?.location ?? Location.Ref.make({ directory: location.directory, workspaceID: location.workspaceID }),
|
||||||
|
}),
|
||||||
|
get: (input) => runtime.session.get(input.sessionID),
|
||||||
|
prompt: runtime.session.prompt,
|
||||||
|
interrupt: (input) => runtime.session.interrupt(input.sessionID),
|
||||||
|
},
|
||||||
} satisfies Interface
|
} satisfies Interface
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -20,12 +20,18 @@ import { FSUtil } from "../fs-util"
|
|||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import { Integration } from "../integration"
|
import { Integration } from "../integration"
|
||||||
import { Location } from "../location"
|
import { Location } from "../location"
|
||||||
|
import { LocationMutation } from "../location-mutation"
|
||||||
import { ModelsDev } from "../models-dev"
|
import { ModelsDev } from "../models-dev"
|
||||||
import { Npm } from "../npm"
|
import { Npm } from "../npm"
|
||||||
import { PluginV2 } from "../plugin"
|
import { PluginV2 } from "../plugin"
|
||||||
|
import { PluginRuntime } from "../plugin/runtime"
|
||||||
|
import { PermissionV2 } from "../permission"
|
||||||
import { Reference } from "../reference"
|
import { Reference } from "../reference"
|
||||||
|
import { Shell } from "../shell"
|
||||||
import { SkillV2 } from "../skill"
|
import { SkillV2 } from "../skill"
|
||||||
import { State } from "../state"
|
import { State } from "../state"
|
||||||
|
import { ToolRegistry } from "../tool/registry"
|
||||||
|
import { Tools } from "../tool/tools"
|
||||||
import { FetchHttpClient, HttpClient } from "effect/unstable/http"
|
import { FetchHttpClient, HttpClient } from "effect/unstable/http"
|
||||||
import { AgentPlugin } from "./agent"
|
import { AgentPlugin } from "./agent"
|
||||||
import { CommandPlugin } from "./command"
|
import { CommandPlugin } from "./command"
|
||||||
@@ -34,6 +40,8 @@ import { ProviderPlugins } from "./provider"
|
|||||||
import { SdkPlugins } from "./sdk"
|
import { SdkPlugins } from "./sdk"
|
||||||
import { SkillPlugin } from "./skill"
|
import { SkillPlugin } from "./skill"
|
||||||
import { VariantPlugin } from "./variant"
|
import { VariantPlugin } from "./variant"
|
||||||
|
import { ShellTool } from "../tool/shell"
|
||||||
|
import { SubagentTool } from "../tool/subagent"
|
||||||
|
|
||||||
export type Requirements =
|
export type Requirements =
|
||||||
| AgentV2.Service
|
| AgentV2.Service
|
||||||
@@ -47,10 +55,15 @@ export type Requirements =
|
|||||||
| HttpClient.HttpClient
|
| HttpClient.HttpClient
|
||||||
| Integration.Service
|
| Integration.Service
|
||||||
| Location.Service
|
| Location.Service
|
||||||
|
| LocationMutation.Service
|
||||||
| ModelsDev.Service
|
| ModelsDev.Service
|
||||||
| Npm.Service
|
| Npm.Service
|
||||||
|
| PermissionV2.Service
|
||||||
|
| PluginRuntime.Service
|
||||||
| Reference.Service
|
| Reference.Service
|
||||||
|
| Shell.Service
|
||||||
| SkillV2.Service
|
| SkillV2.Service
|
||||||
|
| Tools.Service
|
||||||
|
|
||||||
export interface Plugin<R = never> {
|
export interface Plugin<R = never> {
|
||||||
readonly id: string
|
readonly id: string
|
||||||
@@ -78,8 +91,13 @@ const layer = Layer.effectDiscard(
|
|||||||
const filesystem = yield* FileSystem.Service
|
const filesystem = yield* FileSystem.Service
|
||||||
const global = yield* Global.Service
|
const global = yield* Global.Service
|
||||||
const http = yield* HttpClient.HttpClient
|
const http = yield* HttpClient.HttpClient
|
||||||
|
const mutation = yield* LocationMutation.Service
|
||||||
|
const permission = yield* PermissionV2.Service
|
||||||
const skill = yield* SkillV2.Service
|
const skill = yield* SkillV2.Service
|
||||||
const reference = yield* Reference.Service
|
const reference = yield* Reference.Service
|
||||||
|
const shell = yield* Shell.Service
|
||||||
|
const tools = yield* Tools.Service
|
||||||
|
const runtime = yield* PluginRuntime.Service
|
||||||
const add = <R>(input: Plugin<R>) => {
|
const add = <R>(input: Plugin<R>) => {
|
||||||
const loaded = {
|
const loaded = {
|
||||||
id: input.id,
|
id: input.id,
|
||||||
@@ -100,8 +118,13 @@ const layer = Layer.effectDiscard(
|
|||||||
Effect.provideService(FileSystem.Service, filesystem),
|
Effect.provideService(FileSystem.Service, filesystem),
|
||||||
Effect.provideService(Global.Service, global),
|
Effect.provideService(Global.Service, global),
|
||||||
Effect.provideService(HttpClient.HttpClient, http),
|
Effect.provideService(HttpClient.HttpClient, http),
|
||||||
|
Effect.provideService(LocationMutation.Service, mutation),
|
||||||
|
Effect.provideService(PermissionV2.Service, permission),
|
||||||
Effect.provideService(SkillV2.Service, skill),
|
Effect.provideService(SkillV2.Service, skill),
|
||||||
Effect.provideService(Reference.Service, reference),
|
Effect.provideService(Reference.Service, reference),
|
||||||
|
Effect.provideService(Shell.Service, shell),
|
||||||
|
Effect.provideService(Tools.Service, tools),
|
||||||
|
Effect.provideService(PluginRuntime.Service, runtime),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
return plugin.add(PluginV2.ID.make(loaded.id), loaded.effect)
|
return plugin.add(PluginV2.ID.make(loaded.id), loaded.effect)
|
||||||
@@ -115,6 +138,8 @@ const layer = Layer.effectDiscard(
|
|||||||
yield* add(SkillPlugin.Plugin)
|
yield* add(SkillPlugin.Plugin)
|
||||||
yield* add(ModelsDevPlugin)
|
yield* add(ModelsDevPlugin)
|
||||||
yield* add(ConfigExternalPlugin.Plugin)
|
yield* add(ConfigExternalPlugin.Plugin)
|
||||||
|
yield* add(ShellTool.Plugin)
|
||||||
|
yield* add(SubagentTool.Plugin)
|
||||||
yield* add(ConfigAgentPlugin.Plugin)
|
yield* add(ConfigAgentPlugin.Plugin)
|
||||||
yield* add(ConfigCommandPlugin.Plugin)
|
yield* add(ConfigCommandPlugin.Plugin)
|
||||||
yield* add(ConfigSkillPlugin.Plugin)
|
yield* add(ConfigSkillPlugin.Plugin)
|
||||||
@@ -146,6 +171,7 @@ export const node = makeLocationNode({
|
|||||||
AgentV2.node,
|
AgentV2.node,
|
||||||
Config.node,
|
Config.node,
|
||||||
Location.node,
|
Location.node,
|
||||||
|
LocationMutation.node,
|
||||||
ModelsDev.node,
|
ModelsDev.node,
|
||||||
Npm.node,
|
Npm.node,
|
||||||
EventV2.node,
|
EventV2.node,
|
||||||
@@ -153,8 +179,12 @@ export const node = makeLocationNode({
|
|||||||
FileSystem.node,
|
FileSystem.node,
|
||||||
Global.node,
|
Global.node,
|
||||||
httpClient,
|
httpClient,
|
||||||
|
PermissionV2.node,
|
||||||
SkillV2.node,
|
SkillV2.node,
|
||||||
Reference.node,
|
Reference.node,
|
||||||
|
Shell.node,
|
||||||
|
ToolRegistry.toolsNode,
|
||||||
|
PluginRuntime.node,
|
||||||
SdkPlugins.node,
|
SdkPlugins.node,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
export * as PluginRuntime from "./runtime"
|
||||||
|
|
||||||
|
import { Context, Effect, Layer } from "effect"
|
||||||
|
import { AgentV2 } from "../agent"
|
||||||
|
import { makeGlobalNode } from "../effect/app-node"
|
||||||
|
import { Job } from "../job"
|
||||||
|
import { Location } from "../location"
|
||||||
|
import { LocationServiceMap } from "../location-service-map"
|
||||||
|
import { SessionV2 } from "../session"
|
||||||
|
|
||||||
|
export interface Interface {
|
||||||
|
readonly session: Pick<
|
||||||
|
SessionV2.Interface,
|
||||||
|
"get" | "create" | "messages" | "prompt" | "resume" | "interrupt" | "synthetic"
|
||||||
|
>
|
||||||
|
readonly job: Pick<Job.Interface, "start" | "wait" | "block" | "background" | "cancel">
|
||||||
|
readonly location: {
|
||||||
|
readonly agent: {
|
||||||
|
readonly list: (
|
||||||
|
ref: Location.Ref,
|
||||||
|
) => Effect.Effect<{ readonly location: Location.Info; readonly data: AgentV2.Info[] }>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Service extends Context.Service<Service, Interface>()("@opencode/PluginRuntime") {}
|
||||||
|
|
||||||
|
export interface Cell {
|
||||||
|
runtime?: Interface
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeCell = (): Cell => ({})
|
||||||
|
|
||||||
|
const unavailable = <A, E, R>() => Effect.die("Plugin runtime is unavailable") as Effect.Effect<A, E, R>
|
||||||
|
const require = <A, E, R>(cell: Cell, f: (runtime: Interface) => Effect.Effect<A, E, R>) =>
|
||||||
|
Effect.suspend(() => {
|
||||||
|
const runtime = cell.runtime
|
||||||
|
if (runtime === undefined) return unavailable<A, E, R>()
|
||||||
|
return f(runtime)
|
||||||
|
})
|
||||||
|
|
||||||
|
const defaultCell = makeCell()
|
||||||
|
|
||||||
|
export const layerWithCell = (cell: Cell) =>
|
||||||
|
Layer.succeed(
|
||||||
|
Service,
|
||||||
|
Service.of({
|
||||||
|
session: {
|
||||||
|
get: (sessionID) => require(cell, (runtime) => runtime.session.get(sessionID)),
|
||||||
|
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)),
|
||||||
|
resume: (sessionID) => require(cell, (runtime) => runtime.session.resume(sessionID)),
|
||||||
|
interrupt: (sessionID) => require(cell, (runtime) => runtime.session.interrupt(sessionID)),
|
||||||
|
synthetic: (input) => require(cell, (runtime) => runtime.session.synthetic(input)),
|
||||||
|
},
|
||||||
|
job: {
|
||||||
|
start: (input) => require(cell, (runtime) => runtime.job.start(input)),
|
||||||
|
wait: (input) => require(cell, (runtime) => runtime.job.wait(input)),
|
||||||
|
block: (input) => require(cell, (runtime) => runtime.job.block(input)),
|
||||||
|
background: (id) => require(cell, (runtime) => runtime.job.background(id)),
|
||||||
|
cancel: (id) => require(cell, (runtime) => runtime.job.cancel(id)),
|
||||||
|
},
|
||||||
|
location: {
|
||||||
|
agent: {
|
||||||
|
list: (ref) => require(cell, (runtime) => runtime.location.agent.list(ref)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const providerLayerWithCell = (cell: Cell) =>
|
||||||
|
Layer.effectDiscard(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const sessions = yield* SessionV2.Service
|
||||||
|
const jobs = yield* Job.Service
|
||||||
|
const locations = yield* LocationServiceMap.Service
|
||||||
|
const runtime = {
|
||||||
|
session: sessions,
|
||||||
|
job: jobs,
|
||||||
|
location: {
|
||||||
|
agent: {
|
||||||
|
list: (ref) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const location = yield* Location.Service
|
||||||
|
const agents = yield* AgentV2.Service
|
||||||
|
return {
|
||||||
|
location: new Location.Info({
|
||||||
|
directory: location.directory,
|
||||||
|
workspaceID: location.workspaceID,
|
||||||
|
project: location.project,
|
||||||
|
}),
|
||||||
|
data: yield* agents.list(),
|
||||||
|
}
|
||||||
|
}).pipe(Effect.provide(locations.get(ref))),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Interface
|
||||||
|
cell.runtime = runtime
|
||||||
|
yield* Effect.addFinalizer(() =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
if (cell.runtime === runtime) cell.runtime = undefined
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const layer = layerWithCell(defaultCell)
|
||||||
|
export const providerLayer = providerLayerWithCell(defaultCell)
|
||||||
|
|
||||||
|
export const node = makeGlobalNode({ service: Service, layer, deps: [] })
|
||||||
|
|
||||||
|
export const providerNode = makeGlobalNode({
|
||||||
|
name: "plugin-runtime-provider",
|
||||||
|
layer: providerLayer,
|
||||||
|
deps: [node, SessionV2.node, Job.node, LocationServiceMap.node],
|
||||||
|
})
|
||||||
@@ -4,6 +4,14 @@ import type { Plugin } from "@opencode-ai/plugin/v2/effect"
|
|||||||
import { Context, Effect, Layer } from "effect"
|
import { Context, Effect, Layer } from "effect"
|
||||||
import { makeGlobalNode } from "../effect/app-node"
|
import { makeGlobalNode } from "../effect/app-node"
|
||||||
|
|
||||||
|
export interface Store {
|
||||||
|
readonly plugins: Map<string, Plugin>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeStore = (): Store => ({ plugins: new Map() })
|
||||||
|
|
||||||
|
const defaultStore = makeStore()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds the plugins an embedder (the `@opencode-ai/sdk-next` host) contributes,
|
* Holds the plugins an embedder (the `@opencode-ai/sdk-next` host) contributes,
|
||||||
* so `PluginInternal` can add them on every Location boot through the ordinary
|
* so `PluginInternal` can add them on every Location boot through the ordinary
|
||||||
@@ -12,9 +20,10 @@ import { makeGlobalNode } from "../effect/app-node"
|
|||||||
* applies to Locations booted afterward, matching config-plugin timing;
|
* applies to Locations booted afterward, matching config-plugin timing;
|
||||||
* embedders register at startup before creating Sessions.
|
* embedders register at startup before creating Sessions.
|
||||||
*
|
*
|
||||||
* State lives in this global-node service (like `ApplicationTools`) rather than
|
* The store is shared explicitly between the SDK construction graph and the
|
||||||
* module scope, so the list belongs to one embedded instance and is disposed
|
* embedded route graph because `LocationServiceMap` builds Location layers lazily
|
||||||
* with it instead of leaking across `OpenCode.create` calls.
|
* in a nested graph. Each embedded SDK creates its own store, so instances do not
|
||||||
|
* see each other's contributions.
|
||||||
*/
|
*/
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly register: (plugin: Plugin) => Effect.Effect<void>
|
readonly register: (plugin: Plugin) => Effect.Effect<void>
|
||||||
@@ -23,15 +32,25 @@ export interface Interface {
|
|||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SdkPlugins") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/SdkPlugins") {}
|
||||||
|
|
||||||
export const layer = Layer.effect(
|
export const layerWithStore = (store: Store) =>
|
||||||
|
Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* Effect.addFinalizer(() =>
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
const plugins: Plugin[] = []
|
store.plugins.clear()
|
||||||
|
}),
|
||||||
|
)
|
||||||
return Service.of({
|
return Service.of({
|
||||||
register: (plugin) => Effect.sync(() => void plugins.push(plugin)),
|
register: (plugin) =>
|
||||||
all: () => plugins,
|
Effect.sync(() => {
|
||||||
|
store.plugins.set(plugin.id, plugin)
|
||||||
|
}),
|
||||||
|
all: () => [...store.plugins.values()],
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const layer = layerWithStore(defaultStore)
|
||||||
|
|
||||||
export const node = makeGlobalNode({ service: Service, layer, deps: [] })
|
export const node = makeGlobalNode({ service: Service, layer, deps: [] })
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ This folder owns Core's one local tool representation, process and Location regi
|
|||||||
|
|
||||||
## Representations
|
## Representations
|
||||||
|
|
||||||
- `tool.ts` defines the opaque canonical `Tool.make({ description, input, output, execute, toModelOutput })` value. Application tools and shipped built-ins use the same type.
|
- `tool.ts` defines the opaque canonical `Tool.make({ description, input, output, execute, toModelOutput })` value. Shipped built-ins and plugin tools use the same type.
|
||||||
- `application-tools.ts` stores process-scoped application registrations.
|
|
||||||
- `tools.ts` exposes the registration-only `Tools.Service` view used by Location producers.
|
- `tools.ts` exposes the registration-only `Tools.Service` view used by Location producers.
|
||||||
- `registry.ts` stores only canonical tools, overlays Location registrations over application registrations, derives definitions, invokes tools, and applies generic output bounding.
|
- `registry.ts` stores only canonical Location registrations, derives definitions, invokes tools, and applies generic output bounding.
|
||||||
|
|
||||||
Do not add a second executable entry type, registry-owned executor, authorization callback, output-path callback, or legacy normalization path.
|
Do not add a second executable entry type, registry-owned executor, authorization callback, output-path callback, or legacy normalization path.
|
||||||
|
|
||||||
@@ -29,16 +28,15 @@ Leaves own resolution, permission, and side-effect ordering. Translate only expe
|
|||||||
|
|
||||||
## Registration
|
## Registration
|
||||||
|
|
||||||
Built-ins register through `Tools.Service.register({ [name]: tool })`. Application tools register through `ApplicationTools.Service.register(...)`, exposed publicly as `opencode.tools.register(...)`.
|
Built-ins and plugin tools register through `Tools.Service.register({ [name]: tool })`.
|
||||||
|
|
||||||
Both are scoped:
|
Registrations are scoped:
|
||||||
|
|
||||||
- The latest active same-placement registration wins.
|
- The latest active same-placement registration wins.
|
||||||
- Closing any registration removes only that registration and reveals the next active one.
|
- Closing any registration removes only that registration and reveals the next active one.
|
||||||
- Location registrations take precedence over application registrations.
|
|
||||||
- An invocation captures the effective tool once settlement starts.
|
- An invocation captures the effective tool once settlement starts.
|
||||||
|
|
||||||
`ApplicationTools.Service` is process-scoped and shared by all Locations. `ToolRegistry.Service` is Location-scoped. Do not make the registry process-global or construct a separate application-tool service for each Location.
|
`ToolRegistry.Service` is Location-scoped. Do not make the registry process-global or construct a separate application-tool service for each Location.
|
||||||
|
|
||||||
## Permissions
|
## Permissions
|
||||||
|
|
||||||
@@ -54,6 +52,5 @@ Producer capture limits are separate. For example, Bash keeps `AppProcess.maxOut
|
|||||||
|
|
||||||
## Current Gaps
|
## Current Gaps
|
||||||
|
|
||||||
- Plugin boot has not been redesigned to register canonical tools through `Tools.Service`; do not redesign it as part of leaf migrations.
|
|
||||||
- MCP and future Session-scoped registrations still need an explicit canonical registration design.
|
- MCP and future Session-scoped registrations still need an explicit canonical registration design.
|
||||||
- The public Session result shape currently exposes managed `outputPaths`; full storage encapsulation requires a future opaque managed-output reference design.
|
- The public Session result shape currently exposes managed `outputPaths`; full storage encapsulation requires a future opaque managed-output reference design.
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
export * as ApplicationTools from "./application-tools"
|
|
||||||
|
|
||||||
import { Context, Effect, Layer, Scope } from "effect"
|
|
||||||
import { State } from "../state"
|
|
||||||
import { Tool } from "./tool"
|
|
||||||
import { makeGlobalNode } from "../effect/app-node"
|
|
||||||
|
|
||||||
type Data = {
|
|
||||||
readonly entries: Map<string, Entry>
|
|
||||||
}
|
|
||||||
|
|
||||||
type Draft = {
|
|
||||||
readonly set: (name: string, entry: Entry) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Entry {
|
|
||||||
readonly identity: object
|
|
||||||
readonly tool: Tool.AnyTool
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Interface {
|
|
||||||
readonly register: (
|
|
||||||
tools: Readonly<Record<string, Tool.AnyTool>>,
|
|
||||||
) => Effect.Effect<void, Tool.RegistrationError, Scope.Scope>
|
|
||||||
readonly entries: () => ReadonlyMap<string, Entry>
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ApplicationTools") {}
|
|
||||||
|
|
||||||
export const layer = Layer.effect(
|
|
||||||
Service,
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const state = State.create<Data, Draft>({
|
|
||||||
initial: () => ({ entries: new Map() }),
|
|
||||||
draft: (draft) => ({
|
|
||||||
set: (name, tool) => {
|
|
||||||
draft.entries.set(name, tool)
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
return Service.of({
|
|
||||||
register: Effect.fn("ApplicationTools.register")(function* (tools) {
|
|
||||||
const entries = Tool.registrationEntries(tools)
|
|
||||||
if (entries.length === 0) return
|
|
||||||
const registrations = entries.map(([name, tool]) => [name, { identity: {}, tool }] as const)
|
|
||||||
yield* state.transform((draft) => {
|
|
||||||
for (const [name, entry] of registrations) draft.set(name, entry)
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
entries: () => state.get().entries,
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const node = makeGlobalNode({ service: Service, layer, deps: [] })
|
|
||||||
@@ -8,7 +8,6 @@ import { SessionMessage } from "../session/message"
|
|||||||
import { SessionSchema } from "../session/schema"
|
import { SessionSchema } from "../session/schema"
|
||||||
import { ToolOutputStore } from "../tool-output-store"
|
import { ToolOutputStore } from "../tool-output-store"
|
||||||
import { Wildcard } from "../util/wildcard"
|
import { Wildcard } from "../util/wildcard"
|
||||||
import { ApplicationTools } from "./application-tools"
|
|
||||||
import { definition, permission, registrationEntries, settle, type AnyTool, type RegistrationError } from "./tool"
|
import { definition, permission, registrationEntries, settle, type AnyTool, type RegistrationError } from "./tool"
|
||||||
import { Tools } from "./tools"
|
import { Tools } from "./tools"
|
||||||
import { makeLocationNode } from "../effect/app-node"
|
import { makeLocationNode } from "../effect/app-node"
|
||||||
@@ -47,14 +46,12 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
|
|||||||
const registryLayer = Layer.effect(
|
const registryLayer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const applications = yield* ApplicationTools.Service
|
|
||||||
const resources = yield* ToolOutputStore.Service
|
const resources = yield* ToolOutputStore.Service
|
||||||
type Registration = { readonly identity: object; readonly tool: AnyTool }
|
type Registration = { readonly identity: object; readonly tool: AnyTool }
|
||||||
const local = new Map<string, Array<{ readonly token: object; readonly registration: Registration }>>()
|
const local = new Map<string, Array<{ readonly token: object; readonly registration: Registration }>>()
|
||||||
|
|
||||||
const settleWith = Effect.fn("ToolRegistry.settle")(function* (input: ExecuteInput, advertised?: object) {
|
const settleWith = Effect.fn("ToolRegistry.settle")(function* (input: ExecuteInput, advertised?: object) {
|
||||||
const registration =
|
const registration = local.get(input.call.name)?.at(-1)?.registration
|
||||||
local.get(input.call.name)?.at(-1)?.registration ?? applications.entries().get(input.call.name)
|
|
||||||
if (!registration)
|
if (!registration)
|
||||||
return {
|
return {
|
||||||
result: {
|
result: {
|
||||||
@@ -108,7 +105,7 @@ const registryLayer = Layer.effect(
|
|||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
materialize: Effect.fn("ToolRegistry.materialize")(function* (input) {
|
materialize: Effect.fn("ToolRegistry.materialize")(function* (input) {
|
||||||
const registrations = new Map(applications.entries())
|
const registrations = new Map<string, Registration>()
|
||||||
for (const [name, entries] of local) {
|
for (const [name, entries] of local) {
|
||||||
const registration = entries.at(-1)?.registration
|
const registration = entries.at(-1)?.registration
|
||||||
if (registration) registrations.set(name, registration)
|
if (registration) registrations.set(name, registration)
|
||||||
@@ -143,19 +140,16 @@ function whollyDisabled(action: string, rules: PermissionV2.Ruleset) {
|
|||||||
return rule?.resource === "*" && rule.effect === "deny"
|
return rule?.resource === "*" && rule.effect === "deny"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(
|
export const defaultLayer = layer.pipe(Layer.provide(ToolOutputStore.defaultLayer))
|
||||||
Layer.provide(ApplicationTools.layer),
|
|
||||||
Layer.provide(ToolOutputStore.defaultLayer),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const node = makeLocationNode({
|
export const node = makeLocationNode({
|
||||||
service: Service,
|
service: Service,
|
||||||
layer,
|
layer,
|
||||||
deps: [ApplicationTools.node, ToolOutputStore.node],
|
deps: [ToolOutputStore.node],
|
||||||
})
|
})
|
||||||
|
|
||||||
export const toolsNode = makeLocationNode({
|
export const toolsNode = makeLocationNode({
|
||||||
service: Tools.Service,
|
service: Tools.Service,
|
||||||
layer,
|
layer,
|
||||||
deps: [ApplicationTools.node, ToolOutputStore.node],
|
deps: [ToolOutputStore.node],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,19 +2,16 @@ export * as ShellTool from "./shell"
|
|||||||
|
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { ToolFailure } from "@opencode-ai/llm"
|
import { ToolFailure } from "@opencode-ai/llm"
|
||||||
import { Effect, Layer, Schema, Scope } from "effect"
|
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||||
|
import { Effect, Schema, Scope } from "effect"
|
||||||
import { FSUtil } from "../fs-util"
|
import { FSUtil } from "../fs-util"
|
||||||
import { Job } from "../job"
|
|
||||||
import { LocationMutation } from "../location-mutation"
|
import { LocationMutation } from "../location-mutation"
|
||||||
import { LocationServiceMap } from "../location-service-map"
|
|
||||||
import { PermissionV2 } from "../permission"
|
import { PermissionV2 } from "../permission"
|
||||||
|
import { PluginRuntime } from "../plugin/runtime"
|
||||||
import { PositiveInt } from "../schema"
|
import { PositiveInt } from "../schema"
|
||||||
import { SessionV2 } from "../session"
|
|
||||||
import { SessionSchema } from "../session/schema"
|
import { SessionSchema } from "../session/schema"
|
||||||
import { Shell } from "../shell"
|
import { Shell } from "../shell"
|
||||||
import { Tool, type Content } from "./tool"
|
import { Tool, type Content } from "./tool"
|
||||||
import { ApplicationTools } from "./application-tools"
|
|
||||||
import { makeGlobalNode } from "../effect/app-node"
|
|
||||||
|
|
||||||
export const name = "shell"
|
export const name = "shell"
|
||||||
export const DEFAULT_TIMEOUT_MS = 2 * 60 * 1_000
|
export const DEFAULT_TIMEOUT_MS = 2 * 60 * 1_000
|
||||||
@@ -94,21 +91,22 @@ const externalCommandDirectories = (command: string, cwd: string) => {
|
|||||||
return [...directories]
|
return [...directories]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const layer = Layer.effectDiscard(
|
export const Plugin = {
|
||||||
Effect.gen(function* () {
|
id: "core-shell-tool",
|
||||||
const tools = yield* ApplicationTools.Service
|
effect: Effect.fn("ShellTool.Plugin")(function* (ctx: PluginContext) {
|
||||||
const sessions = yield* SessionV2.Service
|
const runtime = yield* PluginRuntime.Service
|
||||||
const jobs = yield* Job.Service
|
|
||||||
const locations = yield* LocationServiceMap.Service
|
|
||||||
const scope = yield* Scope.Scope
|
const scope = yield* Scope.Scope
|
||||||
const fsUtil = yield* FSUtil.Service
|
const fsUtil = yield* FSUtil.Service
|
||||||
|
const mutation = yield* LocationMutation.Service
|
||||||
|
const shell = yield* Shell.Service
|
||||||
|
const permission = yield* PermissionV2.Service
|
||||||
|
|
||||||
const notifyWhenDone = Effect.fn("ShellTool.notifyWhenDone")(function* (
|
const notifyWhenDone = Effect.fn("ShellTool.notifyWhenDone")(function* (
|
||||||
sessionID: SessionSchema.ID,
|
sessionID: SessionSchema.ID,
|
||||||
callID: string,
|
callID: string,
|
||||||
command: string,
|
command: string,
|
||||||
) {
|
) {
|
||||||
yield* jobs.wait({ id: callID }).pipe(
|
yield* runtime.job.wait({ id: callID }).pipe(
|
||||||
Effect.flatMap((result) => {
|
Effect.flatMap((result) => {
|
||||||
const state =
|
const state =
|
||||||
result.info?.status === "completed"
|
result.info?.status === "completed"
|
||||||
@@ -125,7 +123,7 @@ export const layer = Layer.effectDiscard(
|
|||||||
: state === "error"
|
: state === "error"
|
||||||
? (result.info!.error ?? "Command failed")
|
? (result.info!.error ?? "Command failed")
|
||||||
: "Command cancelled"
|
: "Command cancelled"
|
||||||
return sessions.synthetic({
|
return runtime.session.synthetic({
|
||||||
sessionID,
|
sessionID,
|
||||||
text: `<shell id="${callID}" state="${state}" command="${command}">\n${text}\n</shell>`,
|
text: `<shell id="${callID}" state="${state}" command="${command}">\n${text}\n</shell>`,
|
||||||
})
|
})
|
||||||
@@ -134,7 +132,7 @@ export const layer = Layer.effectDiscard(
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
yield* tools
|
yield* ctx.tool
|
||||||
.register({
|
.register({
|
||||||
[name]: Tool.make({
|
[name]: Tool.make({
|
||||||
description: `Execute one shell command string with the host user's filesystem, process, and network authority. The active Location is the default working directory. Relative workdir values resolve from that Location. External workdir values require external_directory approval; best-effort command-argument path warnings are advisory only. Timeout values are milliseconds (default: ${DEFAULT_TIMEOUT_MS}; maximum: ${MAX_TIMEOUT_MS}). Uses the configured shell when set; otherwise uses /bin/sh on POSIX and COMSPEC or cmd.exe on Windows. Background mode (background=true) launches the command asynchronously and returns immediately; you are notified when it finishes.`,
|
description: `Execute one shell command string with the host user's filesystem, process, and network authority. The active Location is the default working directory. Relative workdir values resolve from that Location. External workdir values require external_directory approval; best-effort command-argument path warnings are advisory only. Timeout values are milliseconds (default: ${DEFAULT_TIMEOUT_MS}; maximum: ${MAX_TIMEOUT_MS}). Uses the configured shell when set; otherwise uses /bin/sh on POSIX and COMSPEC or cmd.exe on Windows. Background mode (background=true) launches the command asynchronously and returns immediately; you are notified when it finishes.`,
|
||||||
@@ -154,13 +152,6 @@ export const layer = Layer.effectDiscard(
|
|||||||
},
|
},
|
||||||
execute: (input, context) =>
|
execute: (input, context) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const parent = yield* sessions
|
|
||||||
.get(context.sessionID)
|
|
||||||
.pipe(Effect.mapError(() => new ToolFailure({ message: `Session not found: ${context.sessionID}` })))
|
|
||||||
return yield* Effect.gen(function* () {
|
|
||||||
const mutation = yield* LocationMutation.Service
|
|
||||||
const shell = yield* Shell.Service
|
|
||||||
const permission = yield* PermissionV2.Service
|
|
||||||
const source = {
|
const source = {
|
||||||
type: "tool" as const,
|
type: "tool" as const,
|
||||||
messageID: context.assistantMessageID,
|
messageID: context.assistantMessageID,
|
||||||
@@ -215,14 +206,14 @@ export const layer = Layer.effectDiscard(
|
|||||||
}).pipe(Effect.onInterrupt(() => shell.remove(info.id).pipe(Effect.ignore)))
|
}).pipe(Effect.onInterrupt(() => shell.remove(info.id).pipe(Effect.ignore)))
|
||||||
})
|
})
|
||||||
|
|
||||||
const info = yield* jobs.start({
|
const info = yield* runtime.job.start({
|
||||||
id: context.toolCallID,
|
id: context.toolCallID,
|
||||||
type: name,
|
type: name,
|
||||||
title: input.command,
|
title: input.command,
|
||||||
metadata: { sessionID: context.sessionID },
|
metadata: { sessionID: context.sessionID },
|
||||||
run: run(),
|
run: run(),
|
||||||
})
|
})
|
||||||
yield* jobs.background(info.id)
|
yield* runtime.job.background(info.id)
|
||||||
yield* notifyWhenDone(context.sessionID, context.toolCallID, input.command)
|
yield* notifyWhenDone(context.sessionID, context.toolCallID, input.command)
|
||||||
return {
|
return {
|
||||||
output: BACKGROUND_STARTED,
|
output: BACKGROUND_STARTED,
|
||||||
@@ -262,16 +253,9 @@ export const layer = Layer.effectDiscard(
|
|||||||
status: "completed" as const,
|
status: "completed" as const,
|
||||||
...(warnings.length ? { warnings } : {}),
|
...(warnings.length ? { warnings } : {}),
|
||||||
}
|
}
|
||||||
}).pipe(Effect.provide(locations.get(parent.location)))
|
|
||||||
}).pipe(Effect.mapError(() => new ToolFailure({ message: `Unable to execute command: ${input.command}` }))),
|
}).pipe(Effect.mapError(() => new ToolFailure({ message: `Unable to execute command: ${input.command}` }))),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
|
|
||||||
export const node = makeGlobalNode({
|
|
||||||
name: "shell-tool",
|
|
||||||
layer,
|
|
||||||
deps: [ApplicationTools.node, SessionV2.node, Job.node, LocationServiceMap.node, FSUtil.node],
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
export * as SubagentTool from "./subagent"
|
export * as SubagentTool from "./subagent"
|
||||||
|
|
||||||
import { ToolFailure } from "@opencode-ai/llm"
|
import { ToolFailure } from "@opencode-ai/llm"
|
||||||
import { Effect, Layer, Schema, Scope } from "effect"
|
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||||
|
import { Effect, Schema, Scope } from "effect"
|
||||||
import { AgentV2 } from "../agent"
|
import { AgentV2 } from "../agent"
|
||||||
import { Job } from "../job"
|
import { PluginRuntime } from "../plugin/runtime"
|
||||||
import { LocationServiceMap } from "../location-service-map"
|
|
||||||
import { SessionV2 } from "../session"
|
|
||||||
import { SessionSchema } from "../session/schema"
|
import { SessionSchema } from "../session/schema"
|
||||||
import { makeGlobalNode } from "../effect/app-node"
|
|
||||||
import { ApplicationTools } from "./application-tools"
|
|
||||||
import { Tool } from "./tool"
|
import { Tool } from "./tool"
|
||||||
|
|
||||||
export const name = "subagent"
|
export const name = "subagent"
|
||||||
@@ -40,18 +37,17 @@ export const description = [
|
|||||||
"Use background only for independent work that can run while you continue elsewhere.",
|
"Use background only for independent work that can run while you continue elsewhere.",
|
||||||
].join("\n")
|
].join("\n")
|
||||||
|
|
||||||
export const layer = Layer.effectDiscard(
|
export const Plugin = {
|
||||||
Effect.gen(function* () {
|
id: "core-subagent-tool",
|
||||||
const tools = yield* ApplicationTools.Service
|
effect: Effect.fn("SubagentTool.Plugin")(function* (ctx: PluginContext) {
|
||||||
const sessions = yield* SessionV2.Service
|
const runtime = yield* PluginRuntime.Service
|
||||||
const jobs = yield* Job.Service
|
const agents = yield* AgentV2.Service
|
||||||
const locations = yield* LocationServiceMap.Service
|
|
||||||
const scope = yield* Scope.Scope
|
const scope = yield* Scope.Scope
|
||||||
|
|
||||||
// Concatenate the child's final completed assistant text. Distinguishes "completed with no
|
// Concatenate the child's final completed assistant text. Distinguishes "completed with no
|
||||||
// text" (generic string) from "failed" (the run effect fails, surfaced as a job error).
|
// text" (generic string) from "failed" (the run effect fails, surfaced as a job error).
|
||||||
const latestAssistantText = Effect.fn("SubagentTool.latestAssistantText")(function* (sessionID: SessionSchema.ID) {
|
const latestAssistantText = Effect.fn("SubagentTool.latestAssistantText")(function* (sessionID: SessionSchema.ID) {
|
||||||
const messages = yield* sessions.messages({ sessionID, order: "desc", limit: 20 })
|
const messages = yield* runtime.session.messages({ sessionID, order: "desc", limit: 20 })
|
||||||
const assistant = messages.find(
|
const assistant = messages.find(
|
||||||
(message) =>
|
(message) =>
|
||||||
message.type === "assistant" && message.time.completed !== undefined && message.error === undefined,
|
message.type === "assistant" && message.time.completed !== undefined && message.error === undefined,
|
||||||
@@ -71,7 +67,7 @@ export const layer = Layer.effectDiscard(
|
|||||||
state: "completed" | "error" | "cancelled",
|
state: "completed" | "error" | "cancelled",
|
||||||
text: string,
|
text: string,
|
||||||
) {
|
) {
|
||||||
yield* sessions.synthetic({
|
yield* runtime.session.synthetic({
|
||||||
sessionID: parentID,
|
sessionID: parentID,
|
||||||
text: `<subagent id="${childID}" state="${state}" description="${description}">\n${text}\n</subagent>`,
|
text: `<subagent id="${childID}" state="${state}" description="${description}">\n${text}\n</subagent>`,
|
||||||
})
|
})
|
||||||
@@ -82,7 +78,7 @@ export const layer = Layer.effectDiscard(
|
|||||||
childID: SessionSchema.ID,
|
childID: SessionSchema.ID,
|
||||||
description: string,
|
description: string,
|
||||||
) {
|
) {
|
||||||
yield* jobs.wait({ id: childID }).pipe(
|
yield* runtime.job.wait({ id: childID }).pipe(
|
||||||
Effect.flatMap((result) => {
|
Effect.flatMap((result) => {
|
||||||
if (result.info?.status === "completed")
|
if (result.info?.status === "completed")
|
||||||
return injectCompletion(parentID, childID, description, "completed", result.info.output ?? NO_TEXT)
|
return injectCompletion(parentID, childID, description, "completed", result.info.output ?? NO_TEXT)
|
||||||
@@ -96,7 +92,7 @@ export const layer = Layer.effectDiscard(
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
yield* tools
|
yield* ctx.tool
|
||||||
.register({
|
.register({
|
||||||
[name]: Tool.make({
|
[name]: Tool.make({
|
||||||
description,
|
description,
|
||||||
@@ -105,12 +101,11 @@ export const layer = Layer.effectDiscard(
|
|||||||
toModelOutput: ({ output }) => [{ type: "text", text: output.output }],
|
toModelOutput: ({ output }) => [{ type: "text", text: output.output }],
|
||||||
execute: (input, context) =>
|
execute: (input, context) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const parent = yield* sessions
|
const parent = yield* runtime.session
|
||||||
.get(context.sessionID)
|
.get(context.sessionID)
|
||||||
.pipe(
|
.pipe(
|
||||||
Effect.mapError(() => new ToolFailure({ message: `Parent session not found: ${context.sessionID}` })),
|
Effect.mapError(() => new ToolFailure({ message: `Parent session not found: ${context.sessionID}` })),
|
||||||
)
|
)
|
||||||
const agents = yield* AgentV2.Service.pipe(Effect.provide(locations.get(parent.location)))
|
|
||||||
const agent = yield* agents.resolve(input.agent)
|
const agent = yield* agents.resolve(input.agent)
|
||||||
if (agent === undefined) return yield* new ToolFailure({ message: `Unknown agent: ${input.agent}` })
|
if (agent === undefined) return yield* new ToolFailure({ message: `Unknown agent: ${input.agent}` })
|
||||||
if (agent.mode === "primary")
|
if (agent.mode === "primary")
|
||||||
@@ -118,7 +113,7 @@ export const layer = Layer.effectDiscard(
|
|||||||
|
|
||||||
// Model selection is policy/config/session state, not an LLM-facing tool argument.
|
// Model selection is policy/config/session state, not an LLM-facing tool argument.
|
||||||
const model = agent.model ?? parent.model
|
const model = agent.model ?? parent.model
|
||||||
const child = yield* sessions
|
const child = yield* runtime.session
|
||||||
.create({
|
.create({
|
||||||
parentID: context.sessionID,
|
parentID: context.sessionID,
|
||||||
title: input.description,
|
title: input.description,
|
||||||
@@ -135,12 +130,12 @@ export const layer = Layer.effectDiscard(
|
|||||||
|
|
||||||
const run = Effect.gen(function* () {
|
const run = Effect.gen(function* () {
|
||||||
// The child session owns its agent/model (set at create); prompt only admits input.
|
// The child session owns its agent/model (set at create); prompt only admits input.
|
||||||
yield* sessions.prompt({ sessionID: child.id, prompt: { text: input.prompt }, resume: false })
|
yield* runtime.session.prompt({ sessionID: child.id, prompt: { text: input.prompt }, resume: false })
|
||||||
yield* sessions.resume(child.id)
|
yield* runtime.session.resume(child.id)
|
||||||
return yield* latestAssistantText(child.id)
|
return yield* latestAssistantText(child.id)
|
||||||
}).pipe(Effect.onInterrupt(() => sessions.interrupt(child.id)))
|
}).pipe(Effect.onInterrupt(() => runtime.session.interrupt(child.id)))
|
||||||
|
|
||||||
const info = yield* jobs.start({
|
const info = yield* runtime.job.start({
|
||||||
id: child.id,
|
id: child.id,
|
||||||
type: name,
|
type: name,
|
||||||
title: input.description,
|
title: input.description,
|
||||||
@@ -149,16 +144,16 @@ export const layer = Layer.effectDiscard(
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (background) {
|
if (background) {
|
||||||
yield* jobs.background(info.id)
|
yield* runtime.job.background(info.id)
|
||||||
yield* notifyWhenDone(context.sessionID, child.id, input.description)
|
yield* notifyWhenDone(context.sessionID, child.id, input.description)
|
||||||
return { sessionID: child.id, status: "running" as const, output: BACKGROUND_STARTED }
|
return { sessionID: child.id, status: "running" as const, output: BACKGROUND_STARTED }
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = yield* jobs
|
const result = yield* runtime.job.block({ id: child.id, sessionID: context.sessionID }).pipe(
|
||||||
.block({ id: child.id, sessionID: context.sessionID })
|
|
||||||
.pipe(
|
|
||||||
Effect.onInterrupt(() =>
|
Effect.onInterrupt(() =>
|
||||||
Effect.all([sessions.interrupt(child.id), jobs.cancel(child.id)], { discard: true }),
|
Effect.all([runtime.session.interrupt(child.id), runtime.job.cancel(child.id)], {
|
||||||
|
discard: true,
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if (result?.type === "backgrounded") {
|
if (result?.type === "backgrounded") {
|
||||||
@@ -174,13 +169,4 @@ export const layer = Layer.effectDiscard(
|
|||||||
})
|
})
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
|
|
||||||
// Registered at the app root via ApplicationTools, not as a Location node: SessionV2 sits above
|
|
||||||
// LocationServiceMap, so a location-scoped subagent node would create a static dependency cycle.
|
|
||||||
// Agent lookup is resolved through the parent Session's location when the tool executes.
|
|
||||||
export const node = makeGlobalNode({
|
|
||||||
name: "subagent-tool",
|
|
||||||
layer,
|
|
||||||
deps: [ApplicationTools.node, SessionV2.node, Job.node, LocationServiceMap.node],
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ describe("AgentV2", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const agent = yield* AgentV2.Service
|
const agent = yield* AgentV2.Service
|
||||||
|
|
||||||
expect(yield* agent.all()).toEqual([])
|
expect(yield* agent.list()).toEqual([])
|
||||||
expect(yield* agent.get(AgentV2.ID.make("build"))).toBeUndefined()
|
expect(yield* agent.get(AgentV2.ID.make("build"))).toBeUndefined()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -56,7 +56,7 @@ describe("AgentV2", () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
expect(yield* agent.get(id)).toMatchObject({ id, description: "Reviews code", mode: "subagent" })
|
expect(yield* agent.get(id)).toMatchObject({ id, description: "Reviews code", mode: "subagent" })
|
||||||
expect((yield* agent.all()).map((info) => info.id)).toEqual([id])
|
expect((yield* agent.list()).map((info) => info.id)).toEqual([id])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ describe("AgentV2", () => {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const agents = yield* agent.all()
|
const agents = yield* agent.list()
|
||||||
expect(agents.map((item) => String(item.id)).sort()).toEqual([
|
expect(agents.map((item) => String(item.id)).sort()).toEqual([
|
||||||
"build",
|
"build",
|
||||||
"compaction",
|
"compaction",
|
||||||
|
|||||||
@@ -1,288 +0,0 @@
|
|||||||
import { describe, expect } from "bun:test"
|
|
||||||
import { Tool } from "@opencode-ai/core/tool/tool"
|
|
||||||
import { ApplicationTools } from "@opencode-ai/core/tool/application-tools"
|
|
||||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
|
||||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
|
||||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
|
||||||
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
|
||||||
import { executeTool, settleTool, toolDefinitions } from "./lib/tool"
|
|
||||||
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
|
||||||
import { Tools } from "@opencode-ai/core/tool/tools"
|
|
||||||
import { Deferred, Effect, Exit, Fiber, Layer, Schema, Scope } from "effect"
|
|
||||||
import { testEffect } from "./lib/effect"
|
|
||||||
|
|
||||||
const permission = Layer.mock(PermissionV2.Service, {
|
|
||||||
assert: () => Effect.void,
|
|
||||||
})
|
|
||||||
const applications = ApplicationTools.layer
|
|
||||||
const registry = ToolRegistry.layer.pipe(
|
|
||||||
Layer.provide(permission),
|
|
||||||
Layer.provide(applications),
|
|
||||||
Layer.provide(ToolOutputStore.defaultLayer),
|
|
||||||
)
|
|
||||||
const it = testEffect(Layer.mergeAll(applications, registry))
|
|
||||||
|
|
||||||
const sessionID = SessionV2.ID.make("ses_application_tool")
|
|
||||||
const agent = AgentV2.ID.make("build")
|
|
||||||
const assistantMessageID = SessionMessage.ID.make("msg_application_tool")
|
|
||||||
const contextual = (contexts: Tool.Context[]) =>
|
|
||||||
Tool.make({
|
|
||||||
description: "Read application context",
|
|
||||||
input: Schema.Struct({ query: Schema.String }),
|
|
||||||
output: Schema.Struct({ answer: Schema.String }),
|
|
||||||
execute: ({ query }, context) =>
|
|
||||||
Effect.sync(() => {
|
|
||||||
contexts.push(context)
|
|
||||||
return { answer: query.toUpperCase() }
|
|
||||||
}),
|
|
||||||
toModelOutput: ({ output }) => [
|
|
||||||
{ type: "text", text: output.answer },
|
|
||||||
{ type: "file", data: "aGVsbG8=", mime: "image/png", name: "result.png" },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("ApplicationTools", () => {
|
|
||||||
it.effect("keeps the Core carrier opaque and executes its single handler", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const applications = yield* ApplicationTools.Service
|
|
||||||
const registry = yield* ToolRegistry.Service
|
|
||||||
const contexts: Tool.Context[] = []
|
|
||||||
const tool = contextual(contexts)
|
|
||||||
expect(Object.keys(tool)).toEqual([])
|
|
||||||
|
|
||||||
yield* applications.register({ opaque: tool })
|
|
||||||
expect(
|
|
||||||
yield* executeTool(registry, {
|
|
||||||
sessionID,
|
|
||||||
agent,
|
|
||||||
assistantMessageID,
|
|
||||||
call: { type: "tool-call", id: "call-opaque", name: "opaque", input: { query: "once" } },
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
type: "content",
|
|
||||||
value: [
|
|
||||||
{ type: "text", text: "ONCE" },
|
|
||||||
{ type: "file", uri: "data:image/png;base64,aGVsbG8=", mime: "image/png", name: "result.png" },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
expect(contexts).toEqual([{ sessionID, agent, assistantMessageID, toolCallID: "call-opaque" }])
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("exposes narrow scoped Location registration and sanitizes names", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const tools: Tools.Interface = yield* Tools.Service
|
|
||||||
const registry = yield* ToolRegistry.Service
|
|
||||||
const scope = yield* Scope.make()
|
|
||||||
|
|
||||||
yield* tools.register({ "location.tool/search": contextual([]) }).pipe(Scope.provide(scope))
|
|
||||||
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["location_tool_search"])
|
|
||||||
|
|
||||||
yield* Scope.close(scope, Exit.void)
|
|
||||||
expect(yield* toolDefinitions(registry)).toEqual([])
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("filters an application tool by its name without adding execution authorization", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const applications = yield* ApplicationTools.Service
|
|
||||||
const registry = yield* ToolRegistry.Service
|
|
||||||
const contexts: Tool.Context[] = []
|
|
||||||
yield* applications.register({ application_context: contextual(contexts) })
|
|
||||||
|
|
||||||
expect(
|
|
||||||
yield* toolDefinitions(registry, [{ action: "application_context", resource: "*", effect: "deny" }]),
|
|
||||||
).toEqual([])
|
|
||||||
expect(
|
|
||||||
yield* settleTool(registry, {
|
|
||||||
sessionID,
|
|
||||||
agent,
|
|
||||||
assistantMessageID,
|
|
||||||
call: { type: "tool-call", id: "call-denied", name: "application_context", input: { query: "hello" } },
|
|
||||||
}),
|
|
||||||
).toMatchObject({ result: { type: "content" } })
|
|
||||||
expect(contexts).toEqual([{ sessionID, agent, assistantMessageID, toolCallID: "call-denied" }])
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("advertises and executes a scoped application tool with Session context", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const applications = yield* ApplicationTools.Service
|
|
||||||
const registry = yield* ToolRegistry.Service
|
|
||||||
const contexts: Tool.Context[] = []
|
|
||||||
|
|
||||||
yield* applications.register({ application_context: contextual(contexts) })
|
|
||||||
|
|
||||||
expect(yield* toolDefinitions(registry)).toMatchObject([
|
|
||||||
{ name: "application_context", description: "Read application context" },
|
|
||||||
])
|
|
||||||
expect(
|
|
||||||
yield* settleTool(registry, {
|
|
||||||
sessionID,
|
|
||||||
agent,
|
|
||||||
assistantMessageID,
|
|
||||||
call: { type: "tool-call", id: "call-context", name: "application_context", input: { query: "hello" } },
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
result: {
|
|
||||||
type: "content",
|
|
||||||
value: [
|
|
||||||
{ type: "text", text: "HELLO" },
|
|
||||||
{ type: "file", uri: "data:image/png;base64,aGVsbG8=", mime: "image/png", name: "result.png" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
structured: { answer: "HELLO" },
|
|
||||||
content: [
|
|
||||||
{ type: "text", text: "HELLO" },
|
|
||||||
{ type: "file", uri: "data:image/png;base64,aGVsbG8=", mime: "image/png", name: "result.png" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
expect(contexts).toEqual([{ sessionID, agent, assistantMessageID, toolCallID: "call-context" }])
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("removes an application tool when its registration scope closes", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const applications = yield* ApplicationTools.Service
|
|
||||||
const registry = yield* ToolRegistry.Service
|
|
||||||
const scope = yield* Scope.make()
|
|
||||||
|
|
||||||
yield* applications.register({ temporary: contextual([]) }).pipe(Scope.provide(scope))
|
|
||||||
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["temporary"])
|
|
||||||
|
|
||||||
yield* Scope.close(scope, Exit.void)
|
|
||||||
expect(yield* toolDefinitions(registry)).toEqual([])
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("removes a tool before settling a call produced from an earlier definition", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const applications = yield* ApplicationTools.Service
|
|
||||||
const registry = yield* ToolRegistry.Service
|
|
||||||
const registrationScope = yield* Scope.make()
|
|
||||||
yield* applications.register({ contextual: contextual([]) }).pipe(Scope.provide(registrationScope))
|
|
||||||
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["contextual"])
|
|
||||||
|
|
||||||
yield* Scope.close(registrationScope, Exit.void)
|
|
||||||
expect(
|
|
||||||
yield* settleTool(registry, {
|
|
||||||
sessionID,
|
|
||||||
agent,
|
|
||||||
assistantMessageID,
|
|
||||||
call: { type: "tool-call", id: "call-removed", name: "contextual", input: { query: "hello" } },
|
|
||||||
}),
|
|
||||||
).toEqual({ result: { type: "error", value: "Unknown tool: contextual" } })
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("does not leak a registration into an already closed scope", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const applications = yield* ApplicationTools.Service
|
|
||||||
const registry = yield* ToolRegistry.Service
|
|
||||||
const scope = yield* Scope.make()
|
|
||||||
yield* Scope.close(scope, Exit.void)
|
|
||||||
|
|
||||||
yield* applications.register({ closed: contextual([]) }).pipe(Scope.provide(scope))
|
|
||||||
|
|
||||||
expect(yield* toolDefinitions(registry)).toEqual([])
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("preserves an interrupted application registration until its scope closes", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const applications = yield* ApplicationTools.Service
|
|
||||||
const registry = yield* ToolRegistry.Service
|
|
||||||
const scope = yield* Scope.make()
|
|
||||||
const registered = yield* Deferred.make<void>()
|
|
||||||
const fiber = yield* applications
|
|
||||||
.register({ interrupted: contextual([]) })
|
|
||||||
.pipe(
|
|
||||||
Effect.andThen(Deferred.succeed(registered, undefined)),
|
|
||||||
Effect.andThen(Effect.never),
|
|
||||||
Scope.provide(scope),
|
|
||||||
Effect.forkChild,
|
|
||||||
)
|
|
||||||
yield* Deferred.await(registered)
|
|
||||||
yield* Fiber.interrupt(fiber)
|
|
||||||
|
|
||||||
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["interrupted"])
|
|
||||||
yield* Scope.close(scope, Exit.void)
|
|
||||||
expect(yield* toolDefinitions(registry)).toEqual([])
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("captures the registered record before later State rebuilds", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const applications = yield* ApplicationTools.Service
|
|
||||||
const registry = yield* ToolRegistry.Service
|
|
||||||
const registered = { stable: contextual([]) }
|
|
||||||
yield* applications.register(registered)
|
|
||||||
Object.assign(registered, { late: contextual([]) })
|
|
||||||
|
|
||||||
yield* Effect.scoped(applications.register({ temporary: contextual([]) }))
|
|
||||||
|
|
||||||
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["stable"])
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("settles with the current same-name application tool and restores earlier registrations", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const applications = yield* ApplicationTools.Service
|
|
||||||
const registry = yield* ToolRegistry.Service
|
|
||||||
const firstContexts: Tool.Context[] = []
|
|
||||||
const secondContexts: Tool.Context[] = []
|
|
||||||
const scope = yield* Scope.make()
|
|
||||||
yield* applications.register({ contextual: contextual(firstContexts) })
|
|
||||||
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["contextual"])
|
|
||||||
yield* applications.register({ contextual: contextual(secondContexts) }).pipe(Scope.provide(scope))
|
|
||||||
|
|
||||||
yield* settleTool(registry, {
|
|
||||||
sessionID,
|
|
||||||
agent,
|
|
||||||
assistantMessageID,
|
|
||||||
call: { type: "tool-call", id: "call-second", name: "contextual", input: { query: "second" } },
|
|
||||||
})
|
|
||||||
yield* Scope.close(scope, Exit.void)
|
|
||||||
yield* settleTool(registry, {
|
|
||||||
sessionID,
|
|
||||||
agent,
|
|
||||||
assistantMessageID,
|
|
||||||
call: { type: "tool-call", id: "call-first", name: "contextual", input: { query: "first" } },
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(secondContexts).toEqual([{ sessionID, agent, assistantMessageID, toolCallID: "call-second" }])
|
|
||||||
expect(firstContexts).toEqual([{ sessionID, agent, assistantMessageID, toolCallID: "call-first" }])
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("keeps the Location tool when an application tool has the same name", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const applications = yield* ApplicationTools.Service
|
|
||||||
const registry = yield* ToolRegistry.Service
|
|
||||||
const locationContexts: Tool.Context[] = []
|
|
||||||
const applicationContexts: Tool.Context[] = []
|
|
||||||
const location = contextual(locationContexts)
|
|
||||||
yield* registry.register({ shared: location })
|
|
||||||
yield* applications.register({ shared: contextual(applicationContexts) })
|
|
||||||
|
|
||||||
expect(
|
|
||||||
(yield* toolDefinitions(registry, [{ action: "shared", resource: "*", effect: "deny" }])).map(
|
|
||||||
(definition) => definition.name,
|
|
||||||
),
|
|
||||||
).toEqual([])
|
|
||||||
expect(
|
|
||||||
yield* settleTool(registry, {
|
|
||||||
sessionID,
|
|
||||||
agent,
|
|
||||||
assistantMessageID,
|
|
||||||
call: { type: "tool-call", id: "call-shared", name: "shared", input: { query: "location" } },
|
|
||||||
}),
|
|
||||||
).toMatchObject({ result: { type: "content" } })
|
|
||||||
expect(locationContexts).toEqual([{ sessionID, agent, assistantMessageID, toolCallID: "call-shared" }])
|
|
||||||
expect(applicationContexts).toEqual([])
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
@@ -17,6 +17,10 @@ const it = testEffect(
|
|||||||
Layer.mergeAll(AgentV2.locationLayer.pipe(Layer.provideMerge(EventV2.defaultLayer)), FSUtil.defaultLayer),
|
Layer.mergeAll(AgentV2.locationLayer.pipe(Layer.provideMerge(EventV2.defaultLayer)), FSUtil.defaultLayer),
|
||||||
)
|
)
|
||||||
const decode = Schema.decodeUnknownSync(Config.Info)
|
const decode = Schema.decodeUnknownSync(Config.Info)
|
||||||
|
const defaultPermissions = [
|
||||||
|
{ action: "*", resource: "*", effect: "allow" },
|
||||||
|
{ action: "external_directory", resource: "*", effect: "ask" },
|
||||||
|
] satisfies PermissionV2.Ruleset
|
||||||
|
|
||||||
describe("ConfigAgentPlugin.Plugin", () => {
|
describe("ConfigAgentPlugin.Plugin", () => {
|
||||||
it.effect("applies all global permissions before agent-specific permissions", () =>
|
it.effect("applies all global permissions before agent-specific permissions", () =>
|
||||||
@@ -77,8 +81,7 @@ describe("ConfigAgentPlugin.Plugin", () => {
|
|||||||
const buildAgent = yield* agents.get(build)
|
const buildAgent = yield* agents.get(build)
|
||||||
if (!buildAgent) throw new Error("expected configured build agent")
|
if (!buildAgent) throw new Error("expected configured build agent")
|
||||||
expect(buildAgent.permissions).toEqual([
|
expect(buildAgent.permissions).toEqual([
|
||||||
{ action: "*", resource: "*", effect: "allow" },
|
...defaultPermissions,
|
||||||
{ action: "external_directory", resource: "*", effect: "ask" },
|
|
||||||
{ action: "bash", resource: "*", effect: "allow" },
|
{ action: "bash", resource: "*", effect: "allow" },
|
||||||
{ action: "bash", resource: "*", effect: "ask" },
|
{ action: "bash", resource: "*", effect: "ask" },
|
||||||
{ action: "read", resource: "*", effect: "allow" },
|
{ action: "read", resource: "*", effect: "allow" },
|
||||||
@@ -96,8 +99,7 @@ describe("ConfigAgentPlugin.Plugin", () => {
|
|||||||
model: { providerID: "openrouter", id: "openai/gpt-5", variant: "high" },
|
model: { providerID: "openrouter", id: "openai/gpt-5", variant: "high" },
|
||||||
})
|
})
|
||||||
expect(reviewer.permissions).toEqual([
|
expect(reviewer.permissions).toEqual([
|
||||||
{ action: "*", resource: "*", effect: "allow" },
|
...defaultPermissions,
|
||||||
{ action: "external_directory", resource: "*", effect: "ask" },
|
|
||||||
{ action: "bash", resource: "*", effect: "ask" },
|
{ action: "bash", resource: "*", effect: "ask" },
|
||||||
{ action: "read", resource: "*", effect: "allow" },
|
{ action: "read", resource: "*", effect: "allow" },
|
||||||
{ action: "edit", resource: "*", effect: "deny" },
|
{ action: "edit", resource: "*", effect: "deny" },
|
||||||
@@ -105,8 +107,7 @@ describe("ConfigAgentPlugin.Plugin", () => {
|
|||||||
])
|
])
|
||||||
expect(PermissionV2.evaluate("read", "README.md", reviewer.permissions).effect).toBe("deny")
|
expect(PermissionV2.evaluate("read", "README.md", reviewer.permissions).effect).toBe("deny")
|
||||||
expect((yield* agents.get(AgentV2.ID.make("late")))?.permissions).toEqual([
|
expect((yield* agents.get(AgentV2.ID.make("late")))?.permissions).toEqual([
|
||||||
{ action: "*", resource: "*", effect: "allow" },
|
...defaultPermissions,
|
||||||
{ action: "external_directory", resource: "*", effect: "ask" },
|
|
||||||
{ action: "bash", resource: "*", effect: "ask" },
|
{ action: "bash", resource: "*", effect: "ask" },
|
||||||
{ action: "read", resource: "*", effect: "allow" },
|
{ action: "read", resource: "*", effect: "allow" },
|
||||||
{ action: "edit", resource: "*", effect: "allow" },
|
{ action: "edit", resource: "*", effect: "allow" },
|
||||||
@@ -264,21 +265,13 @@ Use native v2 fields.`,
|
|||||||
system: "Review carefully.",
|
system: "Review carefully.",
|
||||||
description: "Markdown description",
|
description: "Markdown description",
|
||||||
request: { body: { temperature: 0.5 } },
|
request: { body: { temperature: 0.5 } },
|
||||||
permissions: [
|
permissions: [...defaultPermissions, { action: "edit", resource: "*", effect: "deny" }],
|
||||||
{ action: "*", resource: "*", effect: "allow" },
|
|
||||||
{ action: "external_directory", resource: "*", effect: "ask" },
|
|
||||||
{ action: "edit", resource: "*", effect: "deny" },
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
expect(yield* agents.get(AgentV2.ID.make("team/helper"))).toMatchObject({ system: "Help the team." })
|
expect(yield* agents.get(AgentV2.ID.make("team/helper"))).toMatchObject({ system: "Help the team." })
|
||||||
expect(yield* agents.get(AgentV2.ID.make("native"))).toMatchObject({
|
expect(yield* agents.get(AgentV2.ID.make("native"))).toMatchObject({
|
||||||
system: "Use native v2 fields.",
|
system: "Use native v2 fields.",
|
||||||
request: { headers: { "x-agent": "native" }, body: { effort: "high" } },
|
request: { headers: { "x-agent": "native" }, body: { effort: "high" } },
|
||||||
permissions: [
|
permissions: [...defaultPermissions, { action: "edit", resource: "*", effect: "deny" }],
|
||||||
{ action: "*", resource: "*", effect: "allow" },
|
|
||||||
{ action: "external_directory", resource: "*", effect: "ask" },
|
|
||||||
{ action: "edit", resource: "*", effect: "deny" },
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
expect(yield* agents.get(AgentV2.ID.make("disabled"))).toBeUndefined()
|
expect(yield* agents.get(AgentV2.ID.make("disabled"))).toBeUndefined()
|
||||||
expect(yield* agents.get(AgentV2.ID.make("plan"))).toMatchObject({ system: "Make a plan.", mode: "primary" })
|
expect(yield* agents.get(AgentV2.ID.make("plan"))).toMatchObject({ system: "Make a plan.", mode: "primary" })
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class OtherError {
|
|||||||
|
|
||||||
const tags = LayerNode.tags({ app: [] })
|
const tags = LayerNode.tags({ app: [] })
|
||||||
const make = tags.make("app")
|
const make = tags.make("app")
|
||||||
const build = <A, E>(root: LayerNode.Node<A, E, any>) => LayerNode.compile(root) as Layer.Layer<A, E>
|
const build = <A, E>(root: LayerNode.Node<A, E, any>) => LayerNode.compile(root)
|
||||||
const aLayer = Layer.succeed(A, A.of({}))
|
const aLayer = Layer.succeed(A, A.of({}))
|
||||||
const bLayer = Layer.effect(B, Effect.as(A, B.of({})))
|
const bLayer = Layer.effect(B, Effect.as(A, B.of({})))
|
||||||
const cLayer = Layer.effect(
|
const cLayer = Layer.effect(
|
||||||
@@ -32,8 +32,6 @@ const b = make({ service: B, layer: bLayer, deps: [a] })
|
|||||||
const c = make({ service: C, layer: cLayer, deps: [a, b] })
|
const c = make({ service: C, layer: cLayer, deps: [a, b] })
|
||||||
const failing = make({ service: A, layer: failingA, deps: [] })
|
const failing = make({ service: A, layer: failingA, deps: [] })
|
||||||
const dependent = make({ service: B, layer: bLayer, deps: [failing] })
|
const dependent = make({ service: B, layer: bLayer, deps: [failing] })
|
||||||
const inputA = LayerNode.unbound(A, tags.values.app)
|
|
||||||
const inputDependent = make({ service: B, layer: bLayer, deps: [inputA] })
|
|
||||||
|
|
||||||
make({ name: "manual-a", layer: aLayer, deps: [] })
|
make({ name: "manual-a", layer: aLayer, deps: [] })
|
||||||
|
|
||||||
@@ -51,8 +49,8 @@ make({ service: C, layer: cLayer, deps: [a] })
|
|||||||
|
|
||||||
const closed = build(LayerNode.group([c]))
|
const closed = build(LayerNode.group([c]))
|
||||||
const closedWithError = build(LayerNode.group([dependent]))
|
const closedWithError = build(LayerNode.group([dependent]))
|
||||||
const checkClosed: Layer.Layer<C, never, never> = closed
|
const checkClosed: Layer.Layer<C> = closed
|
||||||
const checkError: Layer.Layer<B, LayerError, never> = closedWithError
|
const checkError: Layer.Layer<B, LayerError> = closedWithError
|
||||||
void checkClosed
|
void checkClosed
|
||||||
void checkError
|
void checkError
|
||||||
|
|
||||||
@@ -64,7 +62,6 @@ LayerNode.replace(aLayer, Layer.succeed(B, B.of({})))
|
|||||||
// @ts-expect-error Replacement cannot introduce a new error
|
// @ts-expect-error Replacement cannot introduce a new error
|
||||||
LayerNode.replace(aLayer, Layer.effect(A, Effect.fail(new OtherError())))
|
LayerNode.replace(aLayer, Layer.effect(A, Effect.fail(new OtherError())))
|
||||||
|
|
||||||
// @ts-expect-error Replacement must be closed
|
|
||||||
LayerNode.replace(bLayer, bLayer)
|
LayerNode.replace(bLayer, bLayer)
|
||||||
|
|
||||||
class TagA extends Context.Service<TagA, {}>()("test/TagA") {}
|
class TagA extends Context.Service<TagA, {}>()("test/TagA") {}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import fs from "fs/promises"
|
|||||||
import { tmpdir as osTmpdir } from "os"
|
import { tmpdir as osTmpdir } from "os"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
export const tmpdir = async () => {
|
export const tmpdir = async (prefix = "opencode-core-test-") => {
|
||||||
const dir = await fs.realpath(await fs.mkdtemp(path.join(osTmpdir(), "opencode-core-test-")))
|
const dir = await fs.realpath(await fs.mkdtemp(path.join(osTmpdir(), prefix)))
|
||||||
return {
|
return {
|
||||||
path: dir,
|
path: dir,
|
||||||
async [Symbol.asyncDispose]() {
|
async [Symbol.asyncDispose]() {
|
||||||
|
|||||||
@@ -18,6 +18,22 @@ export const toolDefinitions = (
|
|||||||
model = testModel,
|
model = testModel,
|
||||||
) => registry.materialize({ permissions, model }).pipe(Effect.map((materialized) => materialized.definitions))
|
) => registry.materialize({ permissions, model }).pipe(Effect.map((materialized) => materialized.definitions))
|
||||||
|
|
||||||
|
export function waitForTool(
|
||||||
|
registry: ToolRegistry.Interface,
|
||||||
|
name: string,
|
||||||
|
remaining = 1000,
|
||||||
|
): Effect.Effect<void, Error> {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
if ((yield* toolDefinitions(registry)).some((tool) => tool.name === name)) return
|
||||||
|
if (remaining === 0) {
|
||||||
|
yield* Effect.fail(new Error(`Timed out waiting for tool: ${name}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
yield* Effect.promise(() => Bun.sleep(1))
|
||||||
|
yield* waitForTool(registry, name, remaining - 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const settleTool = (registry: ToolRegistry.Interface, input: ToolRegistry.ExecuteInput, model = testModel) =>
|
export const settleTool = (registry: ToolRegistry.Interface, input: ToolRegistry.ExecuteInput, model = testModel) =>
|
||||||
registry.materialize({ model }).pipe(Effect.flatMap((materialized) => materialized.settle(input)))
|
registry.materialize({ model }).pipe(Effect.flatMap((materialized) => materialized.settle(input)))
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import fs from "fs/promises"
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { DateTime, Effect, Equal, Hash, Layer, Schema } from "effect"
|
import { DateTime, Effect, Equal, Hash, Layer, Schema } from "effect"
|
||||||
import { Tool } from "@opencode-ai/core/tool/tool"
|
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
@@ -17,7 +16,7 @@ import { SessionV2 } from "@opencode-ai/core/session"
|
|||||||
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
||||||
import { tmpdir } from "./fixture/tmpdir"
|
import { tmpdir } from "./fixture/tmpdir"
|
||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
import { toolDefinitions } from "./lib/tool"
|
import { toolDefinitions, waitForTool } from "./lib/tool"
|
||||||
import { FSUtil } from "../src/fs-util"
|
import { FSUtil } from "../src/fs-util"
|
||||||
import { Credential } from "../src/credential"
|
import { Credential } from "../src/credential"
|
||||||
import { Database } from "../src/database/database"
|
import { Database } from "../src/database/database"
|
||||||
@@ -28,14 +27,11 @@ import { Npm } from "../src/npm"
|
|||||||
import { Project } from "../src/project"
|
import { Project } from "../src/project"
|
||||||
import { Reference } from "../src/reference"
|
import { Reference } from "../src/reference"
|
||||||
import { ToolRegistry } from "../src/tool/registry"
|
import { ToolRegistry } from "../src/tool/registry"
|
||||||
import { ApplicationTools } from "../src/tool/application-tools"
|
|
||||||
|
|
||||||
const applicationTools = ApplicationTools.layer
|
|
||||||
const it = testEffect(
|
const it = testEffect(
|
||||||
Layer.merge(
|
Layer.merge(
|
||||||
Layer.mergeAll(applicationTools, Database.defaultLayer, EventV2.defaultLayer),
|
Layer.mergeAll(Database.defaultLayer, EventV2.defaultLayer),
|
||||||
locationServiceMapLayer.pipe(
|
locationServiceMapLayer.pipe(
|
||||||
Layer.provide(applicationTools),
|
|
||||||
Layer.provide(
|
Layer.provide(
|
||||||
Layer.mergeAll(
|
Layer.mergeAll(
|
||||||
Project.defaultLayer,
|
Project.defaultLayer,
|
||||||
@@ -83,14 +79,6 @@ describe("LocationServiceMap", () => {
|
|||||||
).pipe(
|
).pipe(
|
||||||
Effect.flatMap(([blocked, allowed]) =>
|
Effect.flatMap(([blocked, allowed]) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* (yield* ApplicationTools.Service).register({
|
|
||||||
application_context: Tool.make({
|
|
||||||
description: "Read application context",
|
|
||||||
input: Schema.Struct({}),
|
|
||||||
output: Schema.Struct({ ok: Schema.Boolean }),
|
|
||||||
execute: () => Effect.succeed({ ok: true }),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
yield* Effect.promise(() =>
|
yield* Effect.promise(() =>
|
||||||
fs.writeFile(
|
fs.writeFile(
|
||||||
path.join(blocked.path, "opencode.json"),
|
path.join(blocked.path, "opencode.json"),
|
||||||
@@ -105,9 +93,12 @@ describe("LocationServiceMap", () => {
|
|||||||
yield* Reference.Service
|
yield* Reference.Service
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
|
yield* catalog.transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
|
||||||
|
const registry = yield* ToolRegistry.Service
|
||||||
|
yield* waitForTool(registry, "shell")
|
||||||
|
yield* waitForTool(registry, "subagent")
|
||||||
return {
|
return {
|
||||||
providers: yield* catalog.provider.all(),
|
providers: yield* catalog.provider.all(),
|
||||||
tools: yield* toolDefinitions(yield* ToolRegistry.Service),
|
tools: yield* toolDefinitions(registry),
|
||||||
}
|
}
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.scoped,
|
Effect.scoped,
|
||||||
@@ -119,13 +110,14 @@ describe("LocationServiceMap", () => {
|
|||||||
const blockedState = yield* update(blocked.path)
|
const blockedState = yield* update(blocked.path)
|
||||||
expect(blockedState.providers.some((provider) => provider.id === ProviderV2.ID.make("test"))).toBe(false)
|
expect(blockedState.providers.some((provider) => provider.id === ProviderV2.ID.make("test"))).toBe(false)
|
||||||
expect(blockedState.tools.map((tool) => tool.name).sort()).toEqual([
|
expect(blockedState.tools.map((tool) => tool.name).sort()).toEqual([
|
||||||
"application_context",
|
|
||||||
"edit",
|
"edit",
|
||||||
"glob",
|
"glob",
|
||||||
"grep",
|
"grep",
|
||||||
"question",
|
"question",
|
||||||
"read",
|
"read",
|
||||||
|
"shell",
|
||||||
"skill",
|
"skill",
|
||||||
|
"subagent",
|
||||||
"todowrite",
|
"todowrite",
|
||||||
"webfetch",
|
"webfetch",
|
||||||
"websearch",
|
"websearch",
|
||||||
@@ -134,13 +126,14 @@ describe("LocationServiceMap", () => {
|
|||||||
const allowedState = yield* update(allowed.path)
|
const allowedState = yield* update(allowed.path)
|
||||||
expect(allowedState.providers.some((provider) => provider.id === ProviderV2.ID.make("test"))).toBe(true)
|
expect(allowedState.providers.some((provider) => provider.id === ProviderV2.ID.make("test"))).toBe(true)
|
||||||
expect(allowedState.tools.map((tool) => tool.name).sort()).toEqual([
|
expect(allowedState.tools.map((tool) => tool.name).sort()).toEqual([
|
||||||
"application_context",
|
|
||||||
"edit",
|
"edit",
|
||||||
"glob",
|
"glob",
|
||||||
"grep",
|
"grep",
|
||||||
"question",
|
"question",
|
||||||
"read",
|
"read",
|
||||||
|
"shell",
|
||||||
"skill",
|
"skill",
|
||||||
|
"subagent",
|
||||||
"todowrite",
|
"todowrite",
|
||||||
"webfetch",
|
"webfetch",
|
||||||
"websearch",
|
"websearch",
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect, Exit, Fiber } from "effect"
|
import { Effect, Exit, Fiber, Schema } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
|
import { Tool } from "@opencode-ai/core/tool/tool"
|
||||||
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
|
import { testModel } from "./lib/tool"
|
||||||
import { PluginTestLayer } from "./plugin/fixture"
|
import { PluginTestLayer } from "./plugin/fixture"
|
||||||
|
|
||||||
const it = testEffect(PluginTestLayer)
|
const it = testEffect(PluginTestLayer)
|
||||||
@@ -68,4 +71,35 @@ describe("PluginV2", () => {
|
|||||||
expect(yield* agents.get(AgentV2.ID.make("configured"))).toBeUndefined()
|
expect(yield* agents.get(AgentV2.ID.make("configured"))).toBeUndefined()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("registers location tools through the plugin context", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const plugins = yield* PluginV2.Service
|
||||||
|
const registry = yield* ToolRegistry.Service
|
||||||
|
const plugin = define({
|
||||||
|
id: "tool-plugin",
|
||||||
|
effect: (ctx) =>
|
||||||
|
ctx.tool
|
||||||
|
.register({
|
||||||
|
plugin_tool: Tool.make({
|
||||||
|
description: "Plugin tool",
|
||||||
|
input: Schema.Struct({}),
|
||||||
|
output: Schema.Struct({ ok: Schema.Boolean }),
|
||||||
|
execute: () => Effect.succeed({ ok: true }),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.pipe(Effect.orDie),
|
||||||
|
})
|
||||||
|
|
||||||
|
yield* plugins.add(PluginV2.ID.make(plugin.id), plugin.effect)
|
||||||
|
expect((yield* registry.materialize({ model: testModel })).definitions.map((tool) => tool.name)).toContain(
|
||||||
|
"plugin_tool",
|
||||||
|
)
|
||||||
|
|
||||||
|
yield* plugins.remove(PluginV2.ID.make(plugin.id))
|
||||||
|
expect((yield* registry.materialize({ model: testModel })).definitions.map((tool) => tool.name)).not.toContain(
|
||||||
|
"plugin_tool",
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export function host(overrides: Overrides = {}): PluginContext {
|
|||||||
return {
|
return {
|
||||||
options: {},
|
options: {},
|
||||||
agent: overrides.agent ?? {
|
agent: overrides.agent ?? {
|
||||||
|
list: () => Effect.die("unused agent.list"),
|
||||||
transform: () => Effect.die("unused agent.transform"),
|
transform: () => Effect.die("unused agent.transform"),
|
||||||
reload: () => Effect.die("unused agent.reload"),
|
reload: () => Effect.die("unused agent.reload"),
|
||||||
},
|
},
|
||||||
@@ -49,11 +50,21 @@ export function host(overrides: Overrides = {}): PluginContext {
|
|||||||
transform: () => Effect.die("unused skill.transform"),
|
transform: () => Effect.die("unused skill.transform"),
|
||||||
reload: () => Effect.die("unused skill.reload"),
|
reload: () => Effect.die("unused skill.reload"),
|
||||||
},
|
},
|
||||||
|
tool: overrides.tool ?? {
|
||||||
|
register: () => Effect.die("unused tool.register"),
|
||||||
|
},
|
||||||
|
session: overrides.session ?? {
|
||||||
|
create: () => Effect.die("unused session.create"),
|
||||||
|
get: () => Effect.die("unused session.get"),
|
||||||
|
prompt: () => Effect.die("unused session.prompt"),
|
||||||
|
interrupt: () => Effect.die("unused session.interrupt"),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function agentHost(agent: AgentV2.Interface): PluginContext["agent"] {
|
export function agentHost(agent: AgentV2.Interface): PluginContext["agent"] {
|
||||||
return {
|
return {
|
||||||
|
list: () => Effect.die("unused agent.list"),
|
||||||
reload: agent.reload,
|
reload: agent.reload,
|
||||||
transform: (callback) =>
|
transform: (callback) =>
|
||||||
agent.transform((draft) =>
|
agent.transform((draft) =>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { describe, expect } from "bun:test"
|
|||||||
import { Tool } from "@opencode-ai/core/tool/tool"
|
import { Tool } from "@opencode-ai/core/tool/tool"
|
||||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||||
import type { PermissionV2 } from "@opencode-ai/core/permission"
|
import type { PermissionV2 } from "@opencode-ai/core/permission"
|
||||||
import { ApplicationTools } from "@opencode-ai/core/tool/application-tools"
|
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||||
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
||||||
@@ -28,9 +27,8 @@ const outputStore = Layer.mock(ToolOutputStore.Service, {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const registry = ToolRegistry.layer.pipe(Layer.provide(ApplicationTools.layer), Layer.provide(outputStore))
|
const registry = ToolRegistry.layer.pipe(Layer.provide(outputStore))
|
||||||
const it = testEffect(registry)
|
const it = testEffect(registry)
|
||||||
const integrated = testEffect(Layer.mergeAll(ApplicationTools.layer, registry))
|
|
||||||
const identity = {
|
const identity = {
|
||||||
agent: AgentV2.ID.make("build"),
|
agent: AgentV2.ID.make("build"),
|
||||||
assistantMessageID: SessionMessage.ID.make("msg_registry"),
|
assistantMessageID: SessionMessage.ID.make("msg_registry"),
|
||||||
@@ -399,38 +397,6 @@ describe("ToolRegistry", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
integrated.effect("rejects an application call after a Location override is registered", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const applications = yield* ApplicationTools.Service
|
|
||||||
const service = yield* ToolRegistry.Service
|
|
||||||
yield* applications.register({ echo: make() })
|
|
||||||
const materialized = yield* service.materialize({ model: testModel })
|
|
||||||
yield* service.register({ echo: make() })
|
|
||||||
|
|
||||||
expect((yield* materialized.settle(call("echo"))).result).toEqual({
|
|
||||||
type: "error",
|
|
||||||
value: "Stale tool call: echo",
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
integrated.effect("rejects a Location call after removal reveals an application registration", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const applications = yield* ApplicationTools.Service
|
|
||||||
const service = yield* ToolRegistry.Service
|
|
||||||
yield* applications.register({ echo: make() })
|
|
||||||
const scope = yield* Scope.make()
|
|
||||||
yield* service.register({ echo: make() }).pipe(Scope.provide(scope))
|
|
||||||
const materialized = yield* service.materialize({ model: testModel })
|
|
||||||
yield* Scope.close(scope, Exit.void)
|
|
||||||
|
|
||||||
expect((yield* materialized.settle(call("echo"))).result).toEqual({
|
|
||||||
type: "error",
|
|
||||||
value: "Stale tool call: echo",
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("keeps captured execution running after registration mutation", () =>
|
it.effect("keeps captured execution running after registration mutation", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const service = yield* ToolRegistry.Service
|
const service = yield* ToolRegistry.Service
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
|||||||
import { SessionRunnerSystemPrompt } from "@opencode-ai/core/session/runner/system-prompt"
|
import { SessionRunnerSystemPrompt } from "@opencode-ai/core/session/runner/system-prompt"
|
||||||
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
||||||
import { ApplicationTools } from "@opencode-ai/core/tool/application-tools"
|
|
||||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||||
import { Config } from "@opencode-ai/core/config"
|
import { Config } from "@opencode-ai/core/config"
|
||||||
import { ConfigCompaction } from "@opencode-ai/core/config/compaction"
|
import { ConfigCompaction } from "@opencode-ai/core/config/compaction"
|
||||||
@@ -124,12 +123,7 @@ const permission = Layer.succeed(
|
|||||||
list: () => Effect.die("unused"),
|
list: () => Effect.die("unused"),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const applications = ApplicationTools.layer
|
const registry = ToolRegistry.layer.pipe(Layer.provide(permission), Layer.provide(ToolOutputStore.defaultLayer))
|
||||||
const registry = ToolRegistry.layer.pipe(
|
|
||||||
Layer.provide(permission),
|
|
||||||
Layer.provide(applications),
|
|
||||||
Layer.provide(ToolOutputStore.defaultLayer),
|
|
||||||
)
|
|
||||||
const agents = AgentV2.layer.pipe(Layer.provide(EventV2.defaultLayer))
|
const agents = AgentV2.layer.pipe(Layer.provide(EventV2.defaultLayer))
|
||||||
const echo = Layer.effectDiscard(
|
const echo = Layer.effectDiscard(
|
||||||
ToolRegistry.Service.use((registry) =>
|
ToolRegistry.Service.use((registry) =>
|
||||||
@@ -286,7 +280,6 @@ const it = testEffect(
|
|||||||
SessionStore.defaultLayer,
|
SessionStore.defaultLayer,
|
||||||
client,
|
client,
|
||||||
permission,
|
permission,
|
||||||
applications,
|
|
||||||
agents,
|
agents,
|
||||||
registry,
|
registry,
|
||||||
echo,
|
echo,
|
||||||
@@ -582,14 +575,14 @@ const verifyPartialFlushOnInterruption = (kind: FragmentKind) =>
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("SessionRunnerLLM", () => {
|
describe("SessionRunnerLLM", () => {
|
||||||
it.effect("advertises and executes a globally attached application tool", () =>
|
it.effect("advertises and executes a location registered tool", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* setup
|
yield* setup
|
||||||
const applicationTools = yield* ApplicationTools.Service
|
const registry = yield* ToolRegistry.Service
|
||||||
const session = yield* SessionV2.Service
|
const session = yield* SessionV2.Service
|
||||||
const contexts: Tool.Context[] = []
|
const contexts: Tool.Context[] = []
|
||||||
yield* applicationTools.register({
|
yield* registry.register({
|
||||||
application_context: Tool.make({
|
location_context: Tool.make({
|
||||||
description: "Read application context",
|
description: "Read application context",
|
||||||
input: Schema.Struct({ query: Schema.String }),
|
input: Schema.Struct({ query: Schema.String }),
|
||||||
output: Schema.Struct({ answer: Schema.String }),
|
output: Schema.Struct({ answer: Schema.String }),
|
||||||
@@ -604,7 +597,7 @@ describe("SessionRunnerLLM", () => {
|
|||||||
responses = [
|
responses = [
|
||||||
[
|
[
|
||||||
LLMEvent.stepStart({ index: 0 }),
|
LLMEvent.stepStart({ index: 0 }),
|
||||||
LLMEvent.toolCall({ id: "call-application", name: "application_context", input: { query: "hello" } }),
|
LLMEvent.toolCall({ id: "call-location", name: "location_context", input: { query: "hello" } }),
|
||||||
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
|
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
|
||||||
LLMEvent.finish({ reason: "tool-calls" }),
|
LLMEvent.finish({ reason: "tool-calls" }),
|
||||||
],
|
],
|
||||||
@@ -613,13 +606,13 @@ describe("SessionRunnerLLM", () => {
|
|||||||
|
|
||||||
yield* session.resume(sessionID)
|
yield* session.resume(sessionID)
|
||||||
|
|
||||||
expect(requests[0]?.tools.map((tool) => tool.name)).toContain("application_context")
|
expect(requests[0]?.tools.map((tool) => tool.name)).toContain("location_context")
|
||||||
expect(contexts).toEqual([
|
expect(contexts).toEqual([
|
||||||
{
|
{
|
||||||
sessionID,
|
sessionID,
|
||||||
agent: AgentV2.ID.make("build"),
|
agent: AgentV2.ID.make("build"),
|
||||||
assistantMessageID: expect.stringMatching(/^msg_/),
|
assistantMessageID: expect.stringMatching(/^msg_/),
|
||||||
toolCallID: "call-application",
|
toolCallID: "call-location",
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
expect(yield* session.context(sessionID)).toMatchObject([
|
expect(yield* session.context(sessionID)).toMatchObject([
|
||||||
@@ -629,7 +622,7 @@ describe("SessionRunnerLLM", () => {
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "tool",
|
type: "tool",
|
||||||
id: "call-application",
|
id: "call-location",
|
||||||
state: { status: "completed", structured: { answer: "HELLO" } },
|
state: { status: "completed", structured: { answer: "HELLO" } },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -914,10 +907,7 @@ describe("SessionRunnerLLM", () => {
|
|||||||
response = fragmentFixture("text", "text-no-system", ["Done"]).completeEvents
|
response = fragmentFixture("text", "text-no-system", ["Done"]).completeEvents
|
||||||
yield* session.resume(sessionID)
|
yield* session.resume(sessionID)
|
||||||
|
|
||||||
expect(requests.at(-1)?.system.map((part) => part.text)).toEqual([
|
expect(requests.at(-1)?.system.map((part) => part.text)).toEqual(["Build agent instructions", "Initial context"])
|
||||||
"Build agent instructions",
|
|
||||||
"Initial context",
|
|
||||||
])
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -24,12 +24,13 @@ import { SessionExecution } from "@opencode-ai/core/session/execution"
|
|||||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||||
|
import { PluginRuntime } from "@opencode-ai/core/plugin/runtime"
|
||||||
import { ShellTool } from "@opencode-ai/core/tool/shell"
|
import { ShellTool } from "@opencode-ai/core/tool/shell"
|
||||||
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
||||||
import { tmpdir } from "./fixture/tmpdir"
|
import { tmpdir } from "./fixture/tmpdir"
|
||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool"
|
import { toolIdentity, executeTool, settleTool, toolDefinitions, waitForTool } from "./lib/tool"
|
||||||
|
|
||||||
const sessionID = SessionV2.ID.make("ses_shell_tool_test")
|
const sessionID = SessionV2.ID.make("ses_shell_tool_test")
|
||||||
const sessionModel = ModelV2.Ref.make({ id: ModelV2.ID.make("test"), providerID: ProviderV2.ID.make("test") })
|
const sessionModel = ModelV2.Ref.make({ id: ModelV2.ID.make("test"), providerID: ProviderV2.ID.make("test") })
|
||||||
@@ -122,7 +123,7 @@ const layer = AppNodeBuilder.build(
|
|||||||
Job.node,
|
Job.node,
|
||||||
ToolOutputStore.cleanupNode,
|
ToolOutputStore.cleanupNode,
|
||||||
SessionV2.node,
|
SessionV2.node,
|
||||||
ShellTool.node,
|
PluginRuntime.providerNode,
|
||||||
LocationServiceMap.node,
|
LocationServiceMap.node,
|
||||||
filesystem,
|
filesystem,
|
||||||
FSUtil.node,
|
FSUtil.node,
|
||||||
@@ -167,6 +168,7 @@ const withSession = <A, E, R>(directory: string, body: (registry: ToolRegistry.I
|
|||||||
const locations = yield* LocationServiceMap.Service
|
const locations = yield* LocationServiceMap.Service
|
||||||
const locationLayer = locations.get(location)
|
const locationLayer = locations.get(location)
|
||||||
const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locationLayer))
|
const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locationLayer))
|
||||||
|
yield* waitForTool(registry, ShellTool.name)
|
||||||
return yield* body(registry).pipe(Effect.provide(locationLayer))
|
return yield* body(registry).pipe(Effect.provide(locationLayer))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -18,12 +18,13 @@ import { SessionExecution } from "@opencode-ai/core/session/execution"
|
|||||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||||
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
||||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||||
|
import { PluginRuntime } from "@opencode-ai/core/plugin/runtime"
|
||||||
import { SubagentTool } from "@opencode-ai/core/tool/subagent"
|
import { SubagentTool } from "@opencode-ai/core/tool/subagent"
|
||||||
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
||||||
import { tmpdir } from "./fixture/tmpdir"
|
import { tmpdir } from "./fixture/tmpdir"
|
||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
import { executeTool, settleTool, testModel, toolIdentity } from "./lib/tool"
|
import { executeTool, settleTool, testModel, toolIdentity, waitForTool } from "./lib/tool"
|
||||||
|
|
||||||
const childText = "child final response"
|
const childText = "child final response"
|
||||||
const childModel = ModelV2.Ref.make({ id: ModelV2.ID.make("child"), providerID: ProviderV2.ID.make("test") })
|
const childModel = ModelV2.Ref.make({ id: ModelV2.ID.make("child"), providerID: ProviderV2.ID.make("test") })
|
||||||
@@ -98,7 +99,7 @@ const layer = AppNodeBuilder.build(
|
|||||||
Job.node,
|
Job.node,
|
||||||
ToolOutputStore.cleanupNode,
|
ToolOutputStore.cleanupNode,
|
||||||
SessionV2.node,
|
SessionV2.node,
|
||||||
SubagentTool.node,
|
PluginRuntime.providerNode,
|
||||||
LocationServiceMap.node,
|
LocationServiceMap.node,
|
||||||
]),
|
]),
|
||||||
SessionExecution.node,
|
SessionExecution.node,
|
||||||
@@ -142,6 +143,7 @@ describe("SubagentTool", () => {
|
|||||||
|
|
||||||
const locations = yield* LocationServiceMap.Service
|
const locations = yield* LocationServiceMap.Service
|
||||||
const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locations.get(parent.location)))
|
const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locations.get(parent.location)))
|
||||||
|
yield* waitForTool(registry, SubagentTool.name)
|
||||||
expect((yield* registry.materialize({ model: testModel })).definitions.map((tool) => tool.name)).toContain(
|
expect((yield* registry.materialize({ model: testModel })).definitions.map((tool) => tool.name)).toContain(
|
||||||
SubagentTool.name,
|
SubagentTool.name,
|
||||||
)
|
)
|
||||||
@@ -175,6 +177,7 @@ describe("SubagentTool", () => {
|
|||||||
yield* withSubagent(parent.location)
|
yield* withSubagent(parent.location)
|
||||||
const locations = yield* LocationServiceMap.Service
|
const locations = yield* LocationServiceMap.Service
|
||||||
const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locations.get(parent.location)))
|
const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locations.get(parent.location)))
|
||||||
|
yield* waitForTool(registry, SubagentTool.name)
|
||||||
|
|
||||||
const settled = yield* settleTool(registry, {
|
const settled = yield* settleTool(registry, {
|
||||||
sessionID: parent.id,
|
sessionID: parent.id,
|
||||||
@@ -226,6 +229,7 @@ describe("SubagentTool", () => {
|
|||||||
yield* withSubagent(parent.location)
|
yield* withSubagent(parent.location)
|
||||||
const locations = yield* LocationServiceMap.Service
|
const locations = yield* LocationServiceMap.Service
|
||||||
const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locations.get(parent.location)))
|
const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locations.get(parent.location)))
|
||||||
|
yield* waitForTool(registry, SubagentTool.name)
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
yield* executeTool(registry, {
|
yield* executeTool(registry, {
|
||||||
@@ -257,6 +261,7 @@ describe("SubagentTool", () => {
|
|||||||
yield* withSubagent(parent.location)
|
yield* withSubagent(parent.location)
|
||||||
const locations = yield* LocationServiceMap.Service
|
const locations = yield* LocationServiceMap.Service
|
||||||
const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locations.get(parent.location)))
|
const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locations.get(parent.location)))
|
||||||
|
yield* waitForTool(registry, SubagentTool.name)
|
||||||
|
|
||||||
const settled = yield* settleTool(registry, {
|
const settled = yield* settleTool(registry, {
|
||||||
sessionID: parent.id,
|
sessionID: parent.id,
|
||||||
|
|||||||
@@ -244,6 +244,16 @@ export function emitEffectImported(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function emitEffectShape(
|
||||||
|
contract: Contract,
|
||||||
|
options: { readonly module: string; readonly api: string },
|
||||||
|
): Output {
|
||||||
|
return {
|
||||||
|
operations: operations(contract.groups),
|
||||||
|
files: [{ path: "api.ts", content: renderEffectShape(contract.groups, options) }],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function emitPromise(
|
export function emitPromise(
|
||||||
contract: Contract,
|
contract: Contract,
|
||||||
options?: {
|
options?: {
|
||||||
@@ -275,6 +285,75 @@ export function emitPromise(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderEffectShape(groups: ReadonlyArray<Group>, options: { readonly module: string; readonly api: string }) {
|
||||||
|
const endpointTypes = groups.map((group, groupIndex) => {
|
||||||
|
const rawGroup = group.endpoints[0]?.topLevel ? "RawClient" : `RawClient[${JSON.stringify(group.sourceIdentifier)}]`
|
||||||
|
const endpoints = group.endpoints.map((endpoint, endpointIndex) => {
|
||||||
|
const prefix = `Endpoint${groupIndex}_${endpointIndex}`
|
||||||
|
const request =
|
||||||
|
endpoint.operation.inputMode === "none"
|
||||||
|
? ""
|
||||||
|
: `type ${prefix}Request = Parameters<${rawGroup}[${JSON.stringify(endpoint.endpoint.name)}]>[0]`
|
||||||
|
const input = endpoint.input
|
||||||
|
.map(
|
||||||
|
(field) =>
|
||||||
|
`readonly ${JSON.stringify(field.name)}${field.optional ? "?" : ""}: ${prefix}Request[${JSON.stringify(field.source)}][${JSON.stringify(field.name)}]`,
|
||||||
|
)
|
||||||
|
.join("; ")
|
||||||
|
const inputType = endpoint.operation.inputMode === "none" ? "" : `export type ${prefix}Input = { ${input} }`
|
||||||
|
const rawOutput = `EffectValue<ReturnType<${rawGroup}[${JSON.stringify(endpoint.endpoint.name)}]>>`
|
||||||
|
const outputType = isStreamSchema(endpoint.successes[0])
|
||||||
|
? `export type ${prefix}Output = StreamValue<${rawOutput}>`
|
||||||
|
: `export type ${prefix}Output = ${endpoint.unwrapData ? `(${rawOutput})["data"]` : rawOutput}`
|
||||||
|
return [
|
||||||
|
request,
|
||||||
|
endpoint.operation.inputMode === "none" ? "" : inputType,
|
||||||
|
outputType,
|
||||||
|
`export type ${groupShapeTypeName(group, endpoint)}<E = never> = (${endpoint.operation.inputMode === "none" ? "" : `input${endpoint.operation.inputMode === "optional" ? "?" : ""}: ${prefix}Input`}) => ${endpoint.operation.success === "stream" ? `Stream.Stream<${prefix}Output, E>` : `Effect.Effect<${prefix}Output, E>`}`,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n")
|
||||||
|
})
|
||||||
|
const methods = group.endpoints
|
||||||
|
.map(
|
||||||
|
(endpoint) => `readonly ${JSON.stringify(endpoint.operation.name)}: ${groupShapeTypeName(group, endpoint)}<E>`,
|
||||||
|
)
|
||||||
|
.join("\n")
|
||||||
|
return `${endpoints.join("\n\n")}\n\nexport interface ${groupShapeName(group)}<E = never> {\n${methods}\n}`
|
||||||
|
})
|
||||||
|
const clientFields = groups.flatMap((group) =>
|
||||||
|
group.endpoints[0]?.topLevel
|
||||||
|
? group.endpoints.map(
|
||||||
|
(endpoint) =>
|
||||||
|
`readonly ${JSON.stringify(endpoint.operation.name)}: ${groupShapeTypeName(group, endpoint)}<E>`,
|
||||||
|
)
|
||||||
|
: [`readonly ${JSON.stringify(group.identifier)}: ${groupShapeName(group)}<E>`],
|
||||||
|
)
|
||||||
|
return `// Generated by @opencode-ai/httpapi-codegen. Do not edit.
|
||||||
|
import type { Effect, Stream } from "effect"
|
||||||
|
import type { HttpApiClient } from "effect/unstable/httpapi"
|
||||||
|
import type { ${options.api} } from ${JSON.stringify(options.module)}
|
||||||
|
|
||||||
|
type RawClient = HttpApiClient.ForApi<typeof ${options.api}>
|
||||||
|
type EffectValue<A> = A extends Effect.Effect<infer Success, any, any> ? Success : never
|
||||||
|
type StreamValue<A> = A extends Stream.Stream<infer Success, any, any> ? Success : never
|
||||||
|
|
||||||
|
${endpointTypes.join("\n\n")}
|
||||||
|
|
||||||
|
export interface AppApi<E = never> {
|
||||||
|
${clientFields.join("\n")}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupShapeName(group: Group) {
|
||||||
|
return `${identifierPart(group.identifier)}Api`
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupShapeTypeName(group: Group, endpoint: Endpoint) {
|
||||||
|
return `${identifierPart(group.identifier)}${identifierPart(endpoint.operation.name)}Operation`
|
||||||
|
}
|
||||||
|
|
||||||
function assertPromiseEndpoint(endpoint: Endpoint) {
|
function assertPromiseEndpoint(endpoint: Endpoint) {
|
||||||
const name = `${endpoint.group}.${endpoint.endpoint.name}`
|
const name = `${endpoint.group}.${endpoint.endpoint.name}`
|
||||||
const payload = endpoint.payloads[0]
|
const payload = endpoint.payloads[0]
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "3.0.8",
|
"@ai-sdk/provider": "3.0.8",
|
||||||
|
"@opencode-ai/protocol": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
"@opentui/core": "catalog:",
|
"@opentui/core": "catalog:",
|
||||||
"@opentui/keymap": "catalog:",
|
"@opentui/keymap": "catalog:",
|
||||||
"@opentui/solid": "catalog:",
|
"@opentui/solid": "catalog:",
|
||||||
|
"@tsconfig/bun": "catalog:",
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Plugin } from "./index.js"
|
import type { Plugin } from "./index.js"
|
||||||
import { tool } from "./tool.js"
|
import { tool } from "./tool.js"
|
||||||
|
|
||||||
export const ExamplePlugin: Plugin = async (_ctx) => {
|
export const ExamplePlugin: Plugin = async (_ctx) => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { AgentV2Info } from "@opencode-ai/sdk/v2/types"
|
import type { AgentV2Info } from "@opencode-ai/sdk/v2/types"
|
||||||
|
import type { AgentApi } from "./generated/api.js"
|
||||||
import type { Hooks } from "./registration.js"
|
import type { Hooks } from "./registration.js"
|
||||||
|
|
||||||
export interface AgentDraft {
|
export interface AgentDraft {
|
||||||
@@ -12,3 +13,6 @@ export interface AgentDraft {
|
|||||||
export type AgentHooks = Hooks<{
|
export type AgentHooks = Hooks<{
|
||||||
transform: AgentDraft
|
transform: AgentDraft
|
||||||
}>
|
}>
|
||||||
|
|
||||||
|
export type AgentPluginApi = AgentHooks
|
||||||
|
export type AgentDomain = AgentApi & AgentPluginApi
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { PluginOptions } from "../options.js"
|
import type { PluginOptions } from "../options.js"
|
||||||
import type { AgentHooks } from "./agent.js"
|
import type { AgentDomain } from "./agent.js"
|
||||||
import type { AISDKHooks } from "./aisdk.js"
|
import type { AISDKHooks } from "./aisdk.js"
|
||||||
import type { CatalogHooks } from "./catalog.js"
|
import type { CatalogHooks } from "./catalog.js"
|
||||||
import type { CommandHooks } from "./command.js"
|
import type { CommandHooks } from "./command.js"
|
||||||
@@ -8,10 +8,12 @@ import type { PluginDomain } from "./plugin.js"
|
|||||||
import type { ReferenceHooks } from "./reference.js"
|
import type { ReferenceHooks } from "./reference.js"
|
||||||
import type { SkillHooks } from "./skill.js"
|
import type { SkillHooks } from "./skill.js"
|
||||||
import type { Reload } from "./registration.js"
|
import type { Reload } from "./registration.js"
|
||||||
|
import type { ToolDomain } from "./tool.js"
|
||||||
|
import type { SessionDomain } from "./runtime.js"
|
||||||
|
|
||||||
export interface PluginContext {
|
export interface PluginContext {
|
||||||
readonly options: PluginOptions
|
readonly options: PluginOptions
|
||||||
readonly agent: AgentHooks & Reload
|
readonly agent: AgentDomain & Reload
|
||||||
readonly aisdk: AISDKHooks
|
readonly aisdk: AISDKHooks
|
||||||
readonly catalog: CatalogHooks & Reload
|
readonly catalog: CatalogHooks & Reload
|
||||||
readonly command: CommandHooks & Reload
|
readonly command: CommandHooks & Reload
|
||||||
@@ -19,4 +21,6 @@ export interface PluginContext {
|
|||||||
readonly plugin: PluginDomain
|
readonly plugin: PluginDomain
|
||||||
readonly reference: ReferenceHooks & Reload
|
readonly reference: ReferenceHooks & Reload
|
||||||
readonly skill: SkillHooks & Reload
|
readonly skill: SkillHooks & Reload
|
||||||
|
readonly tool: ToolDomain
|
||||||
|
readonly session: SessionDomain
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"api.ts"
|
||||||
|
]
|
||||||
@@ -0,0 +1,705 @@
|
|||||||
|
// Generated by @opencode-ai/httpapi-codegen. Do not edit.
|
||||||
|
import type { Effect, Stream } from "effect"
|
||||||
|
import type { HttpApiClient } from "effect/unstable/httpapi"
|
||||||
|
import type { ClientApi } from "@opencode-ai/protocol/client"
|
||||||
|
|
||||||
|
type RawClient = HttpApiClient.ForApi<typeof ClientApi>
|
||||||
|
type EffectValue<A> = A extends Effect.Effect<infer Success, any, any> ? Success : never
|
||||||
|
type StreamValue<A> = A extends Stream.Stream<infer Success, any, any> ? Success : never
|
||||||
|
|
||||||
|
export type Endpoint0_0Output = EffectValue<ReturnType<RawClient["server.health"]["health.get"]>>
|
||||||
|
export type HealthGetOperation<E = never> = () => Effect.Effect<Endpoint0_0Output, E>
|
||||||
|
|
||||||
|
export interface HealthApi<E = never> {
|
||||||
|
readonly get: HealthGetOperation<E>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Endpoint1_0Request = Parameters<RawClient["server.location"]["location.get"]>[0]
|
||||||
|
export type Endpoint1_0Input = { readonly location?: Endpoint1_0Request["query"]["location"] }
|
||||||
|
export type Endpoint1_0Output = EffectValue<ReturnType<RawClient["server.location"]["location.get"]>>
|
||||||
|
export type LocationGetOperation<E = never> = (input?: Endpoint1_0Input) => Effect.Effect<Endpoint1_0Output, E>
|
||||||
|
|
||||||
|
export interface LocationApi<E = never> {
|
||||||
|
readonly get: LocationGetOperation<E>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Endpoint2_0Request = Parameters<RawClient["server.agent"]["agent.list"]>[0]
|
||||||
|
export type Endpoint2_0Input = { readonly location?: Endpoint2_0Request["query"]["location"] }
|
||||||
|
export type Endpoint2_0Output = EffectValue<ReturnType<RawClient["server.agent"]["agent.list"]>>
|
||||||
|
export type AgentListOperation<E = never> = (input?: Endpoint2_0Input) => Effect.Effect<Endpoint2_0Output, E>
|
||||||
|
|
||||||
|
export interface AgentApi<E = never> {
|
||||||
|
readonly list: AgentListOperation<E>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Endpoint3_0Request = Parameters<RawClient["server.session"]["session.list"]>[0]
|
||||||
|
export type Endpoint3_0Input = {
|
||||||
|
readonly workspace?: Endpoint3_0Request["query"]["workspace"]
|
||||||
|
readonly limit?: Endpoint3_0Request["query"]["limit"]
|
||||||
|
readonly order?: Endpoint3_0Request["query"]["order"]
|
||||||
|
readonly search?: Endpoint3_0Request["query"]["search"]
|
||||||
|
readonly directory?: Endpoint3_0Request["query"]["directory"]
|
||||||
|
readonly project?: Endpoint3_0Request["query"]["project"]
|
||||||
|
readonly subpath?: Endpoint3_0Request["query"]["subpath"]
|
||||||
|
readonly cursor?: Endpoint3_0Request["query"]["cursor"]
|
||||||
|
}
|
||||||
|
export type Endpoint3_0Output = EffectValue<ReturnType<RawClient["server.session"]["session.list"]>>
|
||||||
|
export type SessionListOperation<E = never> = (input?: Endpoint3_0Input) => Effect.Effect<Endpoint3_0Output, E>
|
||||||
|
|
||||||
|
type Endpoint3_1Request = Parameters<RawClient["server.session"]["session.create"]>[0]
|
||||||
|
export type Endpoint3_1Input = {
|
||||||
|
readonly id?: Endpoint3_1Request["payload"]["id"]
|
||||||
|
readonly agent?: Endpoint3_1Request["payload"]["agent"]
|
||||||
|
readonly model?: Endpoint3_1Request["payload"]["model"]
|
||||||
|
readonly location?: Endpoint3_1Request["payload"]["location"]
|
||||||
|
}
|
||||||
|
export type Endpoint3_1Output = EffectValue<ReturnType<RawClient["server.session"]["session.create"]>>["data"]
|
||||||
|
export type SessionCreateOperation<E = never> = (input?: Endpoint3_1Input) => Effect.Effect<Endpoint3_1Output, E>
|
||||||
|
|
||||||
|
export type Endpoint3_2Output = EffectValue<ReturnType<RawClient["server.session"]["session.active"]>>["data"]
|
||||||
|
export type SessionActiveOperation<E = never> = () => Effect.Effect<Endpoint3_2Output, E>
|
||||||
|
|
||||||
|
type Endpoint3_3Request = Parameters<RawClient["server.session"]["session.get"]>[0]
|
||||||
|
export type Endpoint3_3Input = { readonly sessionID: Endpoint3_3Request["params"]["sessionID"] }
|
||||||
|
export type Endpoint3_3Output = EffectValue<ReturnType<RawClient["server.session"]["session.get"]>>["data"]
|
||||||
|
export type SessionGetOperation<E = never> = (input: Endpoint3_3Input) => Effect.Effect<Endpoint3_3Output, E>
|
||||||
|
|
||||||
|
type Endpoint3_4Request = Parameters<RawClient["server.session"]["session.fork"]>[0]
|
||||||
|
export type Endpoint3_4Input = {
|
||||||
|
readonly sessionID: Endpoint3_4Request["params"]["sessionID"]
|
||||||
|
readonly messageID?: Endpoint3_4Request["payload"]["messageID"]
|
||||||
|
}
|
||||||
|
export type Endpoint3_4Output = EffectValue<ReturnType<RawClient["server.session"]["session.fork"]>>["data"]
|
||||||
|
export type SessionForkOperation<E = never> = (input: Endpoint3_4Input) => Effect.Effect<Endpoint3_4Output, E>
|
||||||
|
|
||||||
|
type Endpoint3_5Request = Parameters<RawClient["server.session"]["session.switchAgent"]>[0]
|
||||||
|
export type Endpoint3_5Input = {
|
||||||
|
readonly sessionID: Endpoint3_5Request["params"]["sessionID"]
|
||||||
|
readonly agent: Endpoint3_5Request["payload"]["agent"]
|
||||||
|
}
|
||||||
|
export type Endpoint3_5Output = EffectValue<ReturnType<RawClient["server.session"]["session.switchAgent"]>>
|
||||||
|
export type SessionSwitchAgentOperation<E = never> = (input: Endpoint3_5Input) => Effect.Effect<Endpoint3_5Output, E>
|
||||||
|
|
||||||
|
type Endpoint3_6Request = Parameters<RawClient["server.session"]["session.switchModel"]>[0]
|
||||||
|
export type Endpoint3_6Input = {
|
||||||
|
readonly sessionID: Endpoint3_6Request["params"]["sessionID"]
|
||||||
|
readonly model: Endpoint3_6Request["payload"]["model"]
|
||||||
|
}
|
||||||
|
export type Endpoint3_6Output = EffectValue<ReturnType<RawClient["server.session"]["session.switchModel"]>>
|
||||||
|
export type SessionSwitchModelOperation<E = never> = (input: Endpoint3_6Input) => Effect.Effect<Endpoint3_6Output, E>
|
||||||
|
|
||||||
|
type Endpoint3_7Request = Parameters<RawClient["server.session"]["session.rename"]>[0]
|
||||||
|
export type Endpoint3_7Input = {
|
||||||
|
readonly sessionID: Endpoint3_7Request["params"]["sessionID"]
|
||||||
|
readonly title: Endpoint3_7Request["payload"]["title"]
|
||||||
|
}
|
||||||
|
export type Endpoint3_7Output = EffectValue<ReturnType<RawClient["server.session"]["session.rename"]>>
|
||||||
|
export type SessionRenameOperation<E = never> = (input: Endpoint3_7Input) => Effect.Effect<Endpoint3_7Output, E>
|
||||||
|
|
||||||
|
type Endpoint3_8Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
|
||||||
|
export type Endpoint3_8Input = {
|
||||||
|
readonly sessionID: Endpoint3_8Request["params"]["sessionID"]
|
||||||
|
readonly id?: Endpoint3_8Request["payload"]["id"]
|
||||||
|
readonly prompt: Endpoint3_8Request["payload"]["prompt"]
|
||||||
|
readonly delivery?: Endpoint3_8Request["payload"]["delivery"]
|
||||||
|
readonly resume?: Endpoint3_8Request["payload"]["resume"]
|
||||||
|
}
|
||||||
|
export type Endpoint3_8Output = EffectValue<ReturnType<RawClient["server.session"]["session.prompt"]>>["data"]
|
||||||
|
export type SessionPromptOperation<E = never> = (input: Endpoint3_8Input) => Effect.Effect<Endpoint3_8Output, E>
|
||||||
|
|
||||||
|
type Endpoint3_9Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
|
||||||
|
export type Endpoint3_9Input = {
|
||||||
|
readonly sessionID: Endpoint3_9Request["params"]["sessionID"]
|
||||||
|
readonly id?: Endpoint3_9Request["payload"]["id"]
|
||||||
|
readonly skill: Endpoint3_9Request["payload"]["skill"]
|
||||||
|
readonly resume?: Endpoint3_9Request["payload"]["resume"]
|
||||||
|
}
|
||||||
|
export type Endpoint3_9Output = EffectValue<ReturnType<RawClient["server.session"]["session.skill"]>>
|
||||||
|
export type SessionSkillOperation<E = never> = (input: Endpoint3_9Input) => Effect.Effect<Endpoint3_9Output, E>
|
||||||
|
|
||||||
|
type Endpoint3_10Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
|
||||||
|
export type Endpoint3_10Input = { readonly sessionID: Endpoint3_10Request["params"]["sessionID"] }
|
||||||
|
export type Endpoint3_10Output = EffectValue<ReturnType<RawClient["server.session"]["session.compact"]>>
|
||||||
|
export type SessionCompactOperation<E = never> = (input: Endpoint3_10Input) => Effect.Effect<Endpoint3_10Output, E>
|
||||||
|
|
||||||
|
type Endpoint3_11Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
|
||||||
|
export type Endpoint3_11Input = { readonly sessionID: Endpoint3_11Request["params"]["sessionID"] }
|
||||||
|
export type Endpoint3_11Output = EffectValue<ReturnType<RawClient["server.session"]["session.wait"]>>
|
||||||
|
export type SessionWaitOperation<E = never> = (input: Endpoint3_11Input) => Effect.Effect<Endpoint3_11Output, E>
|
||||||
|
|
||||||
|
type Endpoint3_12Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
|
||||||
|
export type Endpoint3_12Input = {
|
||||||
|
readonly sessionID: Endpoint3_12Request["params"]["sessionID"]
|
||||||
|
readonly messageID: Endpoint3_12Request["payload"]["messageID"]
|
||||||
|
readonly files?: Endpoint3_12Request["payload"]["files"]
|
||||||
|
}
|
||||||
|
export type Endpoint3_12Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.stage"]>>["data"]
|
||||||
|
export type SessionRevertStageOperation<E = never> = (input: Endpoint3_12Input) => Effect.Effect<Endpoint3_12Output, E>
|
||||||
|
|
||||||
|
type Endpoint3_13Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
|
||||||
|
export type Endpoint3_13Input = { readonly sessionID: Endpoint3_13Request["params"]["sessionID"] }
|
||||||
|
export type Endpoint3_13Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.clear"]>>
|
||||||
|
export type SessionRevertClearOperation<E = never> = (input: Endpoint3_13Input) => Effect.Effect<Endpoint3_13Output, E>
|
||||||
|
|
||||||
|
type Endpoint3_14Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
|
||||||
|
export type Endpoint3_14Input = { readonly sessionID: Endpoint3_14Request["params"]["sessionID"] }
|
||||||
|
export type Endpoint3_14Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.commit"]>>
|
||||||
|
export type SessionRevertCommitOperation<E = never> = (input: Endpoint3_14Input) => Effect.Effect<Endpoint3_14Output, E>
|
||||||
|
|
||||||
|
type Endpoint3_15Request = Parameters<RawClient["server.session"]["session.context"]>[0]
|
||||||
|
export type Endpoint3_15Input = { readonly sessionID: Endpoint3_15Request["params"]["sessionID"] }
|
||||||
|
export type Endpoint3_15Output = EffectValue<ReturnType<RawClient["server.session"]["session.context"]>>["data"]
|
||||||
|
export type SessionContextOperation<E = never> = (input: Endpoint3_15Input) => Effect.Effect<Endpoint3_15Output, E>
|
||||||
|
|
||||||
|
type Endpoint3_16Request = Parameters<RawClient["server.session"]["session.history"]>[0]
|
||||||
|
export type Endpoint3_16Input = {
|
||||||
|
readonly sessionID: Endpoint3_16Request["params"]["sessionID"]
|
||||||
|
readonly limit?: Endpoint3_16Request["query"]["limit"]
|
||||||
|
readonly after?: Endpoint3_16Request["query"]["after"]
|
||||||
|
}
|
||||||
|
export type Endpoint3_16Output = EffectValue<ReturnType<RawClient["server.session"]["session.history"]>>
|
||||||
|
export type SessionHistoryOperation<E = never> = (input: Endpoint3_16Input) => Effect.Effect<Endpoint3_16Output, E>
|
||||||
|
|
||||||
|
type Endpoint3_17Request = Parameters<RawClient["server.session"]["session.events"]>[0]
|
||||||
|
export type Endpoint3_17Input = {
|
||||||
|
readonly sessionID: Endpoint3_17Request["params"]["sessionID"]
|
||||||
|
readonly after?: Endpoint3_17Request["query"]["after"]
|
||||||
|
}
|
||||||
|
export type Endpoint3_17Output = StreamValue<EffectValue<ReturnType<RawClient["server.session"]["session.events"]>>>
|
||||||
|
export type SessionEventsOperation<E = never> = (input: Endpoint3_17Input) => Stream.Stream<Endpoint3_17Output, E>
|
||||||
|
|
||||||
|
type Endpoint3_18Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
||||||
|
export type Endpoint3_18Input = { readonly sessionID: Endpoint3_18Request["params"]["sessionID"] }
|
||||||
|
export type Endpoint3_18Output = EffectValue<ReturnType<RawClient["server.session"]["session.interrupt"]>>
|
||||||
|
export type SessionInterruptOperation<E = never> = (input: Endpoint3_18Input) => Effect.Effect<Endpoint3_18Output, E>
|
||||||
|
|
||||||
|
type Endpoint3_19Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
||||||
|
export type Endpoint3_19Input = {
|
||||||
|
readonly sessionID: Endpoint3_19Request["params"]["sessionID"]
|
||||||
|
readonly messageID: Endpoint3_19Request["params"]["messageID"]
|
||||||
|
}
|
||||||
|
export type Endpoint3_19Output = EffectValue<ReturnType<RawClient["server.session"]["session.message"]>>["data"]
|
||||||
|
export type SessionMessageOperation<E = never> = (input: Endpoint3_19Input) => Effect.Effect<Endpoint3_19Output, E>
|
||||||
|
|
||||||
|
export interface SessionApi<E = never> {
|
||||||
|
readonly list: SessionListOperation<E>
|
||||||
|
readonly create: SessionCreateOperation<E>
|
||||||
|
readonly active: SessionActiveOperation<E>
|
||||||
|
readonly get: SessionGetOperation<E>
|
||||||
|
readonly fork: SessionForkOperation<E>
|
||||||
|
readonly switchAgent: SessionSwitchAgentOperation<E>
|
||||||
|
readonly switchModel: SessionSwitchModelOperation<E>
|
||||||
|
readonly rename: SessionRenameOperation<E>
|
||||||
|
readonly prompt: SessionPromptOperation<E>
|
||||||
|
readonly skill: SessionSkillOperation<E>
|
||||||
|
readonly compact: SessionCompactOperation<E>
|
||||||
|
readonly wait: SessionWaitOperation<E>
|
||||||
|
readonly revertStage: SessionRevertStageOperation<E>
|
||||||
|
readonly revertClear: SessionRevertClearOperation<E>
|
||||||
|
readonly revertCommit: SessionRevertCommitOperation<E>
|
||||||
|
readonly context: SessionContextOperation<E>
|
||||||
|
readonly history: SessionHistoryOperation<E>
|
||||||
|
readonly events: SessionEventsOperation<E>
|
||||||
|
readonly interrupt: SessionInterruptOperation<E>
|
||||||
|
readonly message: SessionMessageOperation<E>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Endpoint4_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0]
|
||||||
|
export type Endpoint4_0Input = {
|
||||||
|
readonly sessionID: Endpoint4_0Request["params"]["sessionID"]
|
||||||
|
readonly limit?: Endpoint4_0Request["query"]["limit"]
|
||||||
|
readonly order?: Endpoint4_0Request["query"]["order"]
|
||||||
|
readonly cursor?: Endpoint4_0Request["query"]["cursor"]
|
||||||
|
}
|
||||||
|
export type Endpoint4_0Output = EffectValue<ReturnType<RawClient["server.message"]["session.messages"]>>
|
||||||
|
export type MessageListOperation<E = never> = (input: Endpoint4_0Input) => Effect.Effect<Endpoint4_0Output, E>
|
||||||
|
|
||||||
|
export interface MessageApi<E = never> {
|
||||||
|
readonly list: MessageListOperation<E>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Endpoint5_0Request = Parameters<RawClient["server.model"]["model.list"]>[0]
|
||||||
|
export type Endpoint5_0Input = { readonly location?: Endpoint5_0Request["query"]["location"] }
|
||||||
|
export type Endpoint5_0Output = EffectValue<ReturnType<RawClient["server.model"]["model.list"]>>
|
||||||
|
export type ModelListOperation<E = never> = (input?: Endpoint5_0Input) => Effect.Effect<Endpoint5_0Output, E>
|
||||||
|
|
||||||
|
export interface ModelApi<E = never> {
|
||||||
|
readonly list: ModelListOperation<E>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Endpoint6_0Request = Parameters<RawClient["server.generate"]["generate.text"]>[0]
|
||||||
|
export type Endpoint6_0Input = {
|
||||||
|
readonly location?: Endpoint6_0Request["query"]["location"]
|
||||||
|
readonly prompt: Endpoint6_0Request["payload"]["prompt"]
|
||||||
|
readonly model?: Endpoint6_0Request["payload"]["model"]
|
||||||
|
}
|
||||||
|
export type Endpoint6_0Output = EffectValue<ReturnType<RawClient["server.generate"]["generate.text"]>>["data"]
|
||||||
|
export type GenerateTextOperation<E = never> = (input: Endpoint6_0Input) => Effect.Effect<Endpoint6_0Output, E>
|
||||||
|
|
||||||
|
export interface GenerateApi<E = never> {
|
||||||
|
readonly text: GenerateTextOperation<E>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Endpoint7_0Request = Parameters<RawClient["server.provider"]["provider.list"]>[0]
|
||||||
|
export type Endpoint7_0Input = { readonly location?: Endpoint7_0Request["query"]["location"] }
|
||||||
|
export type Endpoint7_0Output = EffectValue<ReturnType<RawClient["server.provider"]["provider.list"]>>
|
||||||
|
export type ProviderListOperation<E = never> = (input?: Endpoint7_0Input) => Effect.Effect<Endpoint7_0Output, E>
|
||||||
|
|
||||||
|
type Endpoint7_1Request = Parameters<RawClient["server.provider"]["provider.get"]>[0]
|
||||||
|
export type Endpoint7_1Input = {
|
||||||
|
readonly providerID: Endpoint7_1Request["params"]["providerID"]
|
||||||
|
readonly location?: Endpoint7_1Request["query"]["location"]
|
||||||
|
}
|
||||||
|
export type Endpoint7_1Output = EffectValue<ReturnType<RawClient["server.provider"]["provider.get"]>>
|
||||||
|
export type ProviderGetOperation<E = never> = (input: Endpoint7_1Input) => Effect.Effect<Endpoint7_1Output, E>
|
||||||
|
|
||||||
|
export interface ProviderApi<E = never> {
|
||||||
|
readonly list: ProviderListOperation<E>
|
||||||
|
readonly get: ProviderGetOperation<E>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Endpoint8_0Request = Parameters<RawClient["server.integration"]["integration.list"]>[0]
|
||||||
|
export type Endpoint8_0Input = { readonly location?: Endpoint8_0Request["query"]["location"] }
|
||||||
|
export type Endpoint8_0Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.list"]>>
|
||||||
|
export type IntegrationListOperation<E = never> = (input?: Endpoint8_0Input) => Effect.Effect<Endpoint8_0Output, E>
|
||||||
|
|
||||||
|
type Endpoint8_1Request = Parameters<RawClient["server.integration"]["integration.get"]>[0]
|
||||||
|
export type Endpoint8_1Input = {
|
||||||
|
readonly integrationID: Endpoint8_1Request["params"]["integrationID"]
|
||||||
|
readonly location?: Endpoint8_1Request["query"]["location"]
|
||||||
|
}
|
||||||
|
export type Endpoint8_1Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.get"]>>
|
||||||
|
export type IntegrationGetOperation<E = never> = (input: Endpoint8_1Input) => Effect.Effect<Endpoint8_1Output, E>
|
||||||
|
|
||||||
|
type Endpoint8_2Request = Parameters<RawClient["server.integration"]["integration.connect.key"]>[0]
|
||||||
|
export type Endpoint8_2Input = {
|
||||||
|
readonly integrationID: Endpoint8_2Request["params"]["integrationID"]
|
||||||
|
readonly location?: Endpoint8_2Request["query"]["location"]
|
||||||
|
readonly key: Endpoint8_2Request["payload"]["key"]
|
||||||
|
readonly label?: Endpoint8_2Request["payload"]["label"]
|
||||||
|
}
|
||||||
|
export type Endpoint8_2Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.connect.key"]>>
|
||||||
|
export type IntegrationConnectKeyOperation<E = never> = (input: Endpoint8_2Input) => Effect.Effect<Endpoint8_2Output, E>
|
||||||
|
|
||||||
|
type Endpoint8_3Request = Parameters<RawClient["server.integration"]["integration.connect.oauth"]>[0]
|
||||||
|
export type Endpoint8_3Input = {
|
||||||
|
readonly integrationID: Endpoint8_3Request["params"]["integrationID"]
|
||||||
|
readonly location?: Endpoint8_3Request["query"]["location"]
|
||||||
|
readonly methodID: Endpoint8_3Request["payload"]["methodID"]
|
||||||
|
readonly inputs: Endpoint8_3Request["payload"]["inputs"]
|
||||||
|
readonly label?: Endpoint8_3Request["payload"]["label"]
|
||||||
|
}
|
||||||
|
export type Endpoint8_3Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.connect.oauth"]>>
|
||||||
|
export type IntegrationConnectOauthOperation<E = never> = (
|
||||||
|
input: Endpoint8_3Input,
|
||||||
|
) => Effect.Effect<Endpoint8_3Output, E>
|
||||||
|
|
||||||
|
type Endpoint8_4Request = Parameters<RawClient["server.integration"]["integration.attempt.status"]>[0]
|
||||||
|
export type Endpoint8_4Input = {
|
||||||
|
readonly attemptID: Endpoint8_4Request["params"]["attemptID"]
|
||||||
|
readonly location?: Endpoint8_4Request["query"]["location"]
|
||||||
|
}
|
||||||
|
export type Endpoint8_4Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.attempt.status"]>>
|
||||||
|
export type IntegrationAttemptStatusOperation<E = never> = (
|
||||||
|
input: Endpoint8_4Input,
|
||||||
|
) => Effect.Effect<Endpoint8_4Output, E>
|
||||||
|
|
||||||
|
type Endpoint8_5Request = Parameters<RawClient["server.integration"]["integration.attempt.complete"]>[0]
|
||||||
|
export type Endpoint8_5Input = {
|
||||||
|
readonly attemptID: Endpoint8_5Request["params"]["attemptID"]
|
||||||
|
readonly location?: Endpoint8_5Request["query"]["location"]
|
||||||
|
readonly code?: Endpoint8_5Request["payload"]["code"]
|
||||||
|
}
|
||||||
|
export type Endpoint8_5Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.attempt.complete"]>>
|
||||||
|
export type IntegrationAttemptCompleteOperation<E = never> = (
|
||||||
|
input: Endpoint8_5Input,
|
||||||
|
) => Effect.Effect<Endpoint8_5Output, E>
|
||||||
|
|
||||||
|
type Endpoint8_6Request = Parameters<RawClient["server.integration"]["integration.attempt.cancel"]>[0]
|
||||||
|
export type Endpoint8_6Input = {
|
||||||
|
readonly attemptID: Endpoint8_6Request["params"]["attemptID"]
|
||||||
|
readonly location?: Endpoint8_6Request["query"]["location"]
|
||||||
|
}
|
||||||
|
export type Endpoint8_6Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.attempt.cancel"]>>
|
||||||
|
export type IntegrationAttemptCancelOperation<E = never> = (
|
||||||
|
input: Endpoint8_6Input,
|
||||||
|
) => Effect.Effect<Endpoint8_6Output, E>
|
||||||
|
|
||||||
|
export interface IntegrationApi<E = never> {
|
||||||
|
readonly list: IntegrationListOperation<E>
|
||||||
|
readonly get: IntegrationGetOperation<E>
|
||||||
|
readonly connectKey: IntegrationConnectKeyOperation<E>
|
||||||
|
readonly connectOauth: IntegrationConnectOauthOperation<E>
|
||||||
|
readonly attemptStatus: IntegrationAttemptStatusOperation<E>
|
||||||
|
readonly attemptComplete: IntegrationAttemptCompleteOperation<E>
|
||||||
|
readonly attemptCancel: IntegrationAttemptCancelOperation<E>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Endpoint9_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
|
||||||
|
export type Endpoint9_0Input = {
|
||||||
|
readonly credentialID: Endpoint9_0Request["params"]["credentialID"]
|
||||||
|
readonly location?: Endpoint9_0Request["query"]["location"]
|
||||||
|
readonly label: Endpoint9_0Request["payload"]["label"]
|
||||||
|
}
|
||||||
|
export type Endpoint9_0Output = EffectValue<ReturnType<RawClient["server.credential"]["credential.update"]>>
|
||||||
|
export type CredentialUpdateOperation<E = never> = (input: Endpoint9_0Input) => Effect.Effect<Endpoint9_0Output, E>
|
||||||
|
|
||||||
|
type Endpoint9_1Request = Parameters<RawClient["server.credential"]["credential.remove"]>[0]
|
||||||
|
export type Endpoint9_1Input = {
|
||||||
|
readonly credentialID: Endpoint9_1Request["params"]["credentialID"]
|
||||||
|
readonly location?: Endpoint9_1Request["query"]["location"]
|
||||||
|
}
|
||||||
|
export type Endpoint9_1Output = EffectValue<ReturnType<RawClient["server.credential"]["credential.remove"]>>
|
||||||
|
export type CredentialRemoveOperation<E = never> = (input: Endpoint9_1Input) => Effect.Effect<Endpoint9_1Output, E>
|
||||||
|
|
||||||
|
export interface CredentialApi<E = never> {
|
||||||
|
readonly update: CredentialUpdateOperation<E>
|
||||||
|
readonly remove: CredentialRemoveOperation<E>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Endpoint10_0Request = Parameters<RawClient["server.project"]["project.current"]>[0]
|
||||||
|
export type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query"]["location"] }
|
||||||
|
export type Endpoint10_0Output = EffectValue<ReturnType<RawClient["server.project"]["project.current"]>>
|
||||||
|
export type ProjectCurrentOperation<E = never> = (input?: Endpoint10_0Input) => Effect.Effect<Endpoint10_0Output, E>
|
||||||
|
|
||||||
|
type Endpoint10_1Request = Parameters<RawClient["server.project"]["project.directories"]>[0]
|
||||||
|
export type Endpoint10_1Input = {
|
||||||
|
readonly projectID: Endpoint10_1Request["params"]["projectID"]
|
||||||
|
readonly location?: Endpoint10_1Request["query"]["location"]
|
||||||
|
}
|
||||||
|
export type Endpoint10_1Output = EffectValue<ReturnType<RawClient["server.project"]["project.directories"]>>
|
||||||
|
export type ProjectDirectoriesOperation<E = never> = (input: Endpoint10_1Input) => Effect.Effect<Endpoint10_1Output, E>
|
||||||
|
|
||||||
|
export interface ProjectApi<E = never> {
|
||||||
|
readonly current: ProjectCurrentOperation<E>
|
||||||
|
readonly directories: ProjectDirectoriesOperation<E>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Endpoint11_0Request = Parameters<RawClient["server.permission"]["permission.request.list"]>[0]
|
||||||
|
export type Endpoint11_0Input = { readonly location?: Endpoint11_0Request["query"]["location"] }
|
||||||
|
export type Endpoint11_0Output = EffectValue<ReturnType<RawClient["server.permission"]["permission.request.list"]>>
|
||||||
|
export type PermissionListRequestsOperation<E = never> = (
|
||||||
|
input?: Endpoint11_0Input,
|
||||||
|
) => Effect.Effect<Endpoint11_0Output, E>
|
||||||
|
|
||||||
|
type Endpoint11_1Request = Parameters<RawClient["server.permission"]["permission.saved.list"]>[0]
|
||||||
|
export type Endpoint11_1Input = { readonly projectID?: Endpoint11_1Request["query"]["projectID"] }
|
||||||
|
export type Endpoint11_1Output = EffectValue<
|
||||||
|
ReturnType<RawClient["server.permission"]["permission.saved.list"]>
|
||||||
|
>["data"]
|
||||||
|
export type PermissionListSavedOperation<E = never> = (
|
||||||
|
input?: Endpoint11_1Input,
|
||||||
|
) => Effect.Effect<Endpoint11_1Output, E>
|
||||||
|
|
||||||
|
type Endpoint11_2Request = Parameters<RawClient["server.permission"]["permission.saved.remove"]>[0]
|
||||||
|
export type Endpoint11_2Input = { readonly id: Endpoint11_2Request["params"]["id"] }
|
||||||
|
export type Endpoint11_2Output = EffectValue<ReturnType<RawClient["server.permission"]["permission.saved.remove"]>>
|
||||||
|
export type PermissionRemoveSavedOperation<E = never> = (
|
||||||
|
input: Endpoint11_2Input,
|
||||||
|
) => Effect.Effect<Endpoint11_2Output, E>
|
||||||
|
|
||||||
|
type Endpoint11_3Request = Parameters<RawClient["server.permission"]["session.permission.create"]>[0]
|
||||||
|
export type Endpoint11_3Input = {
|
||||||
|
readonly sessionID: Endpoint11_3Request["params"]["sessionID"]
|
||||||
|
readonly id?: Endpoint11_3Request["payload"]["id"]
|
||||||
|
readonly action: Endpoint11_3Request["payload"]["action"]
|
||||||
|
readonly resources: Endpoint11_3Request["payload"]["resources"]
|
||||||
|
readonly save?: Endpoint11_3Request["payload"]["save"]
|
||||||
|
readonly metadata?: Endpoint11_3Request["payload"]["metadata"]
|
||||||
|
readonly source?: Endpoint11_3Request["payload"]["source"]
|
||||||
|
readonly agent?: Endpoint11_3Request["payload"]["agent"]
|
||||||
|
}
|
||||||
|
export type Endpoint11_3Output = EffectValue<
|
||||||
|
ReturnType<RawClient["server.permission"]["session.permission.create"]>
|
||||||
|
>["data"]
|
||||||
|
export type PermissionCreateOperation<E = never> = (input: Endpoint11_3Input) => Effect.Effect<Endpoint11_3Output, E>
|
||||||
|
|
||||||
|
type Endpoint11_4Request = Parameters<RawClient["server.permission"]["session.permission.list"]>[0]
|
||||||
|
export type Endpoint11_4Input = { readonly sessionID: Endpoint11_4Request["params"]["sessionID"] }
|
||||||
|
export type Endpoint11_4Output = EffectValue<
|
||||||
|
ReturnType<RawClient["server.permission"]["session.permission.list"]>
|
||||||
|
>["data"]
|
||||||
|
export type PermissionListOperation<E = never> = (input: Endpoint11_4Input) => Effect.Effect<Endpoint11_4Output, E>
|
||||||
|
|
||||||
|
type Endpoint11_5Request = Parameters<RawClient["server.permission"]["session.permission.get"]>[0]
|
||||||
|
export type Endpoint11_5Input = {
|
||||||
|
readonly sessionID: Endpoint11_5Request["params"]["sessionID"]
|
||||||
|
readonly requestID: Endpoint11_5Request["params"]["requestID"]
|
||||||
|
}
|
||||||
|
export type Endpoint11_5Output = EffectValue<
|
||||||
|
ReturnType<RawClient["server.permission"]["session.permission.get"]>
|
||||||
|
>["data"]
|
||||||
|
export type PermissionGetOperation<E = never> = (input: Endpoint11_5Input) => Effect.Effect<Endpoint11_5Output, E>
|
||||||
|
|
||||||
|
type Endpoint11_6Request = Parameters<RawClient["server.permission"]["session.permission.reply"]>[0]
|
||||||
|
export type Endpoint11_6Input = {
|
||||||
|
readonly sessionID: Endpoint11_6Request["params"]["sessionID"]
|
||||||
|
readonly requestID: Endpoint11_6Request["params"]["requestID"]
|
||||||
|
readonly reply: Endpoint11_6Request["payload"]["reply"]
|
||||||
|
readonly message?: Endpoint11_6Request["payload"]["message"]
|
||||||
|
}
|
||||||
|
export type Endpoint11_6Output = EffectValue<ReturnType<RawClient["server.permission"]["session.permission.reply"]>>
|
||||||
|
export type PermissionReplyOperation<E = never> = (input: Endpoint11_6Input) => Effect.Effect<Endpoint11_6Output, E>
|
||||||
|
|
||||||
|
export interface PermissionApi<E = never> {
|
||||||
|
readonly listRequests: PermissionListRequestsOperation<E>
|
||||||
|
readonly listSaved: PermissionListSavedOperation<E>
|
||||||
|
readonly removeSaved: PermissionRemoveSavedOperation<E>
|
||||||
|
readonly create: PermissionCreateOperation<E>
|
||||||
|
readonly list: PermissionListOperation<E>
|
||||||
|
readonly get: PermissionGetOperation<E>
|
||||||
|
readonly reply: PermissionReplyOperation<E>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Endpoint12_0Request = Parameters<RawClient["server.fs"]["fs.list"]>[0]
|
||||||
|
export type Endpoint12_0Input = {
|
||||||
|
readonly location?: Endpoint12_0Request["query"]["location"]
|
||||||
|
readonly path?: Endpoint12_0Request["query"]["path"]
|
||||||
|
}
|
||||||
|
export type Endpoint12_0Output = EffectValue<ReturnType<RawClient["server.fs"]["fs.list"]>>
|
||||||
|
export type FileListOperation<E = never> = (input?: Endpoint12_0Input) => Effect.Effect<Endpoint12_0Output, E>
|
||||||
|
|
||||||
|
type Endpoint12_1Request = Parameters<RawClient["server.fs"]["fs.find"]>[0]
|
||||||
|
export type Endpoint12_1Input = {
|
||||||
|
readonly location?: Endpoint12_1Request["query"]["location"]
|
||||||
|
readonly query: Endpoint12_1Request["query"]["query"]
|
||||||
|
readonly type?: Endpoint12_1Request["query"]["type"]
|
||||||
|
readonly limit?: Endpoint12_1Request["query"]["limit"]
|
||||||
|
}
|
||||||
|
export type Endpoint12_1Output = EffectValue<ReturnType<RawClient["server.fs"]["fs.find"]>>
|
||||||
|
export type FileFindOperation<E = never> = (input: Endpoint12_1Input) => Effect.Effect<Endpoint12_1Output, E>
|
||||||
|
|
||||||
|
export interface FileApi<E = never> {
|
||||||
|
readonly list: FileListOperation<E>
|
||||||
|
readonly find: FileFindOperation<E>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Endpoint13_0Request = Parameters<RawClient["server.command"]["command.list"]>[0]
|
||||||
|
export type Endpoint13_0Input = { readonly location?: Endpoint13_0Request["query"]["location"] }
|
||||||
|
export type Endpoint13_0Output = EffectValue<ReturnType<RawClient["server.command"]["command.list"]>>
|
||||||
|
export type CommandListOperation<E = never> = (input?: Endpoint13_0Input) => Effect.Effect<Endpoint13_0Output, E>
|
||||||
|
|
||||||
|
export interface CommandApi<E = never> {
|
||||||
|
readonly list: CommandListOperation<E>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Endpoint14_0Request = Parameters<RawClient["server.skill"]["skill.list"]>[0]
|
||||||
|
export type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] }
|
||||||
|
export type Endpoint14_0Output = EffectValue<ReturnType<RawClient["server.skill"]["skill.list"]>>
|
||||||
|
export type SkillListOperation<E = never> = (input?: Endpoint14_0Input) => Effect.Effect<Endpoint14_0Output, E>
|
||||||
|
|
||||||
|
export interface SkillApi<E = never> {
|
||||||
|
readonly list: SkillListOperation<E>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Endpoint15_0Output = StreamValue<EffectValue<ReturnType<RawClient["server.event"]["event.subscribe"]>>>
|
||||||
|
export type EventSubscribeOperation<E = never> = () => Stream.Stream<Endpoint15_0Output, E>
|
||||||
|
|
||||||
|
export interface EventApi<E = never> {
|
||||||
|
readonly subscribe: EventSubscribeOperation<E>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Endpoint16_0Request = Parameters<RawClient["server.pty"]["pty.list"]>[0]
|
||||||
|
export type Endpoint16_0Input = { readonly location?: Endpoint16_0Request["query"]["location"] }
|
||||||
|
export type Endpoint16_0Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.list"]>>
|
||||||
|
export type PtyListOperation<E = never> = (input?: Endpoint16_0Input) => Effect.Effect<Endpoint16_0Output, E>
|
||||||
|
|
||||||
|
type Endpoint16_1Request = Parameters<RawClient["server.pty"]["pty.create"]>[0]
|
||||||
|
export type Endpoint16_1Input = {
|
||||||
|
readonly location?: Endpoint16_1Request["query"]["location"]
|
||||||
|
readonly command?: Endpoint16_1Request["payload"]["command"]
|
||||||
|
readonly args?: Endpoint16_1Request["payload"]["args"]
|
||||||
|
readonly cwd?: Endpoint16_1Request["payload"]["cwd"]
|
||||||
|
readonly title?: Endpoint16_1Request["payload"]["title"]
|
||||||
|
readonly env?: Endpoint16_1Request["payload"]["env"]
|
||||||
|
}
|
||||||
|
export type Endpoint16_1Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.create"]>>
|
||||||
|
export type PtyCreateOperation<E = never> = (input?: Endpoint16_1Input) => Effect.Effect<Endpoint16_1Output, E>
|
||||||
|
|
||||||
|
type Endpoint16_2Request = Parameters<RawClient["server.pty"]["pty.get"]>[0]
|
||||||
|
export type Endpoint16_2Input = {
|
||||||
|
readonly ptyID: Endpoint16_2Request["params"]["ptyID"]
|
||||||
|
readonly location?: Endpoint16_2Request["query"]["location"]
|
||||||
|
}
|
||||||
|
export type Endpoint16_2Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.get"]>>
|
||||||
|
export type PtyGetOperation<E = never> = (input: Endpoint16_2Input) => Effect.Effect<Endpoint16_2Output, E>
|
||||||
|
|
||||||
|
type Endpoint16_3Request = Parameters<RawClient["server.pty"]["pty.update"]>[0]
|
||||||
|
export type Endpoint16_3Input = {
|
||||||
|
readonly ptyID: Endpoint16_3Request["params"]["ptyID"]
|
||||||
|
readonly location?: Endpoint16_3Request["query"]["location"]
|
||||||
|
readonly title?: Endpoint16_3Request["payload"]["title"]
|
||||||
|
readonly size?: Endpoint16_3Request["payload"]["size"]
|
||||||
|
}
|
||||||
|
export type Endpoint16_3Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.update"]>>
|
||||||
|
export type PtyUpdateOperation<E = never> = (input: Endpoint16_3Input) => Effect.Effect<Endpoint16_3Output, E>
|
||||||
|
|
||||||
|
type Endpoint16_4Request = Parameters<RawClient["server.pty"]["pty.remove"]>[0]
|
||||||
|
export type Endpoint16_4Input = {
|
||||||
|
readonly ptyID: Endpoint16_4Request["params"]["ptyID"]
|
||||||
|
readonly location?: Endpoint16_4Request["query"]["location"]
|
||||||
|
}
|
||||||
|
export type Endpoint16_4Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.remove"]>>
|
||||||
|
export type PtyRemoveOperation<E = never> = (input: Endpoint16_4Input) => Effect.Effect<Endpoint16_4Output, E>
|
||||||
|
|
||||||
|
export interface PtyApi<E = never> {
|
||||||
|
readonly list: PtyListOperation<E>
|
||||||
|
readonly create: PtyCreateOperation<E>
|
||||||
|
readonly get: PtyGetOperation<E>
|
||||||
|
readonly update: PtyUpdateOperation<E>
|
||||||
|
readonly remove: PtyRemoveOperation<E>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Endpoint17_0Request = Parameters<RawClient["server.shell"]["shell.list"]>[0]
|
||||||
|
export type Endpoint17_0Input = { readonly location?: Endpoint17_0Request["query"]["location"] }
|
||||||
|
export type Endpoint17_0Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.list"]>>
|
||||||
|
export type ShellListOperation<E = never> = (input?: Endpoint17_0Input) => Effect.Effect<Endpoint17_0Output, E>
|
||||||
|
|
||||||
|
type Endpoint17_1Request = Parameters<RawClient["server.shell"]["shell.create"]>[0]
|
||||||
|
export type Endpoint17_1Input = {
|
||||||
|
readonly location?: Endpoint17_1Request["query"]["location"]
|
||||||
|
readonly command: Endpoint17_1Request["payload"]["command"]
|
||||||
|
readonly cwd?: Endpoint17_1Request["payload"]["cwd"]
|
||||||
|
readonly timeout?: Endpoint17_1Request["payload"]["timeout"]
|
||||||
|
readonly metadata?: Endpoint17_1Request["payload"]["metadata"]
|
||||||
|
}
|
||||||
|
export type Endpoint17_1Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.create"]>>
|
||||||
|
export type ShellCreateOperation<E = never> = (input: Endpoint17_1Input) => Effect.Effect<Endpoint17_1Output, E>
|
||||||
|
|
||||||
|
type Endpoint17_2Request = Parameters<RawClient["server.shell"]["shell.get"]>[0]
|
||||||
|
export type Endpoint17_2Input = {
|
||||||
|
readonly id: Endpoint17_2Request["params"]["id"]
|
||||||
|
readonly location?: Endpoint17_2Request["query"]["location"]
|
||||||
|
}
|
||||||
|
export type Endpoint17_2Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.get"]>>
|
||||||
|
export type ShellGetOperation<E = never> = (input: Endpoint17_2Input) => Effect.Effect<Endpoint17_2Output, E>
|
||||||
|
|
||||||
|
type Endpoint17_3Request = Parameters<RawClient["server.shell"]["shell.output"]>[0]
|
||||||
|
export type Endpoint17_3Input = {
|
||||||
|
readonly id: Endpoint17_3Request["params"]["id"]
|
||||||
|
readonly location?: Endpoint17_3Request["query"]["location"]
|
||||||
|
readonly cursor?: Endpoint17_3Request["query"]["cursor"]
|
||||||
|
readonly limit?: Endpoint17_3Request["query"]["limit"]
|
||||||
|
}
|
||||||
|
export type Endpoint17_3Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.output"]>>
|
||||||
|
export type ShellOutputOperation<E = never> = (input: Endpoint17_3Input) => Effect.Effect<Endpoint17_3Output, E>
|
||||||
|
|
||||||
|
type Endpoint17_4Request = Parameters<RawClient["server.shell"]["shell.remove"]>[0]
|
||||||
|
export type Endpoint17_4Input = {
|
||||||
|
readonly id: Endpoint17_4Request["params"]["id"]
|
||||||
|
readonly location?: Endpoint17_4Request["query"]["location"]
|
||||||
|
}
|
||||||
|
export type Endpoint17_4Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.remove"]>>
|
||||||
|
export type ShellRemoveOperation<E = never> = (input: Endpoint17_4Input) => Effect.Effect<Endpoint17_4Output, E>
|
||||||
|
|
||||||
|
export interface ShellApi<E = never> {
|
||||||
|
readonly list: ShellListOperation<E>
|
||||||
|
readonly create: ShellCreateOperation<E>
|
||||||
|
readonly get: ShellGetOperation<E>
|
||||||
|
readonly output: ShellOutputOperation<E>
|
||||||
|
readonly remove: ShellRemoveOperation<E>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Endpoint18_0Request = Parameters<RawClient["server.question"]["question.request.list"]>[0]
|
||||||
|
export type Endpoint18_0Input = { readonly location?: Endpoint18_0Request["query"]["location"] }
|
||||||
|
export type Endpoint18_0Output = EffectValue<ReturnType<RawClient["server.question"]["question.request.list"]>>
|
||||||
|
export type QuestionListRequestsOperation<E = never> = (
|
||||||
|
input?: Endpoint18_0Input,
|
||||||
|
) => Effect.Effect<Endpoint18_0Output, E>
|
||||||
|
|
||||||
|
type Endpoint18_1Request = Parameters<RawClient["server.question"]["session.question.list"]>[0]
|
||||||
|
export type Endpoint18_1Input = { readonly sessionID: Endpoint18_1Request["params"]["sessionID"] }
|
||||||
|
export type Endpoint18_1Output = EffectValue<ReturnType<RawClient["server.question"]["session.question.list"]>>["data"]
|
||||||
|
export type QuestionListOperation<E = never> = (input: Endpoint18_1Input) => Effect.Effect<Endpoint18_1Output, E>
|
||||||
|
|
||||||
|
type Endpoint18_2Request = Parameters<RawClient["server.question"]["session.question.reply"]>[0]
|
||||||
|
export type Endpoint18_2Input = {
|
||||||
|
readonly sessionID: Endpoint18_2Request["params"]["sessionID"]
|
||||||
|
readonly requestID: Endpoint18_2Request["params"]["requestID"]
|
||||||
|
readonly answers: Endpoint18_2Request["payload"]["answers"]
|
||||||
|
}
|
||||||
|
export type Endpoint18_2Output = EffectValue<ReturnType<RawClient["server.question"]["session.question.reply"]>>
|
||||||
|
export type QuestionReplyOperation<E = never> = (input: Endpoint18_2Input) => Effect.Effect<Endpoint18_2Output, E>
|
||||||
|
|
||||||
|
type Endpoint18_3Request = Parameters<RawClient["server.question"]["session.question.reject"]>[0]
|
||||||
|
export type Endpoint18_3Input = {
|
||||||
|
readonly sessionID: Endpoint18_3Request["params"]["sessionID"]
|
||||||
|
readonly requestID: Endpoint18_3Request["params"]["requestID"]
|
||||||
|
}
|
||||||
|
export type Endpoint18_3Output = EffectValue<ReturnType<RawClient["server.question"]["session.question.reject"]>>
|
||||||
|
export type QuestionRejectOperation<E = never> = (input: Endpoint18_3Input) => Effect.Effect<Endpoint18_3Output, E>
|
||||||
|
|
||||||
|
export interface QuestionApi<E = never> {
|
||||||
|
readonly listRequests: QuestionListRequestsOperation<E>
|
||||||
|
readonly list: QuestionListOperation<E>
|
||||||
|
readonly reply: QuestionReplyOperation<E>
|
||||||
|
readonly reject: QuestionRejectOperation<E>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Endpoint19_0Request = Parameters<RawClient["server.reference"]["reference.list"]>[0]
|
||||||
|
export type Endpoint19_0Input = { readonly location?: Endpoint19_0Request["query"]["location"] }
|
||||||
|
export type Endpoint19_0Output = EffectValue<ReturnType<RawClient["server.reference"]["reference.list"]>>
|
||||||
|
export type ReferenceListOperation<E = never> = (input?: Endpoint19_0Input) => Effect.Effect<Endpoint19_0Output, E>
|
||||||
|
|
||||||
|
export interface ReferenceApi<E = never> {
|
||||||
|
readonly list: ReferenceListOperation<E>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Endpoint20_0Request = Parameters<RawClient["server.projectCopy"]["projectCopy.create"]>[0]
|
||||||
|
export type Endpoint20_0Input = {
|
||||||
|
readonly projectID: Endpoint20_0Request["params"]["projectID"]
|
||||||
|
readonly location?: Endpoint20_0Request["query"]["location"]
|
||||||
|
readonly strategy: Endpoint20_0Request["payload"]["strategy"]
|
||||||
|
readonly directory: Endpoint20_0Request["payload"]["directory"]
|
||||||
|
readonly name?: Endpoint20_0Request["payload"]["name"]
|
||||||
|
}
|
||||||
|
export type Endpoint20_0Output = EffectValue<ReturnType<RawClient["server.projectCopy"]["projectCopy.create"]>>
|
||||||
|
export type ProjectCopyCreateOperation<E = never> = (input: Endpoint20_0Input) => Effect.Effect<Endpoint20_0Output, E>
|
||||||
|
|
||||||
|
type Endpoint20_1Request = Parameters<RawClient["server.projectCopy"]["projectCopy.remove"]>[0]
|
||||||
|
export type Endpoint20_1Input = {
|
||||||
|
readonly projectID: Endpoint20_1Request["params"]["projectID"]
|
||||||
|
readonly location?: Endpoint20_1Request["query"]["location"]
|
||||||
|
readonly directory: Endpoint20_1Request["payload"]["directory"]
|
||||||
|
readonly force: Endpoint20_1Request["payload"]["force"]
|
||||||
|
}
|
||||||
|
export type Endpoint20_1Output = EffectValue<ReturnType<RawClient["server.projectCopy"]["projectCopy.remove"]>>
|
||||||
|
export type ProjectCopyRemoveOperation<E = never> = (input: Endpoint20_1Input) => Effect.Effect<Endpoint20_1Output, E>
|
||||||
|
|
||||||
|
type Endpoint20_2Request = Parameters<RawClient["server.projectCopy"]["projectCopy.refresh"]>[0]
|
||||||
|
export type Endpoint20_2Input = {
|
||||||
|
readonly projectID: Endpoint20_2Request["params"]["projectID"]
|
||||||
|
readonly location?: Endpoint20_2Request["query"]["location"]
|
||||||
|
}
|
||||||
|
export type Endpoint20_2Output = EffectValue<ReturnType<RawClient["server.projectCopy"]["projectCopy.refresh"]>>
|
||||||
|
export type ProjectCopyRefreshOperation<E = never> = (input: Endpoint20_2Input) => Effect.Effect<Endpoint20_2Output, E>
|
||||||
|
|
||||||
|
export interface ProjectCopyApi<E = never> {
|
||||||
|
readonly create: ProjectCopyCreateOperation<E>
|
||||||
|
readonly remove: ProjectCopyRemoveOperation<E>
|
||||||
|
readonly refresh: ProjectCopyRefreshOperation<E>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppApi<E = never> {
|
||||||
|
readonly health: HealthApi<E>
|
||||||
|
readonly location: LocationApi<E>
|
||||||
|
readonly agent: AgentApi<E>
|
||||||
|
readonly session: SessionApi<E>
|
||||||
|
readonly message: MessageApi<E>
|
||||||
|
readonly model: ModelApi<E>
|
||||||
|
readonly generate: GenerateApi<E>
|
||||||
|
readonly provider: ProviderApi<E>
|
||||||
|
readonly integration: IntegrationApi<E>
|
||||||
|
readonly credential: CredentialApi<E>
|
||||||
|
readonly project: ProjectApi<E>
|
||||||
|
readonly permission: PermissionApi<E>
|
||||||
|
readonly file: FileApi<E>
|
||||||
|
readonly command: CommandApi<E>
|
||||||
|
readonly skill: SkillApi<E>
|
||||||
|
readonly event: EventApi<E>
|
||||||
|
readonly pty: PtyApi<E>
|
||||||
|
readonly shell: ShellApi<E>
|
||||||
|
readonly question: QuestionApi<E>
|
||||||
|
readonly reference: ReferenceApi<E>
|
||||||
|
readonly projectCopy: ProjectCopyApi<E>
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
export type { PluginContext } from "./context.js"
|
export type { PluginContext } from "./context.js"
|
||||||
export { define } from "./plugin.js"
|
export { define } from "./plugin.js"
|
||||||
export type { Plugin } from "./plugin.js"
|
export type { Plugin } from "./plugin.js"
|
||||||
|
export type { ToolDomain } from "./tool.js"
|
||||||
|
export type { SessionDomain } from "./runtime.js"
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import type { SessionApi } from "./generated/api.js"
|
||||||
|
|
||||||
|
export type SessionDomain = Pick<SessionApi<unknown>, "create" | "get" | "prompt" | "interrupt">
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import type { Effect, Scope } from "effect"
|
||||||
|
|
||||||
|
export interface ToolDomain {
|
||||||
|
readonly register: (tools: Readonly<Record<string, unknown>>) => Effect.Effect<void, unknown, Scope.Scope>
|
||||||
|
}
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/tsconfig.json",
|
"$schema": "https://json.schemastore.org/tsconfig.json",
|
||||||
"extends": "@tsconfig/node22/tsconfig.json",
|
"extends": "@tsconfig/bun/tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"module": "nodenext",
|
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"moduleResolution": "nodenext",
|
|
||||||
"lib": ["es2022", "dom", "dom.iterable"]
|
"lib": ["es2022", "dom", "dom.iterable"]
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { InvalidRequestError, SessionNotFoundError } from "./errors"
|
||||||
|
import { makeDefaultApi } from "./api"
|
||||||
|
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
||||||
|
|
||||||
|
class LocationMiddleware extends HttpApiMiddleware.Service<LocationMiddleware>()(
|
||||||
|
"@opencode-ai/client/LocationMiddleware",
|
||||||
|
) {}
|
||||||
|
|
||||||
|
class SessionLocationMiddleware extends HttpApiMiddleware.Service<SessionLocationMiddleware>()(
|
||||||
|
"@opencode-ai/client/SessionLocationMiddleware",
|
||||||
|
{ error: [InvalidRequestError, SessionNotFoundError] },
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export const ClientApi = makeDefaultApi({
|
||||||
|
locationMiddleware: LocationMiddleware,
|
||||||
|
sessionLocationMiddleware: SessionLocationMiddleware,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const groupNames = {
|
||||||
|
"server.health": "health",
|
||||||
|
"server.location": "location",
|
||||||
|
"server.agent": "agent",
|
||||||
|
"server.session": "session",
|
||||||
|
"server.message": "message",
|
||||||
|
"server.model": "model",
|
||||||
|
"server.generate": "generate",
|
||||||
|
"server.provider": "provider",
|
||||||
|
"server.integration": "integration",
|
||||||
|
"server.credential": "credential",
|
||||||
|
"server.permission": "permission",
|
||||||
|
"server.fs": "file",
|
||||||
|
"server.command": "command",
|
||||||
|
"server.skill": "skill",
|
||||||
|
"server.event": "event",
|
||||||
|
"server.pty": "pty",
|
||||||
|
"server.shell": "shell",
|
||||||
|
"server.question": "question",
|
||||||
|
"server.reference": "reference",
|
||||||
|
"server.project": "project",
|
||||||
|
"server.projectCopy": "projectCopy",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const endpointNames = {
|
||||||
|
"session.messages": "list",
|
||||||
|
"integration.connect.key": "connectKey",
|
||||||
|
"integration.connect.oauth": "connectOauth",
|
||||||
|
"integration.attempt.status": "attemptStatus",
|
||||||
|
"integration.attempt.complete": "attemptComplete",
|
||||||
|
"integration.attempt.cancel": "attemptCancel",
|
||||||
|
"session.revert.stage": "revertStage",
|
||||||
|
"session.revert.clear": "revertClear",
|
||||||
|
"session.revert.commit": "revertCommit",
|
||||||
|
"permission.request.list": "listRequests",
|
||||||
|
"permission.saved.list": "listSaved",
|
||||||
|
"permission.saved.remove": "removeSaved",
|
||||||
|
"question.request.list": "listRequests",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const promiseOmitEndpoints = new Set(["pty.connect", "pty.connectToken"])
|
||||||
|
export const effectOmitEndpoints = new Set(["fs.read", "pty.connect", "pty.connectToken"])
|
||||||
@@ -11,7 +11,7 @@ const opencode = yield * OpenCode.create()
|
|||||||
const session = yield * opencode.sessions.get({ sessionID })
|
const session = yield * opencode.sessions.get({ sessionID })
|
||||||
```
|
```
|
||||||
|
|
||||||
It also exports `Tool` and exposes local-only `tools.register(...)`, replacing the former `@opencode-ai/core/public` facade. Registration uses Core's host-level `ApplicationTools` service shared by the host's Locations; each Location retains its own `ToolRegistry` for overlay, lookup, and settlement. Closing the owning Effect Scope releases router resources, location services, fibers, and scoped tool registrations.
|
It also exports `Tool` for plugins that register tools with `ctx.tool.register(...)`. Embedded plugins run through the ordinary discovery flow and register tools into each Location's `ToolRegistry` through the normal `Tools.Service.register(...)` path. Closing the owning Effect Scope releases router resources, location services, fibers, and scoped tool registrations.
|
||||||
|
|
||||||
`sessions.events({ sessionID, after })` replays durable events after the optional aggregate sequence, then emits newly committed durable events. `sessions.interrupt(...)` targets execution owned by this host, and `sessions.message(...)` retrieves one projected Session message.
|
`sessions.events({ sessionID, after })` replays durable events after the optional aggregate sequence, then emits newly committed durable events. `sessions.interrupt(...)` targets execution owned by this host, and `sessions.message(...)` retrieves one projected Session message.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { OpenCode } from "@opencode-ai/client/effect"
|
import { OpenCode } from "@opencode-ai/client/effect"
|
||||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||||
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
|
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
|
||||||
import { ApplicationTools } from "@opencode-ai/core/tool/application-tools"
|
|
||||||
import { createEmbeddedRoutes } from "@opencode-ai/server/routes"
|
import { createEmbeddedRoutes } from "@opencode-ai/server/routes"
|
||||||
import { Context, Effect, Layer, Scope } from "effect"
|
import { Context, Effect, Layer, Scope } from "effect"
|
||||||
import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http"
|
import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http"
|
||||||
@@ -9,18 +8,18 @@ import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http"
|
|||||||
export const create = Effect.fn("OpenCode.create")(function* () {
|
export const create = Effect.fn("OpenCode.create")(function* () {
|
||||||
const scope = yield* Scope.Scope
|
const scope = yield* Scope.Scope
|
||||||
const memoMap = yield* Layer.makeMemoMap
|
const memoMap = yield* Layer.makeMemoMap
|
||||||
|
const sdkPlugins = SdkPlugins.makeStore()
|
||||||
const context = yield* Layer.buildWithMemoMap(
|
const context = yield* Layer.buildWithMemoMap(
|
||||||
Layer.mergeAll(ApplicationTools.layer, PermissionSaved.defaultLayer, SdkPlugins.layer),
|
Layer.mergeAll(PermissionSaved.defaultLayer, SdkPlugins.layerWithStore(sdkPlugins)),
|
||||||
memoMap,
|
memoMap,
|
||||||
scope,
|
scope,
|
||||||
)
|
)
|
||||||
const tools = Context.get(context, ApplicationTools.Service)
|
|
||||||
const plugins = Context.get(context, SdkPlugins.Service)
|
const plugins = Context.get(context, SdkPlugins.Service)
|
||||||
const permissions = Context.get(context, PermissionSaved.Service)
|
const permissions = Context.get(context, PermissionSaved.Service)
|
||||||
const web = yield* Effect.acquireRelease(
|
const web = yield* Effect.acquireRelease(
|
||||||
Effect.sync(() =>
|
Effect.sync(() =>
|
||||||
HttpRouter.toWebHandler(
|
HttpRouter.toWebHandler(
|
||||||
createEmbeddedRoutes().pipe(
|
createEmbeddedRoutes(sdkPlugins).pipe(
|
||||||
HttpRouter.provideRequest(Layer.succeed(PermissionSaved.Service, permissions)),
|
HttpRouter.provideRequest(Layer.succeed(PermissionSaved.Service, permissions)),
|
||||||
Layer.provide(HttpServer.layerServices),
|
Layer.provide(HttpServer.layerServices),
|
||||||
),
|
),
|
||||||
@@ -38,7 +37,8 @@ export const create = Effect.fn("OpenCode.create")(function* () {
|
|||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
...client,
|
...client,
|
||||||
tools: { register: tools.register },
|
sessions: client.session,
|
||||||
|
events: client.event,
|
||||||
// The embedded host contributes plugins through the ordinary discovery flow:
|
// The embedded host contributes plugins through the ordinary discovery flow:
|
||||||
// each plugin's `effect` runs inside every Location with the real
|
// each plugin's `effect` runs inside every Location with the real
|
||||||
// `PluginContext`, so `ctx.agent.transform` and every other hook behave exactly
|
// `PluginContext`, so `ctx.agent.transform` and every other hook behave exactly
|
||||||
|
|||||||
@@ -1,90 +1,115 @@
|
|||||||
import { expect, test } from "bun:test"
|
import { expect } from "bun:test"
|
||||||
import { mkdtemp, rm } from "node:fs/promises"
|
|
||||||
import { tmpdir } from "node:os"
|
|
||||||
import { join } from "node:path"
|
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
import { Deferred, Effect, Latch, Option, Schema, Stream } from "effect"
|
import { Deferred, Effect, Latch, Layer, Option, Schema, Stream } from "effect"
|
||||||
|
import { testEffect } from "../../core/test/lib/effect"
|
||||||
|
import { tmpdir } from "../../core/test/fixture/tmpdir"
|
||||||
import type { OpenCodeEvent } from "../src"
|
import type { OpenCodeEvent } from "../src"
|
||||||
|
|
||||||
test("embedded client uses the real router and handlers", async () => {
|
Flag.OPENCODE_DB = ":memory:"
|
||||||
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, Provider, Session, Tool } = await import("../src")
|
|
||||||
const sessionID = Session.ID.make(`ses_embedded_${crypto.randomUUID()}`)
|
|
||||||
const model = Model.Ref.make({ id: Model.ID.make("embedded"), providerID: Provider.ID.make("test") })
|
|
||||||
|
|
||||||
try {
|
const it = testEffect(Layer.empty)
|
||||||
const program = Effect.gen(function* () {
|
type Sdk = typeof import("../src")
|
||||||
const opencode = yield* OpenCode.create()
|
type Fixture = { readonly directory: string; readonly sdk: Sdk }
|
||||||
yield* opencode.tools.register({
|
|
||||||
embedded_tool: Tool.make({
|
const withEmbedded = <A, E, R>(prefix: string, f: (fixture: Fixture) => Effect.Effect<A, E, R>) =>
|
||||||
|
Effect.acquireRelease(
|
||||||
|
Effect.promise(() => tmpdir(prefix)),
|
||||||
|
(directory) => Effect.promise(() => directory[Symbol.asyncDispose]()),
|
||||||
|
).pipe(
|
||||||
|
Effect.flatMap((directory) =>
|
||||||
|
Effect.promise(() => import("../src")).pipe(Effect.flatMap((sdk) => f({ directory: directory.path, sdk }))),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const sessionID = (fixture: Fixture) => fixture.sdk.Session.ID.create()
|
||||||
|
|
||||||
|
const location = (fixture: Fixture) =>
|
||||||
|
fixture.sdk.Location.Ref.make({ directory: fixture.sdk.AbsolutePath.make(fixture.directory) })
|
||||||
|
|
||||||
|
it.live(
|
||||||
|
"embedded client uses the real router and handlers",
|
||||||
|
() =>
|
||||||
|
withEmbedded("opencode-embedded-", (fixture) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const opencode = yield* fixture.sdk.OpenCode.create()
|
||||||
|
const id = sessionID(fixture)
|
||||||
|
const model = fixture.sdk.Model.Ref.make({
|
||||||
|
id: fixture.sdk.Model.ID.make("embedded"),
|
||||||
|
providerID: fixture.sdk.Provider.ID.make("test"),
|
||||||
|
})
|
||||||
|
|
||||||
|
yield* opencode.plugin({
|
||||||
|
id: `embedded-tools-${crypto.randomUUID()}`,
|
||||||
|
effect: (ctx) =>
|
||||||
|
ctx.tool
|
||||||
|
.register({
|
||||||
|
embedded_tool: fixture.sdk.Tool.make({
|
||||||
description: "Embedded test tool",
|
description: "Embedded test tool",
|
||||||
input: Schema.Struct({}),
|
input: Schema.Struct({}),
|
||||||
output: Schema.Struct({ ok: Schema.Boolean }),
|
output: Schema.Struct({ ok: Schema.Boolean }),
|
||||||
execute: () => Effect.succeed({ ok: true }),
|
execute: () => Effect.succeed({ ok: true }),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
.pipe(Effect.orDie),
|
||||||
const created = yield* opencode.session.create({
|
|
||||||
id: sessionID,
|
|
||||||
agent: Agent.ID.make("build"),
|
|
||||||
location: Location.Ref.make({ directory: AbsolutePath.make(directory) }),
|
|
||||||
})
|
})
|
||||||
yield* opencode.session.switchModel({ sessionID, model })
|
|
||||||
const selected = yield* opencode.session.get({ sessionID })
|
const created = yield* opencode.sessions.create({
|
||||||
const page = yield* opencode.session.list({ directory: AbsolutePath.make(directory) })
|
id,
|
||||||
const active = yield* opencode.session.active()
|
agent: fixture.sdk.Agent.ID.make("build"),
|
||||||
const admitted = yield* opencode.session.prompt({
|
location: location(fixture),
|
||||||
sessionID,
|
})
|
||||||
prompt: Prompt.make({ text: "Do not run" }),
|
yield* opencode.sessions.switchModel({ sessionID: id, model })
|
||||||
|
const selected = yield* opencode.sessions.get({ sessionID: id })
|
||||||
|
const page = yield* opencode.sessions.list({ directory: fixture.sdk.AbsolutePath.make(fixture.directory) })
|
||||||
|
const active = yield* opencode.sessions.active()
|
||||||
|
const admitted = yield* opencode.sessions.prompt({
|
||||||
|
sessionID: id,
|
||||||
|
prompt: fixture.sdk.Prompt.make({ text: "Do not run" }),
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
const context = yield* opencode.session.context({ sessionID })
|
const context = yield* opencode.sessions.context({ sessionID: id })
|
||||||
const wake = yield* opencode.session.prompt({
|
const wake = yield* opencode.sessions.prompt({
|
||||||
sessionID,
|
sessionID: id,
|
||||||
prompt: Prompt.make({ text: "Promote this input" }),
|
prompt: fixture.sdk.Prompt.make({ text: "Promote this input" }),
|
||||||
})
|
})
|
||||||
const prompted = yield* opencode.session.events({ sessionID }).pipe(
|
const prompted = yield* opencode.sessions.events({ sessionID: id }).pipe(
|
||||||
Stream.filter((event) => event.type === "session.next.prompted" && event.data.messageID === wake.id),
|
Stream.filter((event) => event.type === "session.next.prompted" && event.data.messageID === wake.id),
|
||||||
Stream.runHead,
|
Stream.runHead,
|
||||||
Effect.timeout("10 seconds"),
|
Effect.timeout("10 seconds"),
|
||||||
Effect.map(Option.getOrThrow),
|
Effect.map(Option.getOrThrow),
|
||||||
)
|
)
|
||||||
const wakeContext = yield* opencode.session.context({ sessionID })
|
const wakeContext = yield* opencode.sessions.context({ sessionID: id })
|
||||||
const event = yield* opencode.session
|
const event = yield* opencode.sessions
|
||||||
.events({ sessionID })
|
.events({ sessionID: id })
|
||||||
.pipe(Stream.take(1), Stream.runHead, Effect.map(Option.getOrUndefined))
|
.pipe(Stream.take(1), Stream.runHead, Effect.map(Option.getOrUndefined))
|
||||||
const modelMessage = Option.fromNullishOr(context.find((message) => message.type === "model-switched")).pipe(
|
const modelMessage = Option.fromNullishOr(context.find((message) => message.type === "model-switched")).pipe(
|
||||||
Option.getOrThrow,
|
Option.getOrThrow,
|
||||||
)
|
)
|
||||||
const message = yield* opencode.session.message({ sessionID, messageID: modelMessage.id })
|
const message = yield* opencode.sessions.message({ sessionID: id, messageID: modelMessage.id })
|
||||||
yield* opencode.session.interrupt({ sessionID })
|
yield* opencode.sessions.interrupt({ sessionID: id })
|
||||||
const other = yield* opencode.session.create({
|
const other = yield* opencode.sessions.create({ location: location(fixture) })
|
||||||
location: Location.Ref.make({ directory: AbsolutePath.make(directory) }),
|
const missingSessionID = fixture.sdk.Session.ID.create()
|
||||||
})
|
|
||||||
const missingSessionID = Session.ID.make(`ses_missing_${crypto.randomUUID()}`)
|
|
||||||
const missing = yield* Effect.all(
|
const missing = yield* Effect.all(
|
||||||
[
|
[
|
||||||
opencode.session.events({ sessionID: missingSessionID }).pipe(Stream.runHead, Effect.flip),
|
opencode.sessions.events({ sessionID: missingSessionID }).pipe(Stream.runHead, Effect.flip),
|
||||||
opencode.session.interrupt({ sessionID: missingSessionID }).pipe(Effect.flip),
|
opencode.sessions.interrupt({ sessionID: missingSessionID }).pipe(Effect.flip),
|
||||||
opencode.session.message({ sessionID: missingSessionID, messageID: modelMessage.id }).pipe(Effect.flip),
|
opencode.sessions.message({ sessionID: missingSessionID, messageID: modelMessage.id }).pipe(Effect.flip),
|
||||||
],
|
],
|
||||||
{ concurrency: "unbounded" },
|
{ concurrency: "unbounded" },
|
||||||
)
|
)
|
||||||
const missingMessage = yield* Effect.flip(
|
const missingMessage = yield* Effect.flip(
|
||||||
opencode.session.message({
|
opencode.sessions.message({
|
||||||
sessionID: other.id,
|
sessionID: other.id,
|
||||||
messageID: modelMessage.id,
|
messageID: modelMessage.id,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(created.id).toBe(sessionID)
|
expect(created.id).toBe(id)
|
||||||
expect(selected.model?.id).toBe(model.id)
|
expect(selected.model?.id).toBe(model.id)
|
||||||
expect(selected.model?.providerID).toBe(model.providerID)
|
expect(selected.model?.providerID).toBe(model.providerID)
|
||||||
expect(page.data.some((session) => session.id === sessionID)).toBe(true)
|
expect(page.data.some((session) => session.id === id)).toBe(true)
|
||||||
expect(active).toEqual({})
|
expect(active).toEqual({})
|
||||||
expect(admitted.sessionID).toBe(sessionID)
|
expect(admitted.sessionID).toBe(id)
|
||||||
expect(prompted.type).toBe("session.next.prompted")
|
expect(prompted.type).toBe("session.next.prompted")
|
||||||
expect(wakeContext).toContainEqual(expect.objectContaining({ id: wake.id, type: "user" }))
|
expect(wakeContext).toContainEqual(expect.objectContaining({ id: wake.id, type: "user" }))
|
||||||
expect(context.some((message) => message.type === "model-switched")).toBe(true)
|
expect(context.some((message) => message.type === "model-switched")).toBe(true)
|
||||||
@@ -96,64 +121,53 @@ test("embedded client uses the real router and handlers", async () => {
|
|||||||
"SessionNotFoundError",
|
"SessionNotFoundError",
|
||||||
])
|
])
|
||||||
expect(missingMessage._tag).toBe("MessageNotFoundError")
|
expect(missingMessage._tag).toBe("MessageNotFoundError")
|
||||||
})
|
}),
|
||||||
await Effect.runPromise(Effect.scoped(program))
|
),
|
||||||
} finally {
|
10_000,
|
||||||
Flag.OPENCODE_DB = database
|
)
|
||||||
await rm(directory, { recursive: true, force: true })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Location-owned runner events reach the ready global client", async () => {
|
it.live(
|
||||||
const directory = await mkdtemp(join(tmpdir(), "opencode-embedded-events-"))
|
"Location-owned runner events reach the ready global client",
|
||||||
const database = Flag.OPENCODE_DB
|
() =>
|
||||||
Flag.OPENCODE_DB = join(directory, "opencode.sqlite")
|
withEmbedded("opencode-embedded-events-", (fixture) =>
|
||||||
const { AbsolutePath, Location, OpenCode, Prompt, Session } = await import("../src")
|
Effect.gen(function* () {
|
||||||
const sessionID = Session.ID.make(`ses_embedded_${crypto.randomUUID()}`)
|
const opencode = yield* fixture.sdk.OpenCode.create()
|
||||||
|
const id = sessionID(fixture)
|
||||||
try {
|
|
||||||
const program = Effect.gen(function* () {
|
|
||||||
const opencode = yield* OpenCode.create()
|
|
||||||
const connected = yield* Latch.make(false)
|
const connected = yield* Latch.make(false)
|
||||||
const prompted = yield* Deferred.make<OpenCodeEvent>()
|
const prompted = yield* Deferred.make<OpenCodeEvent>()
|
||||||
yield* opencode.event.subscribe().pipe(
|
|
||||||
|
yield* opencode.events.subscribe().pipe(
|
||||||
Stream.runForEach((event) =>
|
Stream.runForEach((event) =>
|
||||||
event.type === "server.connected"
|
event.type === "server.connected"
|
||||||
? connected.open
|
? connected.open
|
||||||
: event.type === "session.next.prompted" && event.data.sessionID === sessionID
|
: event.type === "session.next.prompted" && event.data.sessionID === id
|
||||||
? Deferred.succeed(prompted, event).pipe(Effect.asVoid)
|
? Deferred.succeed(prompted, event).pipe(Effect.asVoid)
|
||||||
: Effect.void,
|
: Effect.void,
|
||||||
),
|
),
|
||||||
Effect.forkScoped,
|
Effect.forkScoped,
|
||||||
)
|
)
|
||||||
yield* connected.await
|
yield* connected.await
|
||||||
yield* opencode.session.create({
|
yield* opencode.sessions.create({ id, location: location(fixture) })
|
||||||
id: sessionID,
|
yield* opencode.sessions.prompt({
|
||||||
location: Location.Ref.make({ directory: AbsolutePath.make(directory) }),
|
sessionID: id,
|
||||||
|
prompt: fixture.sdk.Prompt.make({ text: "Observe this input" }),
|
||||||
})
|
})
|
||||||
yield* opencode.session.prompt({ sessionID, prompt: Prompt.make({ text: "Observe this input" }) })
|
|
||||||
|
|
||||||
const event = yield* Deferred.await(prompted).pipe(Effect.timeout("4 seconds"))
|
const event = yield* Deferred.await(prompted).pipe(Effect.timeout("4 seconds"))
|
||||||
expect(event.durable).toEqual(expect.objectContaining({ aggregateID: sessionID, seq: expect.any(Number) }))
|
expect(event.durable).toEqual(expect.objectContaining({ aggregateID: id, seq: expect.any(Number) }))
|
||||||
})
|
}),
|
||||||
await Effect.runPromise(Effect.scoped(program))
|
),
|
||||||
} finally {
|
10_000,
|
||||||
Flag.OPENCODE_DB = database
|
)
|
||||||
await rm(directory, { recursive: true, force: true })
|
|
||||||
}
|
|
||||||
}, 10_000)
|
|
||||||
|
|
||||||
test("independent embedded hosts do not share live notifications", async () => {
|
it.live(
|
||||||
const directory = await mkdtemp(join(tmpdir(), "opencode-embedded-hosts-"))
|
"independent embedded hosts do not share live notifications",
|
||||||
const database = Flag.OPENCODE_DB
|
() =>
|
||||||
Flag.OPENCODE_DB = join(directory, "opencode.sqlite")
|
withEmbedded("opencode-embedded-hosts-", (fixture) =>
|
||||||
const { AbsolutePath, Agent, Location, OpenCode, Session } = await import("../src")
|
Effect.gen(function* () {
|
||||||
const sessionID = Session.ID.make(`ses_embedded_${crypto.randomUUID()}`)
|
const first = yield* fixture.sdk.OpenCode.create()
|
||||||
|
const second = yield* fixture.sdk.OpenCode.create()
|
||||||
try {
|
const id = sessionID(fixture)
|
||||||
const program = Effect.gen(function* () {
|
|
||||||
const first = yield* OpenCode.create()
|
|
||||||
const second = yield* OpenCode.create()
|
|
||||||
const firstReady = yield* Latch.make(false)
|
const firstReady = yield* Latch.make(false)
|
||||||
const secondReady = yield* Latch.make(false)
|
const secondReady = yield* Latch.make(false)
|
||||||
const firstEvent = yield* Latch.make(false)
|
const firstEvent = yield* Latch.make(false)
|
||||||
@@ -162,51 +176,31 @@ test("independent embedded hosts do not share live notifications", async () => {
|
|||||||
Stream.runForEach((notification: OpenCodeEvent) =>
|
Stream.runForEach((notification: OpenCodeEvent) =>
|
||||||
notification.type === "server.connected"
|
notification.type === "server.connected"
|
||||||
? ready.open
|
? ready.open
|
||||||
: notification.type === "session.next.agent.switched" && notification.data.sessionID === sessionID
|
: notification.type === "session.next.agent.switched" && notification.data.sessionID === id
|
||||||
? event.open
|
? event.open
|
||||||
: Effect.void,
|
: Effect.void,
|
||||||
)
|
)
|
||||||
|
|
||||||
yield* first.event.subscribe().pipe(observe(firstReady, firstEvent), Effect.forkScoped)
|
yield* first.events.subscribe().pipe(observe(firstReady, firstEvent), Effect.forkScoped)
|
||||||
yield* second.event.subscribe().pipe(observe(secondReady, secondEvent), Effect.forkScoped)
|
yield* second.events.subscribe().pipe(observe(secondReady, secondEvent), Effect.forkScoped)
|
||||||
yield* Effect.all([firstReady.await, secondReady.await], { discard: true })
|
yield* Effect.all([firstReady.await, secondReady.await], { discard: true })
|
||||||
yield* first.session.create({
|
yield* first.sessions.create({ id, location: location(fixture) })
|
||||||
id: sessionID,
|
yield* first.sessions.switchAgent({ sessionID: id, agent: fixture.sdk.Agent.ID.make("plan") })
|
||||||
location: Location.Ref.make({ directory: AbsolutePath.make(directory) }),
|
|
||||||
})
|
|
||||||
yield* first.session.switchAgent({ sessionID, agent: Agent.ID.make("plan") })
|
|
||||||
|
|
||||||
yield* firstEvent.await.pipe(Effect.timeout("2 seconds"))
|
yield* firstEvent.await.pipe(Effect.timeout("2 seconds"))
|
||||||
expect(Option.isNone(yield* secondEvent.await.pipe(Effect.timeoutOption("100 millis")))).toBe(true)
|
expect(Option.isNone(yield* secondEvent.await.pipe(Effect.timeoutOption("100 millis")))).toBe(true)
|
||||||
})
|
}),
|
||||||
await Effect.runPromise(Effect.scoped(program))
|
),
|
||||||
} finally {
|
10_000,
|
||||||
Flag.OPENCODE_DB = database
|
)
|
||||||
await rm(directory, { recursive: true, force: true })
|
|
||||||
}
|
|
||||||
}, 10_000)
|
|
||||||
|
|
||||||
test("embedded client is available as a Layer service", async () => {
|
it.live("embedded client is available as a Layer service", () =>
|
||||||
const directory = await mkdtemp(join(tmpdir(), "opencode-embedded-layer-"))
|
withEmbedded("opencode-embedded-layer-", (fixture) => {
|
||||||
const database = Flag.OPENCODE_DB
|
const id = sessionID(fixture)
|
||||||
Flag.OPENCODE_DB = join(directory, "opencode.sqlite")
|
return Effect.gen(function* () {
|
||||||
const { AbsolutePath, Location, OpenCode, Session } = await import("../src")
|
const opencode = yield* fixture.sdk.OpenCode.Service
|
||||||
const sessionID = Session.ID.make(`ses_embedded_${crypto.randomUUID()}`)
|
const created = yield* opencode.sessions.create({ id, location: location(fixture) })
|
||||||
|
expect(created.id).toBe(id)
|
||||||
try {
|
}).pipe(Effect.provide(fixture.sdk.OpenCode.layer))
|
||||||
const created = await Effect.runPromise(
|
}),
|
||||||
Effect.gen(function* () {
|
)
|
||||||
const opencode = yield* OpenCode.Service
|
|
||||||
return yield* opencode.session.create({
|
|
||||||
id: sessionID,
|
|
||||||
location: Location.Ref.make({ directory: AbsolutePath.make(directory) }),
|
|
||||||
})
|
|
||||||
}).pipe(Effect.provide(OpenCode.layer), Effect.scoped),
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(created.id).toBe(sessionID)
|
|
||||||
} finally {
|
|
||||||
Flag.OPENCODE_DB = database
|
|
||||||
await rm(directory, { recursive: true, force: true })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { response } from "../location"
|
|||||||
export const AgentHandler = HttpApiBuilder.group(Api, "server.agent", (handlers) =>
|
export const AgentHandler = HttpApiBuilder.group(Api, "server.agent", (handlers) =>
|
||||||
handlers.handle("agent.list", () =>
|
handlers.handle("agent.list", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
return yield* response(AgentV2.Service.use((agent) => agent.all()))
|
return yield* response(AgentV2.Service.use((agent) => agent.list()))
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import { SessionV2 } from "@opencode-ai/core/session"
|
|||||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||||
import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
|
import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
|
||||||
import { SessionExecutionLocal } from "@opencode-ai/core/session/execution/local"
|
import { SessionExecutionLocal } from "@opencode-ai/core/session/execution/local"
|
||||||
import { SubagentTool } from "@opencode-ai/core/tool/subagent"
|
import { PluginRuntime } from "@opencode-ai/core/plugin/runtime"
|
||||||
import { ShellTool } from "@opencode-ai/core/tool/shell"
|
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
|
||||||
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
||||||
import { HttpRouter, HttpServer } from "effect/unstable/http"
|
import { HttpRouter, HttpServer } from "effect/unstable/http"
|
||||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||||
@@ -31,8 +31,7 @@ const applicationServices = LayerNode.group([
|
|||||||
httpClient,
|
httpClient,
|
||||||
ToolOutputStore.cleanupNode,
|
ToolOutputStore.cleanupNode,
|
||||||
SessionV2.node,
|
SessionV2.node,
|
||||||
SubagentTool.node,
|
PluginRuntime.providerNode,
|
||||||
ShellTool.node,
|
|
||||||
PermissionSaved.node,
|
PermissionSaved.node,
|
||||||
PtyTicket.node,
|
PtyTicket.node,
|
||||||
Credential.node,
|
Credential.node,
|
||||||
@@ -48,13 +47,22 @@ export function createRoutes(password?: string) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createEmbeddedRoutes() {
|
export function createEmbeddedRoutes(sdkPlugins?: SdkPlugins.Store) {
|
||||||
return makeRoutes(ServerAuth.Config.layer({ username: "opencode", password: Option.none() }))
|
return makeRoutes(ServerAuth.Config.layer({ username: "opencode", password: Option.none() }), sdkPlugins)
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeRoutes<AuthError, AuthServices>(auth: Layer.Layer<ServerAuth.Config, AuthError, AuthServices>) {
|
function makeRoutes<AuthError, AuthServices>(
|
||||||
|
auth: Layer.Layer<ServerAuth.Config, AuthError, AuthServices>,
|
||||||
|
sdkPlugins?: SdkPlugins.Store,
|
||||||
|
) {
|
||||||
|
const pluginRuntimeCell = PluginRuntime.makeCell()
|
||||||
const serviceLayer = AppNodeBuilder.build(
|
const serviceLayer = AppNodeBuilder.build(
|
||||||
LayerNode.bind(applicationServices, SessionExecution.node, SessionExecutionLocal.node),
|
LayerNode.bind(applicationServices, SessionExecution.node, SessionExecutionLocal.node),
|
||||||
|
[
|
||||||
|
LayerNode.replace(PluginRuntime.layer, PluginRuntime.layerWithCell(pluginRuntimeCell)),
|
||||||
|
LayerNode.replace(PluginRuntime.providerLayer, PluginRuntime.providerLayerWithCell(pluginRuntimeCell)),
|
||||||
|
...(sdkPlugins ? [LayerNode.replace(SdkPlugins.layer, SdkPlugins.layerWithStore(sdkPlugins))] : []),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
return HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe(
|
return HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe(
|
||||||
|
|||||||
@@ -1143,7 +1143,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (session?.revert) {
|
if (session?.revert) {
|
||||||
const error = await sdk.api.session.commit({ sessionID }).then(
|
const error = await sdk.api.session.revertCommit({ sessionID }).then(
|
||||||
() => undefined,
|
() => undefined,
|
||||||
(error) => error,
|
(error) => error,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export function DialogMessage(props: { messageID: string; sessionID: string; set
|
|||||||
description: "undo messages and file changes",
|
description: "undo messages and file changes",
|
||||||
onSelect: async (dialog) => {
|
onSelect: async (dialog) => {
|
||||||
await sdk.api.session
|
await sdk.api.session
|
||||||
.stage({ sessionID: props.sessionID, messageID: props.messageID })
|
.revertStage({ sessionID: props.sessionID, messageID: props.messageID })
|
||||||
.catch((error) => toast.show({ message: errorMessage(error), variant: "error", duration: 5000 }))
|
.catch((error) => toast.show({ message: errorMessage(error), variant: "error", duration: 5000 }))
|
||||||
dialog.clear()
|
dialog.clear()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -185,7 +185,9 @@ export function Session() {
|
|||||||
})
|
})
|
||||||
const permissions = createMemo(() => {
|
const permissions = createMemo(() => {
|
||||||
if (session()?.parentID) return []
|
if (session()?.parentID) return []
|
||||||
return [route.sessionID, ...descendantSessionIDs()].flatMap((sessionID) => data.session.permission.list(sessionID) ?? [])
|
return [route.sessionID, ...descendantSessionIDs()].flatMap(
|
||||||
|
(sessionID) => data.session.permission.list(sessionID) ?? [],
|
||||||
|
)
|
||||||
})
|
})
|
||||||
const questions = createMemo(() => {
|
const questions = createMemo(() => {
|
||||||
if (session()?.parentID) return []
|
if (session()?.parentID) return []
|
||||||
@@ -422,7 +424,7 @@ export function Session() {
|
|||||||
dialog.clear()
|
dialog.clear()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const error = await sdk.api.session.stage({ sessionID: route.sessionID, messageID: target }).then(
|
const error = await sdk.api.session.revertStage({ sessionID: route.sessionID, messageID: target }).then(
|
||||||
() => undefined,
|
() => undefined,
|
||||||
(error) => error,
|
(error) => error,
|
||||||
)
|
)
|
||||||
@@ -439,7 +441,7 @@ export function Session() {
|
|||||||
slash: { name: "redo" },
|
slash: { name: "redo" },
|
||||||
run: () => {
|
run: () => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const error = await sdk.api.session.clear({ sessionID: route.sessionID }).then(
|
const error = await sdk.api.session.revertClear({ sessionID: route.sessionID }).then(
|
||||||
() => undefined,
|
() => undefined,
|
||||||
(error) => error,
|
(error) => error,
|
||||||
)
|
)
|
||||||
@@ -1049,7 +1051,9 @@ function SessionMessageView(props: { message: SessionMessage }) {
|
|||||||
<Match when={props.message.type === "agent-switched" || props.message.type === "model-switched"}>
|
<Match when={props.message.type === "agent-switched" || props.message.type === "model-switched"}>
|
||||||
<SessionSwitchMessageV2 message={props.message} />
|
<SessionSwitchMessageV2 message={props.message} />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.message.type === "system" || props.message.type === "synthetic" || props.message.type === "skill"}>
|
<Match
|
||||||
|
when={props.message.type === "system" || props.message.type === "synthetic" || props.message.type === "skill"}
|
||||||
|
>
|
||||||
<Show when={props.message.type === "skill"} fallback={<SessionNoticeMessageV2 message={props.message} />}>
|
<Show when={props.message.type === "skill"} fallback={<SessionNoticeMessageV2 message={props.message} />}>
|
||||||
<SessionSkillMessage message={props.message as Extract<SessionMessage, { type: "skill" }>} />
|
<SessionSkillMessage message={props.message as Extract<SessionMessage, { type: "skill" }>} />
|
||||||
</Show>
|
</Show>
|
||||||
@@ -1217,11 +1221,7 @@ function SessionNoticeMessageV2(props: { message: SessionMessage }) {
|
|||||||
if (props.message.type === "system" || props.message.type === "synthetic") return props.message.text
|
if (props.message.type === "system" || props.message.type === "synthetic") return props.message.text
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return (
|
return <text fg={theme.textMuted}>{text()}</text>
|
||||||
<text fg={theme.textMuted}>
|
|
||||||
{text()}
|
|
||||||
</text>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function SessionSkillMessage(props: { message: Extract<SessionMessage, { type: "skill" }> }) {
|
function SessionSkillMessage(props: { message: Extract<SessionMessage, { type: "skill" }> }) {
|
||||||
@@ -1267,7 +1267,7 @@ function RevertMessage(props: {
|
|||||||
onMouseUp={() => {
|
onMouseUp={() => {
|
||||||
if (renderer.getSelection()?.getSelectedText()) return
|
if (renderer.getSelection()?.getSelectedText()) return
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const error = await sdk.api.session.clear({ sessionID: route.sessionID }).then(
|
const error = await sdk.api.session.revertClear({ sessionID: route.sessionID }).then(
|
||||||
() => undefined,
|
() => undefined,
|
||||||
(error) => error,
|
(error) => error,
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -186,7 +186,7 @@ Event replay owner claims are separate from clustered Session execution ownershi
|
|||||||
|
|
||||||
## Current Tool Registry Slice
|
## Current Tool Registry Slice
|
||||||
|
|
||||||
`ApplicationTools` stores process-scoped application registrations shared by all Locations. Each Location-scoped `ToolRegistry` overlays Location registrations, materializes definitions, and owns lookup and settlement. Closing a contribution scope removes its definition and rebuilds the advertised catalog. Trusted tool executors capture and perform authorization; the registry applies catalog visibility filtering, decodes input, invokes the retained handler, validates output, and settles failures as typed tool-result errors.
|
Each Location-scoped `ToolRegistry` stores scoped tool registrations, materializes definitions, and owns lookup and settlement. Built-ins and plugins contribute through the same `Tools.Service.register(...)` path. Closing a contribution scope removes its definition and rebuilds the advertised catalog. Trusted tool executors capture and perform authorization; the registry applies catalog visibility filtering, decodes input, invokes the retained handler, validates output, and settles failures as typed tool-result errors.
|
||||||
|
|
||||||
When a Session omits `agent`, both execution and permission evaluation use the default `build` agent. A caller must not observe `build` model behavior while permission checks silently evaluate an empty no-agent policy.
|
When a Session omits `agent`, both execution and permission evaluation use the default `build` agent. A caller must not observe `build` model behavior while permission checks silently evaluate an empty no-agent policy.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user