mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-27 21:51:49 -04:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9609670544 | |||
| e1abf59c59 | |||
| 4f976bcf1a | |||
| e25a724bc2 | |||
| 521ea87192 |
@@ -3,15 +3,14 @@ import { compile, emitEffectImported, emitEffectShape, emitPromise, write } from
|
||||
import {
|
||||
ClientApi,
|
||||
effectOmitEndpoints,
|
||||
endpointNames,
|
||||
groupNames,
|
||||
promiseOmitEndpoints,
|
||||
} from "@opencode-ai/protocol/client"
|
||||
import { Effect } from "effect"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
const promiseContract = compile(ClientApi, { groupNames, endpointNames, omitEndpoints: promiseOmitEndpoints })
|
||||
const effectContract = compile(ClientApi, { groupNames, endpointNames, omitEndpoints: effectOmitEndpoints })
|
||||
const promiseContract = compile(ClientApi, { groupNames, omitEndpoints: promiseOmitEndpoints })
|
||||
const effectContract = compile(ClientApi, { groupNames, omitEndpoints: effectOmitEndpoints })
|
||||
|
||||
await Effect.runPromise(
|
||||
Effect.all(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export {
|
||||
ClientApi,
|
||||
effectOmitEndpoints,
|
||||
endpointNames,
|
||||
groupNames,
|
||||
promiseOmitEndpoints,
|
||||
} from "@opencode-ai/protocol/client"
|
||||
|
||||
@@ -291,9 +291,11 @@ export interface SessionApi<E = never> {
|
||||
readonly shell: SessionShellOperation<E>
|
||||
readonly compact: SessionCompactOperation<E>
|
||||
readonly wait: SessionWaitOperation<E>
|
||||
readonly revertStage: SessionRevertStageOperation<E>
|
||||
readonly revertClear: SessionRevertClearOperation<E>
|
||||
readonly revertCommit: SessionRevertCommitOperation<E>
|
||||
readonly revert: {
|
||||
readonly stage: SessionRevertStageOperation<E>
|
||||
readonly clear: SessionRevertClearOperation<E>
|
||||
readonly commit: SessionRevertCommitOperation<E>
|
||||
}
|
||||
readonly context: SessionContextOperation<E>
|
||||
readonly instructions: {
|
||||
readonly entry: {
|
||||
@@ -438,11 +440,15 @@ export type IntegrationAttemptCancelOperation<E = never> = (
|
||||
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>
|
||||
readonly connect: {
|
||||
readonly key: IntegrationConnectKeyOperation<E>
|
||||
readonly oauth: IntegrationConnectOauthOperation<E>
|
||||
}
|
||||
readonly attempt: {
|
||||
readonly status: IntegrationAttemptStatusOperation<E>
|
||||
readonly complete: IntegrationAttemptCompleteOperation<E>
|
||||
readonly cancel: IntegrationAttemptCancelOperation<E>
|
||||
}
|
||||
}
|
||||
|
||||
type Endpoint10_0Request = Parameters<RawClient["server.mcp"]["mcp.list"]>[0]
|
||||
@@ -453,11 +459,13 @@ export type ServerMcpListOperation<E = never> = (input?: Endpoint10_0Input) => E
|
||||
type Endpoint10_1Request = Parameters<RawClient["server.mcp"]["mcp.resource.catalog"]>[0]
|
||||
export type Endpoint10_1Input = { readonly location?: Endpoint10_1Request["query"]["location"] }
|
||||
export type Endpoint10_1Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.resource.catalog"]>>
|
||||
export type ServerMcpCatalogOperation<E = never> = (input?: Endpoint10_1Input) => Effect.Effect<Endpoint10_1Output, E>
|
||||
export type ServerMcpResourceCatalogOperation<E = never> = (
|
||||
input?: Endpoint10_1Input,
|
||||
) => Effect.Effect<Endpoint10_1Output, E>
|
||||
|
||||
export interface ServerMcpApi<E = never> {
|
||||
readonly list: ServerMcpListOperation<E>
|
||||
readonly catalog: ServerMcpCatalogOperation<E>
|
||||
readonly resource: { readonly catalog: ServerMcpResourceCatalogOperation<E> }
|
||||
}
|
||||
|
||||
type Endpoint11_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
|
||||
@@ -507,7 +515,7 @@ export interface ProjectApi<E = never> {
|
||||
type Endpoint13_0Request = Parameters<RawClient["server.form"]["form.request.list"]>[0]
|
||||
export type Endpoint13_0Input = { readonly location?: Endpoint13_0Request["query"]["location"] }
|
||||
export type Endpoint13_0Output = EffectValue<ReturnType<RawClient["server.form"]["form.request.list"]>>
|
||||
export type FormListRequestsOperation<E = never> = (input?: Endpoint13_0Input) => Effect.Effect<Endpoint13_0Output, E>
|
||||
export type FormRequestListOperation<E = never> = (input?: Endpoint13_0Input) => Effect.Effect<Endpoint13_0Output, E>
|
||||
|
||||
type Endpoint13_1Request = Parameters<RawClient["server.form"]["session.form.list"]>[0]
|
||||
export type Endpoint13_1Input = { readonly sessionID: Endpoint13_1Request["params"]["sessionID"] }
|
||||
@@ -561,7 +569,7 @@ export type Endpoint13_6Output = EffectValue<ReturnType<RawClient["server.form"]
|
||||
export type FormCancelOperation<E = never> = (input: Endpoint13_6Input) => Effect.Effect<Endpoint13_6Output, E>
|
||||
|
||||
export interface FormApi<E = never> {
|
||||
readonly listRequests: FormListRequestsOperation<E>
|
||||
readonly request: { readonly list: FormRequestListOperation<E> }
|
||||
readonly list: FormListOperation<E>
|
||||
readonly create: FormCreateOperation<E>
|
||||
readonly get: FormGetOperation<E>
|
||||
@@ -573,7 +581,7 @@ export interface FormApi<E = never> {
|
||||
type Endpoint14_0Request = Parameters<RawClient["server.permission"]["permission.request.list"]>[0]
|
||||
export type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] }
|
||||
export type Endpoint14_0Output = EffectValue<ReturnType<RawClient["server.permission"]["permission.request.list"]>>
|
||||
export type PermissionListRequestsOperation<E = never> = (
|
||||
export type PermissionRequestListOperation<E = never> = (
|
||||
input?: Endpoint14_0Input,
|
||||
) => Effect.Effect<Endpoint14_0Output, E>
|
||||
|
||||
@@ -582,14 +590,14 @@ export type Endpoint14_1Input = { readonly projectID?: Endpoint14_1Request["quer
|
||||
export type Endpoint14_1Output = EffectValue<
|
||||
ReturnType<RawClient["server.permission"]["permission.saved.list"]>
|
||||
>["data"]
|
||||
export type PermissionListSavedOperation<E = never> = (
|
||||
export type PermissionSavedListOperation<E = never> = (
|
||||
input?: Endpoint14_1Input,
|
||||
) => Effect.Effect<Endpoint14_1Output, E>
|
||||
|
||||
type Endpoint14_2Request = Parameters<RawClient["server.permission"]["permission.saved.remove"]>[0]
|
||||
export type Endpoint14_2Input = { readonly id: Endpoint14_2Request["params"]["id"] }
|
||||
export type Endpoint14_2Output = EffectValue<ReturnType<RawClient["server.permission"]["permission.saved.remove"]>>
|
||||
export type PermissionRemoveSavedOperation<E = never> = (
|
||||
export type PermissionSavedRemoveOperation<E = never> = (
|
||||
input: Endpoint14_2Input,
|
||||
) => Effect.Effect<Endpoint14_2Output, E>
|
||||
|
||||
@@ -637,9 +645,8 @@ export type Endpoint14_6Output = EffectValue<ReturnType<RawClient["server.permis
|
||||
export type PermissionReplyOperation<E = never> = (input: Endpoint14_6Input) => Effect.Effect<Endpoint14_6Output, E>
|
||||
|
||||
export interface PermissionApi<E = never> {
|
||||
readonly listRequests: PermissionListRequestsOperation<E>
|
||||
readonly listSaved: PermissionListSavedOperation<E>
|
||||
readonly removeSaved: PermissionRemoveSavedOperation<E>
|
||||
readonly request: { readonly list: PermissionRequestListOperation<E> }
|
||||
readonly saved: { readonly list: PermissionSavedListOperation<E>; readonly remove: PermissionSavedRemoveOperation<E> }
|
||||
readonly create: PermissionCreateOperation<E>
|
||||
readonly list: PermissionListOperation<E>
|
||||
readonly get: PermissionGetOperation<E>
|
||||
@@ -808,7 +815,7 @@ export interface ShellApi<E = never> {
|
||||
type Endpoint21_0Request = Parameters<RawClient["server.question"]["question.request.list"]>[0]
|
||||
export type Endpoint21_0Input = { readonly location?: Endpoint21_0Request["query"]["location"] }
|
||||
export type Endpoint21_0Output = EffectValue<ReturnType<RawClient["server.question"]["question.request.list"]>>
|
||||
export type QuestionListRequestsOperation<E = never> = (
|
||||
export type QuestionRequestListOperation<E = never> = (
|
||||
input?: Endpoint21_0Input,
|
||||
) => Effect.Effect<Endpoint21_0Output, E>
|
||||
|
||||
@@ -835,7 +842,7 @@ export type Endpoint21_3Output = EffectValue<ReturnType<RawClient["server.questi
|
||||
export type QuestionRejectOperation<E = never> = (input: Endpoint21_3Input) => Effect.Effect<Endpoint21_3Output, E>
|
||||
|
||||
export interface QuestionApi<E = never> {
|
||||
readonly listRequests: QuestionListRequestsOperation<E>
|
||||
readonly request: { readonly list: QuestionRequestListOperation<E> }
|
||||
readonly list: QuestionListOperation<E>
|
||||
readonly reply: QuestionReplyOperation<E>
|
||||
readonly reject: QuestionRejectOperation<E>
|
||||
@@ -905,16 +912,15 @@ export interface VcsApi<E = never> {
|
||||
}
|
||||
|
||||
export type Endpoint25_0Output = EffectValue<ReturnType<RawClient["server.debug"]["debug.location"]>>
|
||||
export type DebugLocationOperation<E = never> = () => Effect.Effect<Endpoint25_0Output, E>
|
||||
export type DebugLocationListOperation<E = never> = () => Effect.Effect<Endpoint25_0Output, E>
|
||||
|
||||
type Endpoint25_1Request = Parameters<RawClient["server.debug"]["debug.location.evict"]>[0]
|
||||
export type Endpoint25_1Input = { readonly location?: Endpoint25_1Request["query"]["location"] }
|
||||
export type Endpoint25_1Output = EffectValue<ReturnType<RawClient["server.debug"]["debug.location.evict"]>>
|
||||
export type DebugEvictLocationOperation<E = never> = (input?: Endpoint25_1Input) => Effect.Effect<Endpoint25_1Output, E>
|
||||
export type DebugLocationEvictOperation<E = never> = (input?: Endpoint25_1Input) => Effect.Effect<Endpoint25_1Output, E>
|
||||
|
||||
export interface DebugApi<E = never> {
|
||||
readonly location: DebugLocationOperation<E>
|
||||
readonly evictLocation: DebugEvictLocationOperation<E>
|
||||
readonly location: { readonly list: DebugLocationListOperation<E>; readonly evict: DebugLocationEvictOperation<E> }
|
||||
}
|
||||
|
||||
export interface AppApi<E = never> {
|
||||
|
||||
@@ -381,9 +381,7 @@ const adaptGroup4 = (raw: RawClient["server.session"]) => ({
|
||||
shell: Endpoint4_14(raw),
|
||||
compact: Endpoint4_15(raw),
|
||||
wait: Endpoint4_16(raw),
|
||||
revertStage: Endpoint4_17(raw),
|
||||
revertClear: Endpoint4_18(raw),
|
||||
revertCommit: Endpoint4_19(raw),
|
||||
revert: { stage: Endpoint4_17(raw), clear: Endpoint4_18(raw), commit: Endpoint4_19(raw) },
|
||||
context: Endpoint4_20(raw),
|
||||
instructions: { entry: { list: Endpoint4_21(raw), put: Endpoint4_22(raw), remove: Endpoint4_23(raw) } },
|
||||
log: Endpoint4_24(raw),
|
||||
@@ -536,11 +534,8 @@ const Endpoint9_6 = (raw: RawClient["server.integration"]) => (input: Endpoint9_
|
||||
const adaptGroup9 = (raw: RawClient["server.integration"]) => ({
|
||||
list: Endpoint9_0(raw),
|
||||
get: Endpoint9_1(raw),
|
||||
connectKey: Endpoint9_2(raw),
|
||||
connectOauth: Endpoint9_3(raw),
|
||||
attemptStatus: Endpoint9_4(raw),
|
||||
attemptComplete: Endpoint9_5(raw),
|
||||
attemptCancel: Endpoint9_6(raw),
|
||||
connect: { key: Endpoint9_2(raw), oauth: Endpoint9_3(raw) },
|
||||
attempt: { status: Endpoint9_4(raw), complete: Endpoint9_5(raw), cancel: Endpoint9_6(raw) },
|
||||
})
|
||||
|
||||
type Endpoint10_0Request = Parameters<RawClient["server.mcp"]["mcp.list"]>[0]
|
||||
@@ -553,7 +548,10 @@ type Endpoint10_1Input = { readonly location?: Endpoint10_1Request["query"]["loc
|
||||
const Endpoint10_1 = (raw: RawClient["server.mcp"]) => (input?: Endpoint10_1Input) =>
|
||||
raw["mcp.resource.catalog"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup10 = (raw: RawClient["server.mcp"]) => ({ list: Endpoint10_0(raw), catalog: Endpoint10_1(raw) })
|
||||
const adaptGroup10 = (raw: RawClient["server.mcp"]) => ({
|
||||
list: Endpoint10_0(raw),
|
||||
resource: { catalog: Endpoint10_1(raw) },
|
||||
})
|
||||
|
||||
type Endpoint11_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
|
||||
type Endpoint11_0Input = {
|
||||
@@ -690,7 +688,7 @@ const Endpoint13_6 = (raw: RawClient["server.form"]) => (input: Endpoint13_6Inpu
|
||||
)
|
||||
|
||||
const adaptGroup13 = (raw: RawClient["server.form"]) => ({
|
||||
listRequests: Endpoint13_0(raw),
|
||||
request: { list: Endpoint13_0(raw) },
|
||||
list: Endpoint13_1(raw),
|
||||
create: Endpoint13_2(raw),
|
||||
get: Endpoint13_3(raw),
|
||||
@@ -778,9 +776,8 @@ const Endpoint14_6 = (raw: RawClient["server.permission"]) => (input: Endpoint14
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup14 = (raw: RawClient["server.permission"]) => ({
|
||||
listRequests: Endpoint14_0(raw),
|
||||
listSaved: Endpoint14_1(raw),
|
||||
removeSaved: Endpoint14_2(raw),
|
||||
request: { list: Endpoint14_0(raw) },
|
||||
saved: { list: Endpoint14_1(raw), remove: Endpoint14_2(raw) },
|
||||
create: Endpoint14_3(raw),
|
||||
list: Endpoint14_4(raw),
|
||||
get: Endpoint14_5(raw),
|
||||
@@ -1013,7 +1010,7 @@ const Endpoint21_3 = (raw: RawClient["server.question"]) => (input: Endpoint21_3
|
||||
)
|
||||
|
||||
const adaptGroup21 = (raw: RawClient["server.question"]) => ({
|
||||
listRequests: Endpoint21_0(raw),
|
||||
request: { list: Endpoint21_0(raw) },
|
||||
list: Endpoint21_1(raw),
|
||||
reply: Endpoint21_2(raw),
|
||||
reject: Endpoint21_3(raw),
|
||||
@@ -1099,8 +1096,7 @@ const Endpoint25_1 = (raw: RawClient["server.debug"]) => (input?: Endpoint25_1In
|
||||
raw["debug.location.evict"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup25 = (raw: RawClient["server.debug"]) => ({
|
||||
location: Endpoint25_0(raw),
|
||||
evictLocation: Endpoint25_1(raw),
|
||||
location: { list: Endpoint25_0(raw), evict: Endpoint25_1(raw) },
|
||||
})
|
||||
|
||||
const adaptClient = (raw: RawClient) => ({
|
||||
|
||||
@@ -89,8 +89,8 @@ import type {
|
||||
IntegrationAttemptCancelOutput,
|
||||
ServerMcpListInput,
|
||||
ServerMcpListOutput,
|
||||
ServerMcpCatalogInput,
|
||||
ServerMcpCatalogOutput,
|
||||
ServerMcpResourceCatalogInput,
|
||||
ServerMcpResourceCatalogOutput,
|
||||
CredentialUpdateInput,
|
||||
CredentialUpdateOutput,
|
||||
CredentialRemoveInput,
|
||||
@@ -100,8 +100,8 @@ import type {
|
||||
ProjectCurrentOutput,
|
||||
ProjectDirectoriesInput,
|
||||
ProjectDirectoriesOutput,
|
||||
FormListRequestsInput,
|
||||
FormListRequestsOutput,
|
||||
FormRequestListInput,
|
||||
FormRequestListOutput,
|
||||
FormListInput,
|
||||
FormListOutput,
|
||||
FormCreateInput,
|
||||
@@ -114,12 +114,12 @@ import type {
|
||||
FormReplyOutput,
|
||||
FormCancelInput,
|
||||
FormCancelOutput,
|
||||
PermissionListRequestsInput,
|
||||
PermissionListRequestsOutput,
|
||||
PermissionListSavedInput,
|
||||
PermissionListSavedOutput,
|
||||
PermissionRemoveSavedInput,
|
||||
PermissionRemoveSavedOutput,
|
||||
PermissionRequestListInput,
|
||||
PermissionRequestListOutput,
|
||||
PermissionSavedListInput,
|
||||
PermissionSavedListOutput,
|
||||
PermissionSavedRemoveInput,
|
||||
PermissionSavedRemoveOutput,
|
||||
PermissionCreateInput,
|
||||
PermissionCreateOutput,
|
||||
PermissionListInput,
|
||||
@@ -161,8 +161,8 @@ import type {
|
||||
ShellOutputOutput,
|
||||
ShellRemoveInput,
|
||||
ShellRemoveOutput,
|
||||
QuestionListRequestsInput,
|
||||
QuestionListRequestsOutput,
|
||||
QuestionRequestListInput,
|
||||
QuestionRequestListOutput,
|
||||
QuestionListInput,
|
||||
QuestionListOutput,
|
||||
QuestionReplyInput,
|
||||
@@ -181,9 +181,9 @@ import type {
|
||||
VcsStatusOutput,
|
||||
VcsDiffInput,
|
||||
VcsDiffOutput,
|
||||
DebugLocationOutput,
|
||||
DebugEvictLocationInput,
|
||||
DebugEvictLocationOutput,
|
||||
DebugLocationListOutput,
|
||||
DebugLocationEvictInput,
|
||||
DebugLocationEvictOutput,
|
||||
} from "./types"
|
||||
import { ClientError } from "./client-error"
|
||||
|
||||
@@ -601,40 +601,42 @@ export function make(options: ClientOptions) {
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
revertStage: (input: SessionRevertStageInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: SessionRevertStageOutput }>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`,
|
||||
body: { messageID: input["messageID"], files: input["files"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 409, 500, 400, 401],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
revertClear: (input: SessionRevertClearInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionRevertClearOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/clear`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 409, 500, 400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
revertCommit: (input: SessionRevertCommitInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionRevertCommitOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 409, 400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
revert: {
|
||||
stage: (input: SessionRevertStageInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: SessionRevertStageOutput }>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`,
|
||||
body: { messageID: input["messageID"], files: input["files"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 409, 500, 400, 401],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
clear: (input: SessionRevertClearInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionRevertClearOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/clear`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 409, 500, 400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
commit: (input: SessionRevertCommitInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionRevertCommitOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 409, 400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
context: (input: SessionContextInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: SessionContextOutput }>(
|
||||
{
|
||||
@@ -836,69 +838,73 @@ export function make(options: ClientOptions) {
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
connectKey: (input: IntegrationConnectKeyInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationConnectKeyOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/key`,
|
||||
query: { location: input["location"] },
|
||||
body: { key: input["key"], label: input["label"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
connectOauth: (input: IntegrationConnectOauthInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationConnectOauthOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth`,
|
||||
query: { location: input["location"] },
|
||||
body: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
attemptStatus: (input: IntegrationAttemptStatusInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationAttemptStatusOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
attemptComplete: (input: IntegrationAttemptCompleteInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationAttemptCompleteOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}/complete`,
|
||||
query: { location: input["location"] },
|
||||
body: { code: input["code"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
attemptCancel: (input: IntegrationAttemptCancelInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationAttemptCancelOutput>(
|
||||
{
|
||||
method: "DELETE",
|
||||
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
connect: {
|
||||
key: (input: IntegrationConnectKeyInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationConnectKeyOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/key`,
|
||||
query: { location: input["location"] },
|
||||
body: { key: input["key"], label: input["label"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
oauth: (input: IntegrationConnectOauthInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationConnectOauthOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth`,
|
||||
query: { location: input["location"] },
|
||||
body: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
attempt: {
|
||||
status: (input: IntegrationAttemptStatusInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationAttemptStatusOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
complete: (input: IntegrationAttemptCompleteInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationAttemptCompleteOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}/complete`,
|
||||
query: { location: input["location"] },
|
||||
body: { code: input["code"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
cancel: (input: IntegrationAttemptCancelInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationAttemptCancelOutput>(
|
||||
{
|
||||
method: "DELETE",
|
||||
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
},
|
||||
"server.mcp": {
|
||||
list: (input?: ServerMcpListInput, requestOptions?: RequestOptions) =>
|
||||
@@ -913,18 +919,20 @@ export function make(options: ClientOptions) {
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
catalog: (input?: ServerMcpCatalogInput, requestOptions?: RequestOptions) =>
|
||||
request<ServerMcpCatalogOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/mcp/resource`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
resource: {
|
||||
catalog: (input?: ServerMcpResourceCatalogInput, requestOptions?: RequestOptions) =>
|
||||
request<ServerMcpResourceCatalogOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/mcp/resource`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
},
|
||||
credential: {
|
||||
update: (input: CredentialUpdateInput, requestOptions?: RequestOptions) =>
|
||||
@@ -985,18 +993,20 @@ export function make(options: ClientOptions) {
|
||||
),
|
||||
},
|
||||
form: {
|
||||
listRequests: (input?: FormListRequestsInput, requestOptions?: RequestOptions) =>
|
||||
request<FormListRequestsOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/form/request`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
request: {
|
||||
list: (input?: FormRequestListInput, requestOptions?: RequestOptions) =>
|
||||
request<FormRequestListOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/form/request`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
list: (input: FormListInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: FormListOutput }>(
|
||||
{
|
||||
@@ -1074,41 +1084,45 @@ export function make(options: ClientOptions) {
|
||||
),
|
||||
},
|
||||
permission: {
|
||||
listRequests: (input?: PermissionListRequestsInput, requestOptions?: RequestOptions) =>
|
||||
request<PermissionListRequestsOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/permission/request`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
listSaved: (input?: PermissionListSavedInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: PermissionListSavedOutput }>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/permission/saved`,
|
||||
query: { projectID: input?.["projectID"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
removeSaved: (input: PermissionRemoveSavedInput, requestOptions?: RequestOptions) =>
|
||||
request<PermissionRemoveSavedOutput>(
|
||||
{
|
||||
method: "DELETE",
|
||||
path: `/api/permission/saved/${encodeURIComponent(input.id)}`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
request: {
|
||||
list: (input?: PermissionRequestListInput, requestOptions?: RequestOptions) =>
|
||||
request<PermissionRequestListOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/permission/request`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
saved: {
|
||||
list: (input?: PermissionSavedListInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: PermissionSavedListOutput }>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/permission/saved`,
|
||||
query: { projectID: input?.["projectID"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
remove: (input: PermissionSavedRemoveInput, requestOptions?: RequestOptions) =>
|
||||
request<PermissionSavedRemoveOutput>(
|
||||
{
|
||||
method: "DELETE",
|
||||
path: `/api/permission/saved/${encodeURIComponent(input.id)}`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
create: (input: PermissionCreateInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: PermissionCreateOutput }>(
|
||||
{
|
||||
@@ -1390,18 +1404,20 @@ export function make(options: ClientOptions) {
|
||||
),
|
||||
},
|
||||
question: {
|
||||
listRequests: (input?: QuestionListRequestsInput, requestOptions?: RequestOptions) =>
|
||||
request<QuestionListRequestsOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/question/request`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
request: {
|
||||
list: (input?: QuestionRequestListInput, requestOptions?: RequestOptions) =>
|
||||
request<QuestionRequestListOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/question/request`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
list: (input: QuestionListInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: QuestionListOutput }>(
|
||||
{
|
||||
@@ -1518,29 +1534,31 @@ export function make(options: ClientOptions) {
|
||||
),
|
||||
},
|
||||
debug: {
|
||||
location: (requestOptions?: RequestOptions) =>
|
||||
request<DebugLocationOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/debug/location`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
evictLocation: (input?: DebugEvictLocationInput, requestOptions?: RequestOptions) =>
|
||||
request<DebugEvictLocationOutput>(
|
||||
{
|
||||
method: "DELETE",
|
||||
path: `/api/debug/location`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
location: {
|
||||
list: (requestOptions?: RequestOptions) =>
|
||||
request<DebugLocationListOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/debug/location`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
evict: (input?: DebugLocationEvictInput, requestOptions?: RequestOptions) =>
|
||||
request<DebugLocationEvictOutput>(
|
||||
{
|
||||
method: "DELETE",
|
||||
path: `/api/debug/location`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2508,13 +2508,13 @@ export type ServerMcpListOutput = {
|
||||
}>
|
||||
}
|
||||
|
||||
export type ServerMcpCatalogInput = {
|
||||
export type ServerMcpResourceCatalogInput = {
|
||||
readonly location?: {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
}["location"]
|
||||
}
|
||||
|
||||
export type ServerMcpCatalogOutput = {
|
||||
export type ServerMcpResourceCatalogOutput = {
|
||||
readonly location: {
|
||||
readonly directory: string
|
||||
readonly workspaceID?: string
|
||||
@@ -2585,13 +2585,13 @@ export type ProjectDirectoriesInput = {
|
||||
|
||||
export type ProjectDirectoriesOutput = ReadonlyArray<{ readonly directory: string; readonly strategy?: string }>
|
||||
|
||||
export type FormListRequestsInput = {
|
||||
export type FormRequestListInput = {
|
||||
readonly location?: {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
}["location"]
|
||||
}
|
||||
|
||||
export type FormListRequestsOutput = {
|
||||
export type FormRequestListOutput = {
|
||||
readonly location: {
|
||||
readonly directory: string
|
||||
readonly workspaceID?: string
|
||||
@@ -3659,13 +3659,13 @@ export type FormCancelInput = {
|
||||
|
||||
export type FormCancelOutput = void
|
||||
|
||||
export type PermissionListRequestsInput = {
|
||||
export type PermissionRequestListInput = {
|
||||
readonly location?: {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
}["location"]
|
||||
}
|
||||
|
||||
export type PermissionListRequestsOutput = {
|
||||
export type PermissionRequestListOutput = {
|
||||
readonly location: {
|
||||
readonly directory: string
|
||||
readonly workspaceID?: string
|
||||
@@ -3682,9 +3682,9 @@ export type PermissionListRequestsOutput = {
|
||||
}>
|
||||
}
|
||||
|
||||
export type PermissionListSavedInput = { readonly projectID?: { readonly projectID?: string | undefined }["projectID"] }
|
||||
export type PermissionSavedListInput = { readonly projectID?: { readonly projectID?: string | undefined }["projectID"] }
|
||||
|
||||
export type PermissionListSavedOutput = {
|
||||
export type PermissionSavedListOutput = {
|
||||
readonly data: ReadonlyArray<{
|
||||
readonly id: string
|
||||
readonly projectID: string
|
||||
@@ -3693,9 +3693,9 @@ export type PermissionListSavedOutput = {
|
||||
}>
|
||||
}["data"]
|
||||
|
||||
export type PermissionRemoveSavedInput = { readonly id: { readonly id: string }["id"] }
|
||||
export type PermissionSavedRemoveInput = { readonly id: { readonly id: string }["id"] }
|
||||
|
||||
export type PermissionRemoveSavedOutput = void
|
||||
export type PermissionSavedRemoveOutput = void
|
||||
|
||||
export type PermissionCreateInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
@@ -6049,13 +6049,13 @@ export type ShellRemoveInput = {
|
||||
|
||||
export type ShellRemoveOutput = void
|
||||
|
||||
export type QuestionListRequestsInput = {
|
||||
export type QuestionRequestListInput = {
|
||||
readonly location?: {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
}["location"]
|
||||
}
|
||||
|
||||
export type QuestionListRequestsOutput = {
|
||||
export type QuestionRequestListOutput = {
|
||||
readonly location: {
|
||||
readonly directory: string
|
||||
readonly workspaceID?: string
|
||||
@@ -6221,12 +6221,12 @@ export type VcsDiffOutput = {
|
||||
}>
|
||||
}
|
||||
|
||||
export type DebugLocationOutput = ReadonlyArray<{ readonly directory: string; readonly workspaceID?: string }>
|
||||
export type DebugLocationListOutput = ReadonlyArray<{ readonly directory: string; readonly workspaceID?: string }>
|
||||
|
||||
export type DebugEvictLocationInput = {
|
||||
export type DebugLocationEvictInput = {
|
||||
readonly location?: {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
}["location"]
|
||||
}
|
||||
|
||||
export type DebugEvictLocationOutput = void
|
||||
export type DebugLocationEvictOutput = void
|
||||
|
||||
@@ -19,7 +19,7 @@ import { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||
import { Workspace } from "@opencode-ai/schema/workspace"
|
||||
import { Api } from "@opencode-ai/server/api"
|
||||
import { compile, emitPromise } from "@opencode-ai/httpapi-codegen"
|
||||
import { ClientApi, endpointNames, groupNames, promiseOmitEndpoints } from "../src/contract"
|
||||
import { ClientApi, groupNames, promiseOmitEndpoints } from "../src/contract"
|
||||
|
||||
const Client = await import("../src/effect")
|
||||
|
||||
@@ -49,8 +49,8 @@ test("Core and Server reuse the authoritative Schema and Protocol values", () =>
|
||||
})
|
||||
|
||||
test("client and Server contracts generate identically", () => {
|
||||
const server = compile(Api, { groupNames, endpointNames, omitEndpoints: promiseOmitEndpoints })
|
||||
const client = compile(ClientApi, { groupNames, endpointNames, omitEndpoints: promiseOmitEndpoints })
|
||||
const server = compile(Api, { groupNames, omitEndpoints: promiseOmitEndpoints })
|
||||
const client = compile(ClientApi, { groupNames, omitEndpoints: promiseOmitEndpoints })
|
||||
|
||||
expect(emitPromise(client)).toEqual(emitPromise(server))
|
||||
})
|
||||
|
||||
@@ -32,17 +32,12 @@ test("exposes every standard HTTP API group", () => {
|
||||
"vcs",
|
||||
"debug",
|
||||
])
|
||||
expect(Object.keys(client.debug)).toEqual(["location", "evictLocation"])
|
||||
expect(Object.keys(client.debug)).toEqual(["location"])
|
||||
expect(Object.keys(client.debug.location)).toEqual(["list", "evict"])
|
||||
expect(Object.keys(client.message)).toEqual(["list"])
|
||||
expect(Object.keys(client.integration)).toEqual([
|
||||
"list",
|
||||
"get",
|
||||
"connectKey",
|
||||
"connectOauth",
|
||||
"attemptStatus",
|
||||
"attemptComplete",
|
||||
"attemptCancel",
|
||||
])
|
||||
expect(Object.keys(client.integration)).toEqual(["list", "get", "connect", "attempt"])
|
||||
expect(Object.keys(client.integration.connect)).toEqual(["key", "oauth"])
|
||||
expect(Object.keys(client.integration.attempt)).toEqual(["status", "complete", "cancel"])
|
||||
expect(Object.keys(client.file)).toEqual(["read", "list", "find"])
|
||||
expect(Object.keys(client.vcs)).toEqual(["status", "diff"])
|
||||
expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove"])
|
||||
@@ -66,7 +61,7 @@ test("MCP resource catalog uses the public HTTP contract", async () => {
|
||||
},
|
||||
})
|
||||
|
||||
const result = await client["server.mcp"].catalog({ location: { directory: "/tmp/project" } })
|
||||
const result = await client["server.mcp"].resource.catalog({ location: { directory: "/tmp/project" } })
|
||||
|
||||
expect(result.data.resources[0]?.uri).toBe("docs://readme")
|
||||
expect(request?.method).toBe("GET")
|
||||
|
||||
+3473
-3249
File diff suppressed because it is too large
Load Diff
@@ -177,13 +177,13 @@ describe("OpenAPI.fromSpec", () => {
|
||||
const spec = await opencodeSpec()
|
||||
const result = OpenAPI.fromSpec({ spec, baseUrl })
|
||||
|
||||
expect(result.skipped).toHaveLength(5)
|
||||
expect(result.skipped).toHaveLength(4)
|
||||
expect(result.skipped).toContainEqual({
|
||||
method: "GET",
|
||||
path: "/api/pty/{ptyID}/connect",
|
||||
reason: "WebSocket operations are not supported",
|
||||
})
|
||||
expect(result.skipped.filter((item) => item.reason === "SSE operations are not supported")).toHaveLength(3)
|
||||
expect(result.skipped.filter((item) => item.reason === "SSE operations are not supported")).toHaveLength(2)
|
||||
expect(result.skipped).toContainEqual({
|
||||
method: "GET",
|
||||
path: "/api/fs/read/*",
|
||||
@@ -210,11 +210,11 @@ describe("OpenAPI.fromSpec", () => {
|
||||
if (!Tool.isDefinition(instructionPut)) throw new Error("v2.session.instructions.entry.put was not generated")
|
||||
expect(inputTypeScript(instructionPut)).toBe("{ sessionID: string; key: string; value: unknown }")
|
||||
expect(toolAt(result.tools, "v2_session_instructions_entry_put_2")).toBeUndefined()
|
||||
expect(toolAt(result.tools, "v2.pty.connect")).toBeUndefined()
|
||||
expect(Tool.isDefinition(toolAt(result.tools, "v2.pty.connect"))).toBe(false)
|
||||
expect(toolAt(result.tools, "v2.session.log")).toBeUndefined()
|
||||
expect(toolAt(result.tools, "v2.event.subscribe")).toBeUndefined()
|
||||
expect(toolAt(result.tools, "v2.fs.read")).toBeUndefined()
|
||||
expect(toolAt(result.tools, "v2.pty.connectToken")).not.toBeUndefined()
|
||||
expect(toolAt(result.tools, "v2.pty.connect.token")).not.toBeUndefined()
|
||||
})
|
||||
|
||||
test("preserves operation path sanitization and collision handling", () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as PluginV2 from "./plugin"
|
||||
|
||||
import type { Plugin } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { Event, ID, type Info } from "@opencode-ai/schema/plugin"
|
||||
import { makeLocationNode } from "./effect/app-node"
|
||||
import { Context, Effect, Exit, Layer, Scope, Semaphore } from "effect"
|
||||
@@ -18,6 +18,7 @@ import { SkillV2 } from "./skill"
|
||||
import { State } from "./state"
|
||||
import { ToolRegistry } from "./tool/registry"
|
||||
import { ToolHooks } from "./tool/hooks"
|
||||
import { PluginHooks } from "./plugin/hooks"
|
||||
|
||||
export interface Interface {
|
||||
readonly activate: (plugins: readonly { readonly plugin: Plugin; readonly version?: string }[]) => Effect.Effect<void>
|
||||
@@ -57,12 +58,13 @@ const layer = Layer.effect(
|
||||
generation.length === definitions.length &&
|
||||
generation.every(
|
||||
(plugin, index) => plugin.id === definitions[index]?.id && plugin.version === definitions[index]?.version,
|
||||
)
|
||||
) &&
|
||||
definitions.every((definition) => active.has(definition.id))
|
||||
) {
|
||||
return
|
||||
}
|
||||
generation = undefined
|
||||
const exit = yield* State.batch(
|
||||
yield* State.batch(
|
||||
Effect.gen(function* () {
|
||||
const scopes = Array.from(active.values()).toReversed()
|
||||
active.clear()
|
||||
@@ -81,13 +83,17 @@ const layer = Layer.effect(
|
||||
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(child, exit) : Effect.void)),
|
||||
Effect.exit,
|
||||
)
|
||||
if (Exit.isFailure(loaded)) return loaded
|
||||
if (Exit.isFailure(loaded)) {
|
||||
yield* Effect.logWarning("failed to load plugin", {
|
||||
"plugin.id": definition.id,
|
||||
cause: loaded.cause,
|
||||
})
|
||||
continue
|
||||
}
|
||||
active.set(definition.id, child)
|
||||
}
|
||||
return Exit.void
|
||||
}),
|
||||
)
|
||||
if (Exit.isFailure(exit)) return yield* exit
|
||||
generation = definitions.map((definition) => ({
|
||||
id: definition.id,
|
||||
...(definition.version === undefined ? {} : { version: definition.version }),
|
||||
@@ -131,6 +137,7 @@ export const node = makeLocationNode({
|
||||
SkillV2.node,
|
||||
ToolRegistry.toolsNode,
|
||||
ToolHooks.node,
|
||||
PluginHooks.node,
|
||||
PluginRuntime.node,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
export * as PluginHooks from "./hooks"
|
||||
|
||||
import type { AISDKHooks } from "@opencode-ai/plugin/v2/effect/aisdk"
|
||||
import type { SessionHooks } from "@opencode-ai/plugin/v2/effect/session"
|
||||
import type { ToolHooks } from "@opencode-ai/plugin/v2/effect/tool"
|
||||
import { Context, Effect, Layer, Scope } from "effect"
|
||||
import { makeLocationNode } from "../effect/app-node"
|
||||
import { State } from "../state"
|
||||
|
||||
export interface Domains {
|
||||
readonly aisdk: AISDKHooks
|
||||
readonly session: SessionHooks
|
||||
readonly tool: ToolHooks
|
||||
}
|
||||
|
||||
type Callback<Event> = (event: Event) => Effect.Effect<void>
|
||||
|
||||
export interface Interface {
|
||||
readonly register: <Domain extends keyof Domains, Name extends keyof Domains[Domain]>(
|
||||
domain: Domain,
|
||||
name: Name,
|
||||
callback: Callback<Domains[Domain][Name]>,
|
||||
) => Effect.Effect<State.Registration, never, Scope.Scope>
|
||||
readonly trigger: <Domain extends keyof Domains, Name extends keyof Domains[Domain]>(
|
||||
domain: Domain,
|
||||
name: Name,
|
||||
event: Domains[Domain][Name],
|
||||
) => Effect.Effect<Domains[Domain][Name]>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/PluginHooks") {}
|
||||
|
||||
const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const callbacks = new Map<string, Function[]>()
|
||||
const key = (domain: keyof Domains, name: PropertyKey) => `${domain}.${String(name)}`
|
||||
|
||||
const register: Interface["register"] = Effect.fn("PluginHooks.register")(function* (domain, name, callback) {
|
||||
const scope = yield* Scope.Scope
|
||||
const id = key(domain, name)
|
||||
let active = true
|
||||
callbacks.set(id, [...(callbacks.get(id) ?? []), callback])
|
||||
const dispose = Effect.sync(() => {
|
||||
if (!active) return
|
||||
active = false
|
||||
const next = (callbacks.get(id) ?? []).filter((item) => item !== callback)
|
||||
if (next.length === 0) callbacks.delete(id)
|
||||
else callbacks.set(id, next)
|
||||
})
|
||||
yield* Scope.addFinalizer(scope, dispose)
|
||||
return { dispose }
|
||||
})
|
||||
|
||||
const trigger: Interface["trigger"] = Effect.fn("PluginHooks.trigger")(function* (domain, name, event) {
|
||||
for (const callback of callbacks.get(key(domain, name)) ?? []) {
|
||||
const result: Effect.Effect<void> = Reflect.apply(callback, undefined, [event])
|
||||
yield* result
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
return Service.of({ register, trigger })
|
||||
}),
|
||||
)
|
||||
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [] })
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as PluginHost from "./host"
|
||||
|
||||
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Plugin } from "@opencode-ai/plugin/v2/effect"
|
||||
import { EventManifest } from "@opencode-ai/schema/event-manifest"
|
||||
import { Effect, Schema, Stream } from "effect"
|
||||
import { AgentV2 } from "../agent"
|
||||
@@ -22,6 +22,7 @@ import { Tool } from "../tool/tool"
|
||||
import { Tools } from "../tool/tools"
|
||||
import { ToolHooks } from "../tool/hooks"
|
||||
import { WorkspaceV2 } from "../workspace"
|
||||
import { PluginHooks } from "./hooks"
|
||||
|
||||
const mutable = <T>(value: T) => value as DeepMutable<T>
|
||||
export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Interface) {
|
||||
@@ -36,6 +37,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
||||
const skill = yield* SkillV2.Service
|
||||
const tools = yield* Tools.Service
|
||||
const toolHooks = yield* ToolHooks.Service
|
||||
const hooks = yield* PluginHooks.Service
|
||||
const runtime = yield* PluginRuntime.Service
|
||||
const locationInfo = () =>
|
||||
new Location.Info({
|
||||
@@ -43,7 +45,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
||||
workspaceID: location.workspaceID,
|
||||
project: location.project,
|
||||
})
|
||||
const locationRef = (input?: Parameters<PluginContext["agent"]["list"]>[0]) =>
|
||||
const locationRef = (input?: Parameters<Plugin.Context["agent"]["list"]>[0]) =>
|
||||
input?.location === undefined
|
||||
? undefined
|
||||
: Location.Ref.make({
|
||||
@@ -79,32 +81,32 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
||||
}),
|
||||
},
|
||||
aisdk: {
|
||||
sdk: (callback) =>
|
||||
aisdk.hook.sdk((event) => {
|
||||
hook: (name, callback) => {
|
||||
if (name === "sdk") {
|
||||
return aisdk.hook.sdk((event) => {
|
||||
const output = {
|
||||
model: mutable(event.model),
|
||||
package: event.package,
|
||||
options: event.options,
|
||||
sdk: event.sdk,
|
||||
}
|
||||
return Reflect.apply(callback, undefined, [output]).pipe(
|
||||
Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))),
|
||||
)
|
||||
})
|
||||
}
|
||||
return aisdk.hook.language((event) => {
|
||||
const output = {
|
||||
model: mutable(event.model),
|
||||
package: event.package,
|
||||
options: event.options,
|
||||
sdk: event.sdk,
|
||||
}
|
||||
const result = callback(output)
|
||||
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
|
||||
Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))),
|
||||
)
|
||||
}),
|
||||
language: (callback) =>
|
||||
aisdk.hook.language((event) => {
|
||||
const output = {
|
||||
model: mutable(event.model),
|
||||
sdk: event.sdk,
|
||||
options: event.options,
|
||||
language: event.language,
|
||||
}
|
||||
const result = callback(output)
|
||||
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
|
||||
return Reflect.apply(callback, undefined, [output]).pipe(
|
||||
Effect.tap(() => Effect.sync(() => (event.language = output.language))),
|
||||
)
|
||||
}),
|
||||
})
|
||||
},
|
||||
},
|
||||
catalog: {
|
||||
provider: {
|
||||
@@ -164,25 +166,29 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
||||
integration: {
|
||||
list: () => response(integration.list()),
|
||||
get: (input) => response(integration.get(Integration.ID.make(input.integrationID))),
|
||||
connectKey: (input) =>
|
||||
integration.connection.key({
|
||||
integrationID: Integration.ID.make(input.integrationID),
|
||||
key: input.key,
|
||||
label: input.label,
|
||||
}),
|
||||
connectOauth: (input) =>
|
||||
response(
|
||||
integration.connection.oauth({
|
||||
connect: {
|
||||
key: (input) =>
|
||||
integration.connection.key({
|
||||
integrationID: Integration.ID.make(input.integrationID),
|
||||
methodID: Integration.MethodID.make(input.methodID),
|
||||
inputs: input.inputs,
|
||||
key: input.key,
|
||||
label: input.label,
|
||||
}),
|
||||
),
|
||||
attemptStatus: (input) => response(integration.attempt.status(Integration.AttemptID.make(input.attemptID))),
|
||||
attemptComplete: (input) =>
|
||||
integration.attempt.complete({ attemptID: Integration.AttemptID.make(input.attemptID), code: input.code }),
|
||||
attemptCancel: (input) => integration.attempt.cancel(Integration.AttemptID.make(input.attemptID)),
|
||||
oauth: (input) =>
|
||||
response(
|
||||
integration.connection.oauth({
|
||||
integrationID: Integration.ID.make(input.integrationID),
|
||||
methodID: Integration.MethodID.make(input.methodID),
|
||||
inputs: input.inputs,
|
||||
label: input.label,
|
||||
}),
|
||||
),
|
||||
},
|
||||
attempt: {
|
||||
status: (input) => response(integration.attempt.status(Integration.AttemptID.make(input.attemptID))),
|
||||
complete: (input) =>
|
||||
integration.attempt.complete({ attemptID: Integration.AttemptID.make(input.attemptID), code: input.code }),
|
||||
cancel: (input) => integration.attempt.cancel(Integration.AttemptID.make(input.attemptID)),
|
||||
},
|
||||
reload: integration.reload,
|
||||
connection: {
|
||||
active: (id) => integration.connection.active(Integration.ID.make(id)),
|
||||
@@ -317,11 +323,12 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
||||
registrations,
|
||||
(registration) => tools.register({ [registration.name]: registration.tool }, registration.options),
|
||||
{ discard: true },
|
||||
)
|
||||
).pipe(Effect.orDie)
|
||||
return { dispose: Effect.void }
|
||||
}),
|
||||
execute: {
|
||||
before: (callback) =>
|
||||
toolHooks.hook.before((event) => {
|
||||
hook: (name, callback) => {
|
||||
if (name === "execute.before") {
|
||||
return toolHooks.hook.before((event) => {
|
||||
const output = {
|
||||
tool: event.tool,
|
||||
sessionID: event.sessionID,
|
||||
@@ -330,38 +337,37 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
||||
toolCallID: event.toolCallID,
|
||||
input: event.input,
|
||||
}
|
||||
const result = callback(output)
|
||||
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
|
||||
return Reflect.apply(callback, undefined, [output]).pipe(
|
||||
Effect.tap(() => Effect.sync(() => (event.input = output.input))),
|
||||
)
|
||||
}),
|
||||
after: (callback) =>
|
||||
toolHooks.hook.after((event) => {
|
||||
const output = {
|
||||
tool: event.tool,
|
||||
sessionID: event.sessionID,
|
||||
agent: event.agent,
|
||||
assistantMessageID: event.assistantMessageID,
|
||||
toolCallID: event.toolCallID,
|
||||
input: event.input,
|
||||
result: event.result,
|
||||
output: event.output,
|
||||
outputPaths: event.outputPaths,
|
||||
}
|
||||
const result = callback(output)
|
||||
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
|
||||
Effect.tap(() =>
|
||||
Effect.sync(() => {
|
||||
event.result = output.result
|
||||
event.output = output.output
|
||||
event.outputPaths = output.outputPaths
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
})
|
||||
}
|
||||
return toolHooks.hook.after((event) => {
|
||||
const output = {
|
||||
tool: event.tool,
|
||||
sessionID: event.sessionID,
|
||||
agent: event.agent,
|
||||
assistantMessageID: event.assistantMessageID,
|
||||
toolCallID: event.toolCallID,
|
||||
input: event.input,
|
||||
result: event.result,
|
||||
output: event.output,
|
||||
outputPaths: event.outputPaths,
|
||||
}
|
||||
return Reflect.apply(callback, undefined, [output]).pipe(
|
||||
Effect.tap(() =>
|
||||
Effect.sync(() => {
|
||||
event.result = output.result
|
||||
event.output = output.output
|
||||
event.outputPaths = output.outputPaths
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
},
|
||||
},
|
||||
session: {
|
||||
hook: (name, callback) => hooks.register("session", name, callback),
|
||||
create: (input) =>
|
||||
runtime.session.create({
|
||||
id: input?.id,
|
||||
@@ -375,5 +381,5 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
||||
command: runtime.session.command,
|
||||
interrupt: (input) => runtime.session.interrupt(input.sessionID),
|
||||
},
|
||||
} satisfies PluginContext
|
||||
} satisfies Plugin.Context
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as PluginInternal from "./internal"
|
||||
|
||||
import type { Plugin } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { Context, Effect, Scope } from "effect"
|
||||
import { HttpClient } from "effect/unstable/http"
|
||||
import { AgentV2 } from "../agent"
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
export * as PluginPromise from "./promise"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Plugin, PluginContext } from "@opencode-ai/plugin/v2/promise"
|
||||
import { Plugin } from "@opencode-ai/plugin/v2/effect"
|
||||
import { Effect, Scope, Stream } from "effect"
|
||||
|
||||
type HostRegistration = { readonly dispose: Effect.Effect<void> }
|
||||
type Registration = { readonly dispose: () => Promise<void> }
|
||||
type PromisePlugin = import("@opencode-ai/plugin/v2/plugin").Plugin
|
||||
type PromisePluginContext = import("@opencode-ai/plugin/v2/plugin").Context
|
||||
|
||||
/**
|
||||
* Adapts a Promise plugin into an Effect plugin so the existing Effect-only
|
||||
@@ -16,8 +17,8 @@ type Registration = { readonly dispose: () => Promise<void> }
|
||||
* preserves boot-time batching, so Promise-plugin transforms still coalesce
|
||||
* into one reload per domain.
|
||||
*/
|
||||
export function fromPromise(plugin: Plugin) {
|
||||
return define({
|
||||
export function fromPromise(plugin: PromisePlugin) {
|
||||
return Plugin.define({
|
||||
id: plugin.id,
|
||||
effect: (host) =>
|
||||
Effect.gen(function* () {
|
||||
@@ -43,7 +44,7 @@ export function fromPromise(plugin: Plugin) {
|
||||
}),
|
||||
)
|
||||
|
||||
const context2: PluginContext = {
|
||||
const context2: PromisePluginContext = {
|
||||
options: host.options,
|
||||
agent: {
|
||||
list: (input) => run(host.agent.list(input)),
|
||||
@@ -51,10 +52,8 @@ export function fromPromise(plugin: Plugin) {
|
||||
reload: () => run(host.agent.reload()),
|
||||
},
|
||||
aisdk: {
|
||||
sdk: (callback) =>
|
||||
register(host.aisdk.sdk((event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
||||
language: (callback) =>
|
||||
register(host.aisdk.language((event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
||||
hook: (name, callback) =>
|
||||
register(host.aisdk.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
||||
},
|
||||
catalog: {
|
||||
provider: {
|
||||
@@ -79,11 +78,15 @@ export function fromPromise(plugin: Plugin) {
|
||||
integration: {
|
||||
list: (input) => run(host.integration.list(input)),
|
||||
get: (input) => run(host.integration.get(input)),
|
||||
connectKey: (input) => run(host.integration.connectKey(input)),
|
||||
connectOauth: (input) => run(host.integration.connectOauth(input)),
|
||||
attemptStatus: (input) => run(host.integration.attemptStatus(input)),
|
||||
attemptComplete: (input) => run(host.integration.attemptComplete(input)),
|
||||
attemptCancel: (input) => run(host.integration.attemptCancel(input)),
|
||||
connect: {
|
||||
key: (input) => run(host.integration.connect.key(input)),
|
||||
oauth: (input) => run(host.integration.connect.oauth(input)),
|
||||
},
|
||||
attempt: {
|
||||
status: (input) => run(host.integration.attempt.status(input)),
|
||||
complete: (input) => run(host.integration.attempt.complete(input)),
|
||||
cancel: (input) => run(host.integration.attempt.cancel(input)),
|
||||
},
|
||||
transform: transform(host.integration),
|
||||
reload: () => run(host.integration.reload()),
|
||||
connection: {
|
||||
@@ -104,12 +107,19 @@ export function fromPromise(plugin: Plugin) {
|
||||
transform: transform(host.skill),
|
||||
reload: () => run(host.skill.reload()),
|
||||
},
|
||||
tool: {
|
||||
transform: transform(host.tool),
|
||||
hook: (name, callback) =>
|
||||
register(host.tool.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
||||
},
|
||||
session: {
|
||||
create: (input) => run(host.session.create(input)),
|
||||
get: (input) => run(host.session.get(input)),
|
||||
prompt: (input) => run(host.session.prompt(input)),
|
||||
command: (input) => run(host.session.command(input)),
|
||||
interrupt: (input) => run(host.session.interrupt(input)),
|
||||
hook: (name, callback) =>
|
||||
register(host.session.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
export const AlibabaPlugin = define({
|
||||
id: "opencode.provider.alibaba",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/alibaba") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/alibaba"))
|
||||
|
||||
@@ -75,7 +75,8 @@ export const AmazonBedrockPlugin = define({
|
||||
})
|
||||
}
|
||||
})
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (!["@ai-sdk/amazon-bedrock", "@ai-sdk/amazon-bedrock/mantle"].includes(evt.package)) return
|
||||
const options = { ...evt.options }
|
||||
@@ -108,7 +109,8 @@ export const AmazonBedrockPlugin = define({
|
||||
evt.sdk = mod.createAmazonBedrock(options)
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.language(
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
|
||||
if (
|
||||
|
||||
@@ -17,7 +17,8 @@ export const AnthropicPlugin = define({
|
||||
})
|
||||
}
|
||||
})
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/anthropic") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/anthropic"))
|
||||
|
||||
@@ -26,7 +26,8 @@ export const AzurePlugin = define({
|
||||
})
|
||||
}
|
||||
})
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/azure") return
|
||||
if (evt.model.providerID === ProviderV2.ID.azure) {
|
||||
@@ -44,7 +45,8 @@ export const AzurePlugin = define({
|
||||
evt.sdk = mod.createAzure(evt.options)
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.language(
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.azure) return
|
||||
evt.language = selectLanguage(
|
||||
@@ -75,7 +77,8 @@ export const AzureCognitiveServicesPlugin = define({
|
||||
})
|
||||
}
|
||||
})
|
||||
yield* ctx.aisdk.language(
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return
|
||||
evt.language = selectLanguage(
|
||||
|
||||
@@ -14,7 +14,8 @@ export const CerebrasPlugin = define({
|
||||
})
|
||||
}
|
||||
})
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/cerebras") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/cerebras"))
|
||||
|
||||
@@ -6,7 +6,8 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
export const CloudflareAIGatewayPlugin = define({
|
||||
id: "opencode.provider.cloudflare-ai-gateway",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "ai-gateway-provider") return
|
||||
if (evt.options.baseURL) return
|
||||
|
||||
@@ -19,7 +19,8 @@ export const CloudflareWorkersAIPlugin = define({
|
||||
if (accountId) provider.settings = { ...provider.settings, baseURL: workersEndpoint(accountId) }
|
||||
})
|
||||
})
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== providerID) return
|
||||
if (evt.package !== "@ai-sdk/openai-compatible") return
|
||||
@@ -35,7 +36,8 @@ export const CloudflareWorkersAIPlugin = define({
|
||||
)
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.language(
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== providerID) return
|
||||
evt.language = evt.sdk.languageModel(evt.model.modelID ?? evt.model.id)
|
||||
|
||||
@@ -4,7 +4,8 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
export const CoherePlugin = define({
|
||||
id: "opencode.provider.cohere",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/cohere") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/cohere"))
|
||||
|
||||
@@ -4,7 +4,8 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
export const DeepInfraPlugin = define({
|
||||
id: "opencode.provider.deepinfra",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/deepinfra") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/deepinfra"))
|
||||
|
||||
@@ -7,7 +7,8 @@ export const DynamicProviderPlugin = define({
|
||||
id: "opencode.provider.dynamic",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const npm = yield* Npm.Service
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.sdk) return
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
export const GatewayPlugin = define({
|
||||
id: "opencode.provider.gateway",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/gateway") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/gateway"))
|
||||
|
||||
@@ -23,14 +23,16 @@ export const GithubCopilotPlugin = define({
|
||||
model.enabled = false
|
||||
})
|
||||
})
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/github-copilot") return
|
||||
const mod = yield* Effect.promise(() => import("../../github-copilot/copilot-provider"))
|
||||
evt.sdk = mod.createOpenaiCompatible(evt.options)
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.language(
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
|
||||
if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) {
|
||||
|
||||
@@ -7,7 +7,8 @@ import { ProviderV2 } from "../../provider"
|
||||
export const GitLabPlugin = define({
|
||||
id: "opencode.provider.gitlab",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "gitlab-ai-provider") return
|
||||
const mod = yield* Effect.promise(() => import("gitlab-ai-provider"))
|
||||
@@ -31,7 +32,8 @@ export const GitLabPlugin = define({
|
||||
})
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.language(
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.gitlab) return
|
||||
const featureFlags =
|
||||
|
||||
@@ -85,7 +85,8 @@ export const GoogleVertexPlugin = define({
|
||||
})
|
||||
}
|
||||
})
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID === ProviderV2.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) {
|
||||
evt.options.fetch = authFetch(evt.options.fetch)
|
||||
@@ -104,7 +105,8 @@ export const GoogleVertexPlugin = define({
|
||||
})
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.language(
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.googleVertex) return
|
||||
evt.language = evt.sdk.languageModel(String(evt.model.modelID ?? evt.model.id).trim())
|
||||
@@ -135,7 +137,8 @@ export const GoogleVertexAnthropicPlugin = define({
|
||||
})
|
||||
}
|
||||
})
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/google-vertex/anthropic") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex/anthropic"))
|
||||
@@ -161,7 +164,8 @@ export const GoogleVertexAnthropicPlugin = define({
|
||||
})
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.language(
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return
|
||||
evt.language = evt.sdk.languageModel(String(evt.model.modelID ?? evt.model.id).trim())
|
||||
|
||||
@@ -4,7 +4,8 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
export const GooglePlugin = define({
|
||||
id: "opencode.provider.google",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/google") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/google"))
|
||||
|
||||
@@ -4,7 +4,8 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
export const GroqPlugin = define({
|
||||
id: "opencode.provider.groq",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/groq") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/groq"))
|
||||
|
||||
@@ -4,7 +4,8 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
export const MistralPlugin = define({
|
||||
id: "opencode.provider.mistral",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/mistral") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/mistral"))
|
||||
|
||||
@@ -4,7 +4,8 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
export const OpenAICompatiblePlugin = define({
|
||||
id: "opencode.provider.openai-compatible",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.sdk) return
|
||||
if (!evt.package.includes("@ai-sdk/openai-compatible")) return
|
||||
|
||||
@@ -210,14 +210,16 @@ export const OpenAIPlugin = define({
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
yield* refresh().pipe(Effect.forkScoped)
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/openai") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/openai"))
|
||||
evt.sdk = mod.createOpenAI(evt.options)
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.language(
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.openai) return
|
||||
evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id)
|
||||
|
||||
@@ -23,7 +23,8 @@ export const OpenRouterPlugin = define({
|
||||
}
|
||||
}
|
||||
})
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@openrouter/ai-sdk-provider") return
|
||||
const mod = yield* Effect.promise(() => import("@openrouter/ai-sdk-provider"))
|
||||
|
||||
@@ -4,7 +4,8 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
export const PerplexityPlugin = define({
|
||||
id: "opencode.provider.perplexity",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/perplexity") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/perplexity"))
|
||||
|
||||
@@ -8,7 +8,8 @@ export const SapAICorePlugin = define({
|
||||
id: "opencode.provider.sap-ai-core",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const npm = yield* Npm.Service
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
|
||||
const serviceKey =
|
||||
@@ -37,7 +38,8 @@ export const SapAICorePlugin = define({
|
||||
)
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.language(
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
|
||||
evt.language = evt.sdk(evt.model.modelID ?? evt.model.id)
|
||||
|
||||
@@ -67,7 +67,8 @@ export function cortexFetch(upstream: FetchLike = fetch) {
|
||||
export const SnowflakeCortexPlugin = define({
|
||||
id: "opencode.provider.snowflake-cortex",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("snowflake-cortex")) return
|
||||
const token =
|
||||
|
||||
@@ -4,7 +4,8 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
export const TogetherAIPlugin = define({
|
||||
id: "opencode.provider.togetherai",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/togetherai") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/togetherai"))
|
||||
|
||||
@@ -4,7 +4,8 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
export const VenicePlugin = define({
|
||||
id: "opencode.provider.venice",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "venice-ai-sdk-provider") return
|
||||
const mod = yield* Effect.promise(() => import("venice-ai-sdk-provider"))
|
||||
|
||||
@@ -14,7 +14,8 @@ export const VercelPlugin = define({
|
||||
})
|
||||
}
|
||||
})
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/vercel") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/vercel"))
|
||||
|
||||
@@ -5,14 +5,16 @@ import { ProviderV2 } from "../../provider"
|
||||
export const XAIPlugin = define({
|
||||
id: "opencode.provider.xai",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.sdk(
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/xai") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/xai"))
|
||||
evt.sdk = mod.createXai(evt.options)
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.language(
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("xai")) return
|
||||
evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as SdkPlugins from "./sdk"
|
||||
|
||||
import type { Plugin } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { makeGlobalNode } from "../effect/app-node"
|
||||
import { EventV2 } from "../event"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as PluginSupervisor from "./supervisor"
|
||||
|
||||
import type { Plugin } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { Event } from "@opencode-ai/schema/config"
|
||||
import { Context, Effect, Fiber, Layer, Option, Schema, Semaphore, Stream } from "effect"
|
||||
import path from "path"
|
||||
|
||||
@@ -50,6 +50,8 @@ import { llmClient } from "../../effect/app-node-platform"
|
||||
import { StepFailedError, UserInterruptedError } from "../error"
|
||||
import { toSessionError } from "../to-session-error"
|
||||
import { SessionRunnerRetry } from "./retry"
|
||||
import type { SessionHooks } from "@opencode-ai/plugin/v2/effect/session"
|
||||
import { PluginHooks } from "../../plugin/hooks"
|
||||
|
||||
type StepTokens = {
|
||||
readonly input: number
|
||||
@@ -132,6 +134,7 @@ const layer = Layer.effect(
|
||||
const llm = yield* LLMClient.Service
|
||||
const agents = yield* AgentV2.Service
|
||||
const tools = yield* ToolRegistry.Service
|
||||
const hooks = yield* PluginHooks.Service
|
||||
const models = yield* SessionRunnerModel.Service
|
||||
const store = yield* SessionStore.Service
|
||||
const location = yield* Location.Service
|
||||
@@ -252,10 +255,34 @@ const layer = Layer.effect(
|
||||
tools: toolMaterialization?.definitions ?? [],
|
||||
toolChoice: isLastStep ? "none" : undefined,
|
||||
})
|
||||
const availableTools = new Map(request.tools.map((tool) => [tool.name, tool]))
|
||||
const requestEvent: SessionHooks["request"] = {
|
||||
sessionID: session.id,
|
||||
agent: agent.id,
|
||||
model: resolved.ref,
|
||||
system: [...request.system],
|
||||
messages: [...request.messages],
|
||||
tools: Object.fromEntries(
|
||||
request.tools.map((tool) => [tool.name, { description: tool.description, input: { ...tool.inputSchema } }]),
|
||||
),
|
||||
}
|
||||
// Plugins may reshape the draft, but cannot advertise tools excluded earlier
|
||||
// by permissions or registration state.
|
||||
yield* hooks.trigger("session", "request", requestEvent)
|
||||
const hookedRequest = LLM.updateRequest(request, {
|
||||
system: requestEvent.system,
|
||||
messages: requestEvent.messages,
|
||||
tools: Object.entries(requestEvent.tools).flatMap(([name, tool]) => {
|
||||
const registered = availableTools.get(name)
|
||||
if (!registered) return []
|
||||
return [{ ...registered, description: tool.description, inputSchema: tool.input }]
|
||||
}),
|
||||
})
|
||||
const advertisedTools = new Set(hookedRequest.tools.map((tool) => tool.name))
|
||||
// Automatic compaction completed; rebuild the request from compacted history.
|
||||
if (
|
||||
!(yield* SessionInput.pendingCompaction(db, session.id)) &&
|
||||
(yield* compaction.compactIfNeeded({ sessionID: session.id, messages: context, request }))
|
||||
(yield* compaction.compactIfNeeded({ sessionID: session.id, messages: context, request: hookedRequest }))
|
||||
)
|
||||
return { _tag: "RestartAfterCompaction", step: currentStep } as const
|
||||
const startSnapshot = yield* snapshots.capture()
|
||||
@@ -276,7 +303,7 @@ const layer = Layer.effect(
|
||||
const publish = (event: LLMEvent, outputPaths: ReadonlyArray<string> = [], error?: SessionError.Error) =>
|
||||
serialized(publisher.publish(event, outputPaths, error))
|
||||
let overflowFailure: ProviderErrorEvent | undefined
|
||||
const providerStream = llm.stream(request).pipe(
|
||||
const providerStream = llm.stream(hookedRequest).pipe(
|
||||
Stream.runForEach((event) =>
|
||||
Effect.gen(function* () {
|
||||
if (overflowFailure || publisher.hasProviderError()) return
|
||||
@@ -288,11 +315,11 @@ const layer = Layer.effect(
|
||||
}
|
||||
yield* publish(event)
|
||||
if (event.type !== "tool-call" || event.providerExecuted) return
|
||||
if (!toolMaterialization) {
|
||||
if (!toolMaterialization || !advertisedTools.has(event.name)) {
|
||||
yield* serialized(
|
||||
publisher.failUnsettledTools({
|
||||
type: "tool.execution",
|
||||
message: "Tools are disabled after the maximum agent steps",
|
||||
message: `Tool is not available for this request: ${event.name}`,
|
||||
}),
|
||||
)
|
||||
return
|
||||
@@ -625,6 +652,7 @@ export const node = makeLocationNode({
|
||||
llmClient,
|
||||
AgentV2.node,
|
||||
ToolRegistry.node,
|
||||
PluginHooks.node,
|
||||
SessionRunnerModel.node,
|
||||
SessionStore.node,
|
||||
Location.node,
|
||||
|
||||
@@ -101,27 +101,36 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
||||
ended: (id: string, value: string, ordinal: number, state?: Record<string, unknown>) => Effect.Effect<void>,
|
||||
single = false,
|
||||
) => {
|
||||
const chunks = new Map<string, { readonly ordinal: number; readonly values: string[] }>()
|
||||
const chunks = new Map<
|
||||
string,
|
||||
{ readonly ordinal: number; readonly values: string[]; state?: Record<string, unknown> }
|
||||
>()
|
||||
let nextOrdinal = 0
|
||||
const start = (id: string) =>
|
||||
const start = (id: string, state?: Record<string, unknown>) =>
|
||||
Effect.suspend(() => {
|
||||
if (chunks.has(id)) return Effect.die(new Error(`Duplicate ${name} start: ${id}`))
|
||||
if (single && chunks.size > 0) return Effect.die(new Error(`${name} start before end: ${id}`))
|
||||
const ordinal = nextOrdinal++
|
||||
chunks.set(id, { ordinal, values: [] })
|
||||
chunks.set(id, { ordinal, values: [], state })
|
||||
return Effect.succeed(ordinal)
|
||||
})
|
||||
const append = (id: string, value: string) =>
|
||||
const append = (id: string, value: string, state?: Record<string, unknown>) =>
|
||||
Effect.suspend(() => {
|
||||
const current = chunks.get(id)
|
||||
if (!current) return Effect.die(new Error(`${name} delta before start: ${id}`))
|
||||
current.values.push(value)
|
||||
if (state !== undefined) current.state = { ...current.state, ...state }
|
||||
return Effect.succeed(current.ordinal)
|
||||
})
|
||||
const end = Effect.fnUntraced(function* (id: string, state?: Record<string, unknown>) {
|
||||
const current = chunks.get(id)
|
||||
if (!current) return yield* Effect.die(new Error(`${name} end before start: ${id}`))
|
||||
yield* ended(id, current.values.join(""), current.ordinal, state)
|
||||
yield* ended(
|
||||
id,
|
||||
current.values.join(""),
|
||||
current.ordinal,
|
||||
state === undefined ? current.state : { ...current.state, ...state },
|
||||
)
|
||||
chunks.delete(id)
|
||||
})
|
||||
const flush = Effect.fnUntraced(function* () {
|
||||
@@ -288,7 +297,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
||||
return
|
||||
case "reasoning-start":
|
||||
retryEvidence = true
|
||||
const startedReasoningOrdinal = yield* reasoning.start(event.id)
|
||||
const startedReasoningOrdinal = yield* reasoning.start(event.id, providerState(event.providerMetadata))
|
||||
yield* events.publish(SessionEvent.Reasoning.Started, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: yield* startAssistant(),
|
||||
@@ -297,7 +306,11 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
||||
})
|
||||
return
|
||||
case "reasoning-delta":
|
||||
const deltaReasoningOrdinal = yield* reasoning.append(event.id, event.text)
|
||||
const deltaReasoningOrdinal = yield* reasoning.append(
|
||||
event.id,
|
||||
event.text,
|
||||
providerState(event.providerMetadata),
|
||||
)
|
||||
yield* events.publish(SessionEvent.Reasoning.Delta, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: yield* currentAssistantMessageID(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as ApplyPatchTool from "./apply-patch"
|
||||
|
||||
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { ToolFailure } from "@opencode-ai/llm"
|
||||
import { FileDiff } from "@opencode-ai/schema/file-diff"
|
||||
import { createTwoFilesPatch, diffLines } from "diff"
|
||||
@@ -194,6 +194,19 @@ export const Plugin = {
|
||||
),
|
||||
)
|
||||
.pipe(Effect.orDie)
|
||||
|
||||
yield* ctx.session.hook("request", (event) =>
|
||||
Effect.sync(() => {
|
||||
const usePatch =
|
||||
event.model.providerID.toLowerCase() === "openai" || event.model.id.toLowerCase().includes("gpt")
|
||||
if (usePatch) {
|
||||
delete event.tools.edit
|
||||
delete event.tools.write
|
||||
return
|
||||
}
|
||||
delete event.tools.patch
|
||||
}),
|
||||
)
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
export * as EditTool from "./edit"
|
||||
|
||||
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { ToolFailure } from "@opencode-ai/llm"
|
||||
import { FileDiff } from "@opencode-ai/schema/file-diff"
|
||||
import { createTwoFilesPatch, diffLines } from "diff"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * as GlobTool from "./glob"
|
||||
|
||||
import { ToolFailure } from "@opencode-ai/llm"
|
||||
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { Effect, Schema } from "effect"
|
||||
import path from "path"
|
||||
import { FileSystem } from "../filesystem"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as GrepTool from "./grep"
|
||||
|
||||
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { ToolFailure } from "@opencode-ai/llm"
|
||||
import { Effect, Schema } from "effect"
|
||||
import path from "path"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as QuestionTool from "./question"
|
||||
|
||||
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { ToolFailure } from "@opencode-ai/llm"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Form } from "../form"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as ReadTool from "./read"
|
||||
|
||||
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { dirname } from "path"
|
||||
import { ToolFailure } from "@opencode-ai/llm"
|
||||
import { Effect, Schema } from "effect"
|
||||
|
||||
@@ -191,12 +191,8 @@ const registryLayer = Layer.effect(
|
||||
const registration = entries.at(-1)?.registration
|
||||
if (registration) registrations.set(name, registration)
|
||||
}
|
||||
// OpenAI/GPT models use patch; every other model uses edit and write.
|
||||
const usePatch = input.model.provider.toLowerCase() === "openai" || input.model.id.toLowerCase().includes("gpt")
|
||||
for (const [name, registration] of registrations) {
|
||||
const wrongEditTool = name === "patch" ? !usePatch : (name === "edit" || name === "write") && usePatch
|
||||
if (
|
||||
wrongEditTool ||
|
||||
(registration.deferred && !Flag.CODEMODE_ENABLED) ||
|
||||
whollyDisabled(permission(registration.tool, name), input.permissions ?? [])
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ export * as ShellTool from "./shell"
|
||||
|
||||
import path from "path"
|
||||
import { ToolFailure } from "@opencode-ai/llm"
|
||||
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { Effect, Schema, Scope } from "effect"
|
||||
import { FSUtil } from "../fs-util"
|
||||
import { LocationMutation } from "../location-mutation"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as SkillTool from "./skill"
|
||||
|
||||
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import path from "path"
|
||||
import { ToolFailure } from "@opencode-ai/llm"
|
||||
import { Effect, Schema } from "effect"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
export * as SubagentTool from "./subagent"
|
||||
|
||||
import { ToolFailure } from "@opencode-ai/llm"
|
||||
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { Effect, Schema, Scope } from "effect"
|
||||
import { AgentV2 } from "../agent"
|
||||
import { PluginRuntime } from "../plugin/runtime"
|
||||
import { PermissionV2 } from "../permission"
|
||||
import { SessionSchema } from "../session/schema"
|
||||
import { Tool } from "./tool"
|
||||
|
||||
@@ -42,6 +43,7 @@ export const Plugin = {
|
||||
effect: Effect.fn("SubagentTool.Plugin")(function* (ctx: PluginContext) {
|
||||
const runtime = yield* PluginRuntime.Service
|
||||
const agents = yield* AgentV2.Service
|
||||
const permission = yield* PermissionV2.Service
|
||||
const scope = yield* Scope.Scope
|
||||
|
||||
// Concatenate the child's final completed assistant text. Distinguishes "completed with no
|
||||
@@ -114,6 +116,20 @@ export const Plugin = {
|
||||
if (agent === undefined) return yield* new ToolFailure({ message: `Unknown agent: ${input.agent}` })
|
||||
if (agent.mode === "primary")
|
||||
return yield* new ToolFailure({ message: `Agent ${input.agent} cannot run as a subagent` })
|
||||
yield* permission
|
||||
.assert({
|
||||
action: name,
|
||||
resources: [agent.id],
|
||||
save: [agent.id],
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
source: {
|
||||
type: "tool",
|
||||
messageID: context.assistantMessageID,
|
||||
callID: context.toolCallID,
|
||||
},
|
||||
})
|
||||
.pipe(Effect.mapError((error) => new ToolFailure({ message: `Subagent denied: ${agent.id}`, error })))
|
||||
|
||||
// Model selection is policy/config/session state, not an LLM-facing tool argument.
|
||||
const model = agent.model ?? parent.model
|
||||
@@ -176,5 +192,32 @@ export const Plugin = {
|
||||
),
|
||||
)
|
||||
.pipe(Effect.orDie)
|
||||
|
||||
yield* ctx.session.hook("request", (event) =>
|
||||
Effect.gen(function* () {
|
||||
const tool = event.tools[name]
|
||||
if (!tool) return
|
||||
const selected = yield* agents.resolve(event.agent)
|
||||
if (!selected) return
|
||||
const available = (yield* agents.list())
|
||||
.filter(
|
||||
(agent) =>
|
||||
agent.mode !== "primary" &&
|
||||
!agent.hidden &&
|
||||
PermissionV2.evaluate(name, agent.id, selected.permissions).effect !== "deny",
|
||||
)
|
||||
.toSorted((a, b) => a.id.localeCompare(b.id))
|
||||
if (available.length === 0) return
|
||||
tool.description = [
|
||||
tool.description,
|
||||
"",
|
||||
"Available subagents:",
|
||||
...available.map(
|
||||
(agent) =>
|
||||
`- ${agent.id}: ${agent.description ?? "This subagent should only be called when explicitly requested."}`,
|
||||
),
|
||||
].join("\n")
|
||||
}),
|
||||
)
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as TodoWriteTool from "./todowrite"
|
||||
|
||||
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { ToolFailure } from "@opencode-ai/llm"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { PermissionV2 } from "../permission"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as WebFetchTool from "./webfetch"
|
||||
|
||||
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { ToolFailure } from "@opencode-ai/llm"
|
||||
import { Duration, Effect, Schema } from "effect"
|
||||
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as WebSearchTool from "./websearch"
|
||||
|
||||
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { ToolFailure } from "@opencode-ai/llm"
|
||||
import { Context, Duration, Effect, Layer, Schema } from "effect"
|
||||
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
export * as WriteTool from "./write"
|
||||
|
||||
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { ToolFailure } from "@opencode-ai/llm"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { FileMutation } from "../file-mutation"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { define } from "@opencode-ai/plugin/v2/promise"
|
||||
import { Plugin } from "@opencode-ai/plugin/v2"
|
||||
|
||||
export default define({
|
||||
export default Plugin.define({
|
||||
id: "directory-plugin",
|
||||
setup: async (ctx) => {
|
||||
await ctx.agent.transform((agents) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { define } from "@opencode-ai/plugin/v2/promise"
|
||||
import { Plugin } from "@opencode-ai/plugin/v2"
|
||||
|
||||
export default define({
|
||||
export default Plugin.define({
|
||||
id: "folder-plugin",
|
||||
setup: async (ctx) => {
|
||||
await ctx.agent.transform((agents) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { Plugin as EffectPlugin } from "@opencode-ai/plugin/v2/effect"
|
||||
import { Config as ConfigSchema } from "@opencode-ai/schema/config"
|
||||
import { Plugin } from "@opencode-ai/schema/plugin"
|
||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
@@ -178,7 +178,7 @@ describe("PluginSupervisor config", () => {
|
||||
it.live("loads user plugins before internal post plugins", () =>
|
||||
Effect.gen(function* () {
|
||||
const sdk = yield* SdkPlugins.Service
|
||||
yield* sdk.register(define({ id: "sdk-order", effect: () => Effect.void }))
|
||||
yield* sdk.register(EffectPlugin.define({ id: "sdk-order", effect: () => Effect.void }))
|
||||
yield* withLocation(
|
||||
{
|
||||
plugins: [
|
||||
@@ -275,7 +275,7 @@ function mutablePlugin(description: string) {
|
||||
return `
|
||||
import { define } from ${JSON.stringify(plugin)}
|
||||
|
||||
export default define({
|
||||
export default EffectPlugin.define({
|
||||
id: "mutable-plugin",
|
||||
setup: async (ctx) => {
|
||||
await ctx.agent.transform((agents) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||
import { Tool } from "@opencode-ai/core/tool/tool"
|
||||
import { Tools } from "@opencode-ai/core/tool/tools"
|
||||
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { Effect, type Scope } from "effect"
|
||||
|
||||
export const toolIdentity = {
|
||||
@@ -66,12 +66,10 @@ export const registerToolPlugin = <R>(plugin: {
|
||||
registrations,
|
||||
(registration) => tools.register({ [registration.name]: registration.tool }, registration.options),
|
||||
{ discard: true },
|
||||
)
|
||||
).pipe(Effect.orDie)
|
||||
return { dispose: Effect.void }
|
||||
}),
|
||||
execute: {
|
||||
before: () => Effect.die("registerToolPlugin does not support tool hooks"),
|
||||
after: () => Effect.die("registerToolPlugin does not support tool hooks"),
|
||||
},
|
||||
hook: () => Effect.die("registerToolPlugin does not support tool hooks"),
|
||||
},
|
||||
}
|
||||
yield* plugin.effect(context as PluginContext)
|
||||
|
||||
@@ -4,7 +4,7 @@ import { describe, expect } from "bun:test"
|
||||
import { Config } from "@opencode-ai/schema/config"
|
||||
import { Plugin } from "@opencode-ai/schema/plugin"
|
||||
import { Context, DateTime, Effect, Equal, Hash, RcMap, Schema, Stream } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { Plugin as EffectPlugin } from "@opencode-ai/plugin/v2/effect"
|
||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
@@ -44,7 +44,7 @@ describe("LocationServiceMap", () => {
|
||||
const sdk = yield* SdkPlugins.Service
|
||||
const locations = yield* LocationServiceMap.Service
|
||||
const id = AgentV2.ID.make("persistent-sdk-agent")
|
||||
const plugin = define({
|
||||
const plugin = EffectPlugin.define({
|
||||
id: "persistent-sdk-plugin",
|
||||
effect: (ctx) => ctx.agent.transform((agents) => agents.update(id, () => {})),
|
||||
})
|
||||
@@ -432,7 +432,7 @@ describe("LocationServiceMap", () => {
|
||||
Effect.flatMap((dir) =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* PluginV2.Service
|
||||
const reviewer = define({
|
||||
const reviewer = EffectPlugin.define({
|
||||
id: "reviewer",
|
||||
effect: (ctx) =>
|
||||
ctx.agent
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { Message, SystemPart } from "@opencode-ai/llm"
|
||||
import { Agent } from "@opencode-ai/schema/agent"
|
||||
import { Model } from "@opencode-ai/schema/model"
|
||||
import { Provider } from "@opencode-ai/schema/provider"
|
||||
import { Session } from "@opencode-ai/schema/session"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { PluginHooks } from "../src/plugin/hooks"
|
||||
|
||||
describe("PluginHooks", () => {
|
||||
it("registers scoped domain hooks and triggers them sequentially", async () => {
|
||||
const seen: string[] = []
|
||||
const program = Effect.gen(function* () {
|
||||
const hooks = yield* PluginHooks.Service
|
||||
yield* hooks.register("session", "request", (event) =>
|
||||
Effect.sync(() => {
|
||||
seen.push("first")
|
||||
event.system.push(SystemPart.make("second"))
|
||||
}),
|
||||
)
|
||||
yield* hooks.register("session", "request", (event) =>
|
||||
Effect.sync(() => {
|
||||
seen.push(event.system[1]?.text ?? "missing")
|
||||
event.messages = [Message.user("changed")]
|
||||
}),
|
||||
)
|
||||
const event = {
|
||||
sessionID: Session.ID.make("ses_hooks"),
|
||||
agent: Agent.ID.make("build"),
|
||||
model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make("model") }),
|
||||
system: [SystemPart.make("first")],
|
||||
messages: [Message.user("original")],
|
||||
tools: {},
|
||||
}
|
||||
|
||||
expect(yield* hooks.trigger("session", "request", event)).toBe(event)
|
||||
expect(seen).toEqual(["first", "second"])
|
||||
expect(event.messages).toEqual([Message.user("changed")])
|
||||
})
|
||||
|
||||
await Effect.runPromise(
|
||||
Effect.scoped(program).pipe(
|
||||
Effect.provide(PluginHooks.node.implementation as Layer.Layer<PluginHooks.Service>),
|
||||
),
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Context, Effect, Exit, Fiber, Schema, Stream } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { Plugin as EffectPlugin } from "@opencode-ai/plugin/v2/effect"
|
||||
import { Config as ConfigSchema } from "@opencode-ai/schema/config"
|
||||
import { Plugin } from "@opencode-ai/schema/plugin"
|
||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
@@ -49,7 +49,7 @@ describe("PluginV2", () => {
|
||||
.pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped({ startImmediately: true }))
|
||||
|
||||
const managed = () =>
|
||||
define({
|
||||
EffectPlugin.define({
|
||||
id: "managed",
|
||||
effect: (ctx) =>
|
||||
ctx.agent
|
||||
@@ -97,25 +97,40 @@ describe("PluginV2", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("retries the same generation after materialization fails", () =>
|
||||
it.effect("skips failed plugins and loads the rest", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* PluginV2.Service
|
||||
const agents = yield* AgentV2.Service
|
||||
let fail = true
|
||||
const plugin = define({
|
||||
id: "retry",
|
||||
const good = EffectPlugin.define({
|
||||
id: "good",
|
||||
effect: (ctx) =>
|
||||
ctx.agent
|
||||
.transform(() => {
|
||||
if (fail) throw new Error("materialization failed")
|
||||
})
|
||||
.transform((agents) =>
|
||||
agents.update("configured", (agent) => {
|
||||
agent.description = "loaded"
|
||||
}),
|
||||
)
|
||||
.pipe(Effect.asVoid),
|
||||
})
|
||||
const bad = EffectPlugin.define({
|
||||
id: "bad",
|
||||
effect: () => {
|
||||
if (fail) return Effect.die(new Error("materialization failed"))
|
||||
return Effect.void
|
||||
},
|
||||
})
|
||||
|
||||
yield* plugins.activate([{ plugin: good }, { plugin: bad }])
|
||||
expect(yield* plugins.list()).toEqual([{ id: Plugin.ID.make("good") }])
|
||||
expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("loaded")
|
||||
|
||||
expect(Exit.isFailure(yield* plugins.activate([{ plugin }]).pipe(Effect.exit))).toBe(true)
|
||||
fail = false
|
||||
yield* plugins.activate([{ plugin }])
|
||||
|
||||
expect(yield* plugins.list()).toEqual([{ id: Plugin.ID.make("retry") }])
|
||||
yield* plugins.activate([{ plugin: good }, { plugin: bad }])
|
||||
expect(yield* plugins.list()).toEqual([
|
||||
{ id: Plugin.ID.make("good") },
|
||||
{ id: Plugin.ID.make("bad") },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -142,7 +157,7 @@ describe("PluginV2", () => {
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* PluginV2.Service
|
||||
let visible = true
|
||||
const plugin = define({
|
||||
const plugin = EffectPlugin.define({
|
||||
id: "isolated",
|
||||
effect: () =>
|
||||
Effect.serviceOption(Secret).pipe(
|
||||
@@ -161,7 +176,7 @@ describe("PluginV2", () => {
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* PluginV2.Service
|
||||
const registry = yield* ToolRegistry.Service
|
||||
const plugin = define({
|
||||
const plugin = EffectPlugin.define({
|
||||
id: "tool-plugin",
|
||||
effect: (ctx) =>
|
||||
ctx.tool
|
||||
@@ -202,7 +217,7 @@ describe("PluginV2", () => {
|
||||
output: Schema.Struct({ ok: Schema.Boolean }),
|
||||
execute: () => Effect.succeed({ ok: true }),
|
||||
})
|
||||
const plugin = define({
|
||||
const plugin = EffectPlugin.define({
|
||||
id: "grouped-tools",
|
||||
effect: (ctx) =>
|
||||
ctx.tool
|
||||
@@ -234,7 +249,7 @@ describe("PluginV2", () => {
|
||||
after?: { input: unknown; result: unknown; output: unknown }
|
||||
} = {}
|
||||
|
||||
const plugin = define({
|
||||
const plugin = EffectPlugin.define({
|
||||
id: "tool-hooks",
|
||||
effect: (ctx) =>
|
||||
Effect.gen(function* () {
|
||||
@@ -252,19 +267,23 @@ describe("PluginV2", () => {
|
||||
)
|
||||
.pipe(Effect.orDie)
|
||||
|
||||
yield* ctx.tool.execute
|
||||
.before((event) => {
|
||||
seen.before = event.input
|
||||
event.input = { text: "before-mutated" }
|
||||
})
|
||||
yield* ctx.tool
|
||||
.hook("execute.before", (event) =>
|
||||
Effect.sync(() => {
|
||||
seen.before = event.input
|
||||
event.input = { text: "before-mutated" }
|
||||
}),
|
||||
)
|
||||
.pipe(Effect.asVoid)
|
||||
|
||||
yield* ctx.tool.execute
|
||||
.after((event) => {
|
||||
seen.after = { input: event.input, result: event.result, output: event.output }
|
||||
event.result = { type: "text", value: "after-mutated" }
|
||||
event.output = { structured: { rewritten: true }, content: [] }
|
||||
})
|
||||
yield* ctx.tool
|
||||
.hook("execute.after", (event) =>
|
||||
Effect.sync(() => {
|
||||
seen.after = { input: event.input, result: event.result, output: event.output }
|
||||
event.result = { type: "text", value: "after-mutated" }
|
||||
event.output = { structured: { rewritten: true }, content: [] }
|
||||
}),
|
||||
)
|
||||
.pipe(Effect.asVoid)
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Integration } from "@opencode-ai/core/integration"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { Npm } from "@opencode-ai/core/npm"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { PluginHooks } from "@opencode-ai/core/plugin/hooks"
|
||||
import { PluginRuntime } from "@opencode-ai/core/plugin/runtime"
|
||||
import { Reference } from "@opencode-ai/core/reference"
|
||||
import { SkillV2 } from "@opencode-ai/core/skill"
|
||||
@@ -46,6 +47,7 @@ export const PluginTestLayer = AppNodeBuilder.build(
|
||||
CommandV2.node,
|
||||
Integration.node,
|
||||
PluginRuntime.node,
|
||||
PluginHooks.node,
|
||||
Reference.node,
|
||||
SkillV2.node,
|
||||
ToolHooks.node,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { Plugin } from "@opencode-ai/plugin/v2/effect"
|
||||
import { Effect } from "effect"
|
||||
|
||||
export default define({
|
||||
export default Plugin.define({
|
||||
id: "config-effect-plugin",
|
||||
effect: (ctx) =>
|
||||
ctx.agent
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { define } from "@opencode-ai/plugin/v2/promise"
|
||||
import { Plugin } from "@opencode-ai/plugin/v2"
|
||||
|
||||
export default define({
|
||||
export default Plugin.define({
|
||||
id: "config-promise-plugin",
|
||||
setup: async (ctx) => {
|
||||
await ctx.agent.transform((agents) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { Plugin } from "@opencode-ai/plugin/v2/effect"
|
||||
import { Effect } from "effect"
|
||||
|
||||
export default define({
|
||||
export default Plugin.define({
|
||||
id: "failing-plugin",
|
||||
effect: () => Effect.die("plugin failed"),
|
||||
})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { Plugin } from "@opencode-ai/plugin/v2/effect"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { Effect } from "effect"
|
||||
|
||||
export default define({
|
||||
export default Plugin.define({
|
||||
id: "variant-source",
|
||||
effect: (ctx) =>
|
||||
ctx.catalog
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { Credential } from "@opencode-ai/core/credential"
|
||||
@@ -19,8 +19,7 @@ export function host(overrides: Overrides = {}): PluginContext {
|
||||
reload: () => Effect.die("unused agent.reload"),
|
||||
},
|
||||
aisdk: overrides.aisdk ?? {
|
||||
sdk: () => Effect.die("unused aisdk.sdk"),
|
||||
language: () => Effect.die("unused aisdk.language"),
|
||||
hook: () => Effect.die("unused aisdk.hook"),
|
||||
},
|
||||
catalog: overrides.catalog ?? {
|
||||
provider: {
|
||||
@@ -45,11 +44,15 @@ export function host(overrides: Overrides = {}): PluginContext {
|
||||
integration: overrides.integration ?? {
|
||||
list: () => Effect.die("unused integration.list"),
|
||||
get: () => Effect.die("unused integration.get"),
|
||||
connectKey: () => Effect.die("unused integration.connectKey"),
|
||||
connectOauth: () => Effect.die("unused integration.connectOauth"),
|
||||
attemptStatus: () => Effect.die("unused integration.attemptStatus"),
|
||||
attemptComplete: () => Effect.die("unused integration.attemptComplete"),
|
||||
attemptCancel: () => Effect.die("unused integration.attemptCancel"),
|
||||
connect: {
|
||||
key: () => Effect.die("unused integration.connect.key"),
|
||||
oauth: () => Effect.die("unused integration.connect.oauth"),
|
||||
},
|
||||
attempt: {
|
||||
status: () => Effect.die("unused integration.attempt.status"),
|
||||
complete: () => Effect.die("unused integration.attempt.complete"),
|
||||
cancel: () => Effect.die("unused integration.attempt.cancel"),
|
||||
},
|
||||
transform: () => Effect.die("unused integration.transform"),
|
||||
reload: () => Effect.die("unused integration.reload"),
|
||||
connection: {
|
||||
@@ -72,10 +75,7 @@ export function host(overrides: Overrides = {}): PluginContext {
|
||||
},
|
||||
tool: overrides.tool ?? {
|
||||
transform: () => Effect.die("unused tool.transform"),
|
||||
execute: {
|
||||
before: () => Effect.die("unused tool.execute.before"),
|
||||
after: () => Effect.die("unused tool.execute.after"),
|
||||
},
|
||||
hook: () => Effect.die("unused tool.hook"),
|
||||
},
|
||||
session: overrides.session ?? {
|
||||
create: () => Effect.die("unused session.create"),
|
||||
@@ -83,6 +83,7 @@ export function host(overrides: Overrides = {}): PluginContext {
|
||||
prompt: () => Effect.die("unused session.prompt"),
|
||||
command: () => Effect.die("unused session.command"),
|
||||
interrupt: () => Effect.die("unused session.interrupt"),
|
||||
hook: () => Effect.die("unused session.hook"),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -188,11 +189,15 @@ export function integrationHost(integration: Integration.Interface): PluginConte
|
||||
return {
|
||||
list: () => Effect.die("unused integration.list"),
|
||||
get: () => Effect.die("unused integration.get"),
|
||||
connectKey: () => Effect.die("unused integration.connectKey"),
|
||||
connectOauth: () => Effect.die("unused integration.connectOauth"),
|
||||
attemptStatus: () => Effect.die("unused integration.attemptStatus"),
|
||||
attemptComplete: () => Effect.die("unused integration.attemptComplete"),
|
||||
attemptCancel: () => Effect.die("unused integration.attemptCancel"),
|
||||
connect: {
|
||||
key: () => Effect.die("unused integration.connect.key"),
|
||||
oauth: () => Effect.die("unused integration.connect.oauth"),
|
||||
},
|
||||
attempt: {
|
||||
status: () => Effect.die("unused integration.attempt.status"),
|
||||
complete: () => Effect.die("unused integration.attempt.complete"),
|
||||
cancel: () => Effect.die("unused integration.attempt.cancel"),
|
||||
},
|
||||
reload: integration.reload,
|
||||
connection: {
|
||||
active: (id) => integration.connection.active(Integration.ID.make(id)),
|
||||
|
||||
@@ -4,7 +4,7 @@ import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { PluginPromise } from "@opencode-ai/core/plugin/promise"
|
||||
import { define } from "@opencode-ai/plugin/v2/promise"
|
||||
import { Plugin } from "@opencode-ai/plugin/v2"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
@@ -16,7 +16,7 @@ describe("fromPromise", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
const seen: string[] = []
|
||||
const promisePlugin = define({
|
||||
const promisePlugin = Plugin.define({
|
||||
id: "promise-client-reads",
|
||||
setup: async (ctx) => {
|
||||
const results = await Promise.all([
|
||||
@@ -46,7 +46,7 @@ describe("fromPromise", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
|
||||
const promisePlugin = define({
|
||||
const promisePlugin = Plugin.define({
|
||||
id: "promise-example",
|
||||
setup: async (ctx) => {
|
||||
expect(ctx.options.mode).toBe("strict")
|
||||
@@ -75,7 +75,7 @@ describe("fromPromise", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
|
||||
const promisePlugin = define({
|
||||
const promisePlugin = Plugin.define({
|
||||
id: "promise-dispose",
|
||||
setup: async (ctx) => {
|
||||
const registration = await ctx.agent.transform((draft) => {
|
||||
|
||||
@@ -110,6 +110,25 @@ test("provider state uses the route provider instead of the catalog provider", a
|
||||
})
|
||||
})
|
||||
|
||||
test("reasoning state from an empty delta is retained at reasoning end", async () => {
|
||||
const { published, publisher } = capture()
|
||||
await Effect.runPromise(publisher.publish(LLMEvent.reasoningStart({ id: "reasoning" })))
|
||||
await Effect.runPromise(
|
||||
publisher.publish(
|
||||
LLMEvent.reasoningDelta({
|
||||
id: "reasoning",
|
||||
text: "",
|
||||
providerMetadata: { openai: { signature: "signed" } },
|
||||
}),
|
||||
),
|
||||
)
|
||||
await Effect.runPromise(publisher.publish(LLMEvent.reasoningEnd({ id: "reasoning" })))
|
||||
|
||||
expect(published.find((event) => event.type === "session.reasoning.ended.1")?.data).toMatchObject({
|
||||
state: { signature: "signed" },
|
||||
})
|
||||
})
|
||||
|
||||
test("binary failure emits no success event", async () => {
|
||||
const { published, publisher } = capture()
|
||||
await Effect.runPromise(publisher.publish(call))
|
||||
|
||||
@@ -82,7 +82,7 @@ describe("ToolRegistry", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("selects one edit tool family for each model", () =>
|
||||
it.effect("materializes all permission-eligible edit tools before request policy", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* ToolRegistry.Service
|
||||
yield* service.register({
|
||||
@@ -96,10 +96,13 @@ describe("ToolRegistry", () => {
|
||||
.materialize({ model })
|
||||
.pipe(Effect.map((materialized) => materialized.definitions.map((tool) => tool.name)))
|
||||
|
||||
expect(yield* names({ id: "gpt-5", provider: "openai" })).toEqual(["read", "patch"])
|
||||
expect(yield* names({ id: "gpt-4o", provider: "opencode" })).toEqual(["read", "patch"])
|
||||
expect(yield* names({ id: "computer-use-preview", provider: "openai" })).toEqual(["read", "patch"])
|
||||
expect(yield* names({ id: "claude-sonnet-4", provider: "anthropic" })).toEqual(["read", "edit", "write"])
|
||||
expect(yield* names({ id: "gpt-5", provider: "openai" })).toEqual(["read", "edit", "write", "patch"])
|
||||
expect(yield* names({ id: "claude-sonnet-4", provider: "anthropic" })).toEqual([
|
||||
"read",
|
||||
"edit",
|
||||
"write",
|
||||
"patch",
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
+1865
-689
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
import { isAbsolute, join } from "node:path"
|
||||
import { Effect, FileSystem, PlatformError, Schema, SchemaAST, SchemaRepresentation } from "effect"
|
||||
import { Context, Effect, FileSystem, PlatformError, Schema, SchemaAST, SchemaRepresentation } from "effect"
|
||||
import { HttpMethod, type HttpRouter } from "effect/unstable/http"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import { format } from "prettier"
|
||||
|
||||
export type InputField = {
|
||||
@@ -35,8 +35,6 @@ export type Contract = {
|
||||
readonly groups: ReadonlyArray<Group>
|
||||
}
|
||||
|
||||
export type EndpointName = string | readonly [string, ...Array<string>]
|
||||
|
||||
export class GenerationError extends Schema.TaggedErrorClass<GenerationError>()("GenerationError", {
|
||||
reason: Schema.String,
|
||||
}) {
|
||||
@@ -89,7 +87,6 @@ export function compile<Id extends string, Groups extends HttpApiGroup.Any>(
|
||||
api: HttpApi.HttpApi<Id, Groups>,
|
||||
options?: {
|
||||
readonly groupNames?: Readonly<Record<string, string>>
|
||||
readonly endpointNames?: Readonly<Record<string, EndpointName>>
|
||||
readonly omitEndpoints?: ReadonlySet<string>
|
||||
},
|
||||
): Contract {
|
||||
@@ -151,8 +148,11 @@ export function compile<Id extends string, Groups extends HttpApiGroup.Any>(
|
||||
for (const [path, schema] of schemaPaths) assertPortable(schema, path, portable)
|
||||
}
|
||||
|
||||
const clientPath = normalizeClientPath(
|
||||
options?.endpointNames?.[endpoint.name] ?? clientEndpointName(endpoint.name),
|
||||
const clientPath = clientEndpointPath(
|
||||
group.identifier,
|
||||
Context.getOrElse(endpoint.annotations, OpenApi.Identifier, () =>
|
||||
group.topLevel ? endpoint.name : `${group.identifier}.${endpoint.name}`,
|
||||
),
|
||||
)
|
||||
endpoints.push({
|
||||
group: groupName,
|
||||
@@ -699,21 +699,17 @@ function clientOperationKey(group: Group, endpoint: Endpoint) {
|
||||
return [group.identifier, ...endpoint.clientPath].join(".")
|
||||
}
|
||||
|
||||
function clientEndpointName(name: string) {
|
||||
return name.slice(name.lastIndexOf(".") + 1)
|
||||
}
|
||||
|
||||
function normalizeClientPath(path: EndpointName) {
|
||||
const result = typeof path === "string" ? [path] : [...path]
|
||||
function clientEndpointPath(group: string, name: string) {
|
||||
const parts = name.split(".")
|
||||
if (parts[0] === "v2") parts.shift()
|
||||
const index = parts.lastIndexOf(group.slice(group.lastIndexOf(".") + 1))
|
||||
const result = index < 0 ? parts : parts.slice(index + 1)
|
||||
if (result.length === 0 || result.some((part) => part.length === 0)) {
|
||||
throw new GenerationError({ reason: "Client endpoint path must contain non-empty names" })
|
||||
}
|
||||
if (result.some((part) => part === "__proto__")) {
|
||||
throw new GenerationError({ reason: "Client endpoint path cannot contain __proto__" })
|
||||
}
|
||||
if (typeof path !== "string" && result.some((part) => part.includes("."))) {
|
||||
throw new GenerationError({ reason: "Nested client endpoint path segments cannot contain dots" })
|
||||
}
|
||||
return result as [string, ...Array<string>]
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { mkdtemp, rm } from "node:fs/promises"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { Effect, FileSystem, Schema, SchemaAST, SchemaGetter } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import { format } from "prettier"
|
||||
import {
|
||||
compile as compileContract,
|
||||
@@ -139,48 +139,50 @@ describe("HttpApiCodegen.generate", () => {
|
||||
expect(contract.groups[0]?.endpoints[0]?.operation).toMatchObject({ group: "sessions", name: "get" })
|
||||
})
|
||||
|
||||
test("supports explicit public endpoint names", () => {
|
||||
test("derives nested paths from OpenAPI operation IDs", () => {
|
||||
const source = HttpApi.make("test").add(
|
||||
HttpApiGroup.make("server.permission")
|
||||
.add(HttpApiEndpoint.get("permission.request.list", "/request", { success: Schema.String }))
|
||||
.add(HttpApiEndpoint.get("session.permission.list", "/session", { success: Schema.String })),
|
||||
HttpApiGroup.make("server.session").add(
|
||||
HttpApiEndpoint.get("internal.stage", "/session/revert/stage", { success: Schema.String }).annotateMerge(
|
||||
OpenApi.annotations({ identifier: "v2.session.revert.stage" }),
|
||||
),
|
||||
),
|
||||
)
|
||||
const contract = compileContract(source, {
|
||||
endpointNames: { "permission.request.list": "listRequests" },
|
||||
})
|
||||
const contract = compileContract(source, { groupNames: { "server.session": "session" } })
|
||||
|
||||
expect(contract.groups[0]?.endpoints.map((endpoint) => endpoint.operation.name)).toEqual(["listRequests", "list"])
|
||||
expect(contract.groups[0]?.endpoints[0]?.clientPath).toEqual(["revert", "stage"])
|
||||
expect(OpenApi.fromApi(source).paths["/session/revert/stage"]?.get?.operationId).toBe("v2.session.revert.stage")
|
||||
})
|
||||
|
||||
test("supports explicit nested endpoint paths while string aliases remain flat", () => {
|
||||
test("uses nested OpenAPI operation IDs across emitters", () => {
|
||||
const source = HttpApi.make("test").add(
|
||||
HttpApiGroup.make("server.session")
|
||||
.add(HttpApiEndpoint.get("session.instructions.list", "/session/instructions", { success: Schema.String }))
|
||||
.add(HttpApiEndpoint.put("session.instructions.put", "/session/instructions", { success: Schema.String }))
|
||||
.add(HttpApiEndpoint.delete("session.instructions.remove", "/session/instructions", { success: Schema.String }))
|
||||
.add(HttpApiEndpoint.get("session.messages", "/session/message", { success: Schema.String })),
|
||||
.add(
|
||||
HttpApiEndpoint.get("list", "/session/instructions", { success: Schema.String }).annotateMerge(
|
||||
OpenApi.annotations({ identifier: "v2.session.instructions.list" }),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.put("put", "/session/instructions", { success: Schema.String }).annotateMerge(
|
||||
OpenApi.annotations({ identifier: "v2.session.instructions.put" }),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.delete("remove", "/session/instructions", { success: Schema.String }).annotateMerge(
|
||||
OpenApi.annotations({ identifier: "v2.session.instructions.remove" }),
|
||||
),
|
||||
),
|
||||
)
|
||||
const contract = compileContract(source, {
|
||||
groupNames: { "server.session": "session" },
|
||||
endpointNames: {
|
||||
"session.instructions.list": ["instructions", "list"],
|
||||
"session.instructions.put": ["instructions", "put"],
|
||||
"session.instructions.remove": ["instructions", "remove"],
|
||||
"session.messages": "instructions.flat",
|
||||
},
|
||||
})
|
||||
const contract = compileContract(source, { groupNames: { "server.session": "session" } })
|
||||
|
||||
expect(contract.groups[0]?.endpoints.map((endpoint) => endpoint.clientPath)).toEqual([
|
||||
["instructions", "list"],
|
||||
["instructions", "put"],
|
||||
["instructions", "remove"],
|
||||
["instructions.flat"],
|
||||
])
|
||||
expect(contract.groups[0]?.endpoints.map((endpoint) => endpoint.operation.name)).toEqual([
|
||||
"instructions.list",
|
||||
"instructions.put",
|
||||
"instructions.remove",
|
||||
"instructions.flat",
|
||||
])
|
||||
|
||||
const promise = emitPromise(contract, {
|
||||
@@ -196,7 +198,6 @@ describe("HttpApiCodegen.generate", () => {
|
||||
expect(promiseClient).toContain('"session": { "instructions": { "list": (requestOptions?: RequestOptions)')
|
||||
expect(promiseClient).toContain('"put": (requestOptions?: RequestOptions)')
|
||||
expect(promiseClient).toContain('"remove": (requestOptions?: RequestOptions)')
|
||||
expect(promiseClient).toContain('"instructions.flat": (requestOptions?: RequestOptions)')
|
||||
expect(promiseTypes).toContain('import type { InstructionListWire } from "./instruction-list-wire"')
|
||||
expect(promiseTypes).toContain("export type SessionInstructionsListOutput = InstructionListWire")
|
||||
expect(promiseTypes).toContain("export type SessionInstructionsPutOutput = string")
|
||||
@@ -204,12 +205,12 @@ describe("HttpApiCodegen.generate", () => {
|
||||
|
||||
const effect = emitEffect(contract)
|
||||
expect(effect.files.find((file) => file.path === "session.ts")?.content).toContain(
|
||||
'"instructions": { "list": Endpoint0(raw), "put": Endpoint1(raw), "remove": Endpoint2(raw) }, "instructions.flat": Endpoint3(raw)',
|
||||
'"instructions": { "list": Endpoint0(raw), "put": Endpoint1(raw), "remove": Endpoint2(raw) }',
|
||||
)
|
||||
|
||||
const imported = emitEffectImported(contract, { module: "@example/api", api: "Api" })
|
||||
expect(imported.files.find((file) => file.path === "client.ts")?.content).toContain(
|
||||
'"instructions": { "list": Endpoint0_0(raw), "put": Endpoint0_1(raw), "remove": Endpoint0_2(raw) }, "instructions.flat": Endpoint0_3(raw)',
|
||||
'"instructions": { "list": Endpoint0_0(raw), "put": Endpoint0_1(raw), "remove": Endpoint0_2(raw) }',
|
||||
)
|
||||
|
||||
const shape = emitEffectShape(contract, { module: "@example/api", api: "Api" })
|
||||
@@ -217,25 +218,28 @@ describe("HttpApiCodegen.generate", () => {
|
||||
expect(apiShape).toContain('readonly "instructions": { readonly "list": SessionInstructionsListOperation<E>')
|
||||
expect(apiShape).toContain('readonly "put": SessionInstructionsPutOperation<E>')
|
||||
expect(apiShape).toContain('readonly "remove": SessionInstructionsRemoveOperation<E>')
|
||||
expect(apiShape).toContain('readonly "instructions.flat": SessionInstructionsFlatOperation<E>')
|
||||
})
|
||||
|
||||
test("executes nested Promise endpoint aliases", async () => {
|
||||
test("executes nested Promise operation IDs", async () => {
|
||||
const source = HttpApi.make("test").add(
|
||||
HttpApiGroup.make("session")
|
||||
.add(HttpApiEndpoint.get("instructions.list", "/session/instructions", { success: Schema.String }))
|
||||
.add(HttpApiEndpoint.put("instructions.put", "/session/instructions", { success: Schema.String }))
|
||||
.add(HttpApiEndpoint.delete("instructions.remove", "/session/instructions", { success: Schema.String })),
|
||||
)
|
||||
const output = emitPromise(
|
||||
compileContract(source, {
|
||||
endpointNames: {
|
||||
"instructions.list": ["instructions", "list"],
|
||||
"instructions.put": ["instructions", "put"],
|
||||
"instructions.remove": ["instructions", "remove"],
|
||||
},
|
||||
}),
|
||||
.add(
|
||||
HttpApiEndpoint.get("list", "/session/instructions", { success: Schema.String }).annotateMerge(
|
||||
OpenApi.annotations({ identifier: "session.instructions.list" }),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.put("put", "/session/instructions", { success: Schema.String }).annotateMerge(
|
||||
OpenApi.annotations({ identifier: "session.instructions.put" }),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.delete("remove", "/session/instructions", { success: Schema.String }).annotateMerge(
|
||||
OpenApi.annotations({ identifier: "session.instructions.remove" }),
|
||||
),
|
||||
),
|
||||
)
|
||||
const output = emitPromise(compileContract(source))
|
||||
const directory = await mkdtemp(join(tmpdir(), "opencode-httpapi-codegen-"))
|
||||
const methods: Array<string> = []
|
||||
|
||||
@@ -262,59 +266,67 @@ describe("HttpApiCodegen.generate", () => {
|
||||
test("rejects duplicate and leaf-namespace endpoint paths", () => {
|
||||
const source = HttpApi.make("test").add(
|
||||
HttpApiGroup.make("session")
|
||||
.add(HttpApiEndpoint.get("first", "/first", { success: Schema.String }))
|
||||
.add(HttpApiEndpoint.get("second", "/second", { success: Schema.String })),
|
||||
.add(
|
||||
HttpApiEndpoint.get("first", "/first", { success: Schema.String }).annotateMerge(
|
||||
OpenApi.annotations({ identifier: "session.instructions.list" }),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("second", "/second", { success: Schema.String }).annotateMerge(
|
||||
OpenApi.annotations({ identifier: "session.instructions.list" }),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
expect(() =>
|
||||
compileContract(source, { endpointNames: { first: ["instructions", "list"], second: ["instructions", "list"] } }),
|
||||
).toThrow("Client endpoint name collision: session.instructions.list")
|
||||
expect(() =>
|
||||
compileContract(source, { endpointNames: { first: "instructions", second: ["instructions", "list"] } }),
|
||||
).toThrow("Client endpoint name collision: session.instructions.list")
|
||||
expect(() => compileContract(source)).toThrow("Client endpoint name collision: session.instructions.list")
|
||||
})
|
||||
|
||||
test("rejects nested root collisions across top-level groups", () => {
|
||||
const source = HttpApi.make("test")
|
||||
.add(
|
||||
HttpApiGroup.make("first", { topLevel: true }).add(
|
||||
HttpApiEndpoint.get("first.list", "/first", { success: Schema.String }),
|
||||
HttpApiEndpoint.get("first.list", "/first", { success: Schema.String }).annotateMerge(
|
||||
OpenApi.annotations({ identifier: "instructions.list" }),
|
||||
),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiGroup.make("second", { topLevel: true }).add(
|
||||
HttpApiEndpoint.get("second.put", "/second", { success: Schema.String }),
|
||||
HttpApiEndpoint.get("second.put", "/second", { success: Schema.String }).annotateMerge(
|
||||
OpenApi.annotations({ identifier: "instructions.put" }),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
expect(() =>
|
||||
compileContract(source, {
|
||||
endpointNames: { "first.list": ["instructions", "list"], "second.put": ["instructions", "put"] },
|
||||
}),
|
||||
).toThrow("Client name collision: instructions")
|
||||
expect(() => compileContract(source)).toThrow("Client name collision: instructions")
|
||||
})
|
||||
|
||||
test("rejects nested paths that collide after type-name normalization", () => {
|
||||
const source = HttpApi.make("test").add(
|
||||
HttpApiGroup.make("session")
|
||||
.add(HttpApiEndpoint.get("first", "/first", { success: Schema.String }))
|
||||
.add(HttpApiEndpoint.get("second", "/second", { success: Schema.String })),
|
||||
.add(
|
||||
HttpApiEndpoint.get("first", "/first", { success: Schema.String }).annotateMerge(
|
||||
OpenApi.annotations({ identifier: "session.foo.bar" }),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("second", "/second", { success: Schema.String }).annotateMerge(
|
||||
OpenApi.annotations({ identifier: "session.foo-bar" }),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
expect(() => compileContract(source, { endpointNames: { first: ["foo", "bar"], second: "foo-bar" } })).toThrow(
|
||||
"Client endpoint type collision: SessionFooBar",
|
||||
)
|
||||
expect(() => compileContract(source)).toThrow("Client endpoint type collision: SessionFooBar")
|
||||
})
|
||||
|
||||
test("rejects ambiguous and prototype-mutating nested path segments", () => {
|
||||
const source = api(HttpApiEndpoint.get("get", "/session", { success: Schema.String }))
|
||||
const source = api(
|
||||
HttpApiEndpoint.get("get", "/session", { success: Schema.String }).annotateMerge(
|
||||
OpenApi.annotations({ identifier: "session.__proto__.get" }),
|
||||
),
|
||||
)
|
||||
|
||||
expect(() => compileContract(source, { endpointNames: { get: ["a.b", "get"] } })).toThrow(
|
||||
"Nested client endpoint path segments cannot contain dots",
|
||||
)
|
||||
expect(() => compileContract(source, { endpointNames: { get: ["__proto__", "get"] } })).toThrow(
|
||||
"Client endpoint path cannot contain __proto__",
|
||||
)
|
||||
expect(() => compileContract(source)).toThrow("Client endpoint path cannot contain __proto__")
|
||||
})
|
||||
|
||||
test("rejects normalized group, operation-key, and group prototype collisions", () => {
|
||||
@@ -324,20 +336,38 @@ describe("HttpApiCodegen.generate", () => {
|
||||
expect(() => compileContract(normalized)).toThrow("Client group type collision: FooBar")
|
||||
|
||||
const endpointType = HttpApi.make("test")
|
||||
.add(HttpApiGroup.make("foo").add(HttpApiEndpoint.get("first", "/first", { success: Schema.String })))
|
||||
.add(HttpApiGroup.make("fooBar").add(HttpApiEndpoint.get("second", "/second", { success: Schema.String })))
|
||||
expect(() =>
|
||||
compileContract(endpointType, {
|
||||
endpointNames: { first: ["bar", "baz"], second: ["baz"] },
|
||||
}),
|
||||
).toThrow("Client endpoint type collision: FooBarBaz")
|
||||
.add(
|
||||
HttpApiGroup.make("foo").add(
|
||||
HttpApiEndpoint.get("first", "/first", { success: Schema.String }).annotateMerge(
|
||||
OpenApi.annotations({ identifier: "foo.bar.baz" }),
|
||||
),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiGroup.make("fooBar").add(
|
||||
HttpApiEndpoint.get("second", "/second", { success: Schema.String }).annotateMerge(
|
||||
OpenApi.annotations({ identifier: "fooBar.baz" }),
|
||||
),
|
||||
),
|
||||
)
|
||||
expect(() => compileContract(endpointType)).toThrow("Client endpoint type collision: FooBarBaz")
|
||||
|
||||
const operationKey = HttpApi.make("test")
|
||||
.add(HttpApiGroup.make("a.b").add(HttpApiEndpoint.get("get", "/first", { success: Schema.String })))
|
||||
.add(HttpApiGroup.make("a").add(HttpApiEndpoint.get("b.c", "/second", { success: Schema.String })))
|
||||
expect(() => compileContract(operationKey, { endpointNames: { get: "c", "b.c": ["b", "c"] } })).toThrow(
|
||||
"Client operation key collision: a.b.c",
|
||||
)
|
||||
.add(
|
||||
HttpApiGroup.make("a.b").add(
|
||||
HttpApiEndpoint.get("get", "/first", { success: Schema.String }).annotateMerge(
|
||||
OpenApi.annotations({ identifier: "a.b.c" }),
|
||||
),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiGroup.make("a").add(
|
||||
HttpApiEndpoint.get("b.c", "/second", { success: Schema.String }).annotateMerge(
|
||||
OpenApi.annotations({ identifier: "a.b.c" }),
|
||||
),
|
||||
),
|
||||
)
|
||||
expect(() => compileContract(operationKey)).toThrow("Client operation key collision: a.b.c")
|
||||
|
||||
const prototype = HttpApi.make("test").add(
|
||||
HttpApiGroup.make("session").add(HttpApiEndpoint.get("get", "/session", { success: Schema.String })),
|
||||
|
||||
@@ -14,10 +14,9 @@
|
||||
"./tool": "./src/tool.ts",
|
||||
"./tui": "./src/tui.ts",
|
||||
"./v2/effect": "./src/v2/effect/index.ts",
|
||||
"./v2/effect/integration": "./src/v2/effect/integration.ts",
|
||||
"./v2/effect/plugin": "./src/v2/effect/plugin.ts",
|
||||
"./v2/effect/tool": "./src/v2/effect/tool.ts",
|
||||
"./v2/promise": "./src/v2/promise/index.ts"
|
||||
"./v2/effect/*": "./src/v2/effect/*.ts",
|
||||
"./v2": "./src/v2/promise/index.ts",
|
||||
"./v2/*": "./src/v2/promise/*.ts"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -176,7 +176,7 @@ Both use the same low-level scoped registration registry, but consumers invoke t
|
||||
|
||||
```ts
|
||||
ctx.tool.transform(...) // replayed to build effective tool registry state
|
||||
ctx.tool.hook(...) // invoked at a live tool operation boundary
|
||||
ctx.tool.hook(...) // invoked at a live tool operation boundary
|
||||
```
|
||||
|
||||
The shared low-level machinery owns registration order, scope cleanup, disposal, and snapshots. Each domain owns when its transforms or runtime hooks execute.
|
||||
|
||||
@@ -5,15 +5,13 @@ The Effect plugin API grants plugins two in-process capabilities:
|
||||
- `hook` installs behavior at an OpenCode extension point.
|
||||
- `reload` reruns every transform hook for a stateful domain.
|
||||
|
||||
The public server client will be exposed separately. It is intentionally not part of `PluginContext` yet.
|
||||
|
||||
## Defining A Plugin
|
||||
|
||||
```ts
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { Plugin } from "@opencode-ai/plugin/v2/effect"
|
||||
import { Effect } from "effect"
|
||||
|
||||
export const Plugin = define({
|
||||
export default Plugin.define({
|
||||
id: "example",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.catalog.transform((catalog) => {
|
||||
@@ -25,7 +23,7 @@ export const Plugin = define({
|
||||
})
|
||||
```
|
||||
|
||||
Plugin setup registers hooks imperatively. It does not return a hook object.
|
||||
Plugin setup registers hooks imperatively through each domain's `hook` method.
|
||||
|
||||
Configuration supplied for the plugin is available as `ctx.options`.
|
||||
|
||||
@@ -64,7 +62,8 @@ Runtime hooks intercept live operations rather than rebuilding domain state:
|
||||
|
||||
```ts
|
||||
yield *
|
||||
ctx.aisdk.sdk(
|
||||
ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (event) {
|
||||
if (event.package !== "@ai-sdk/xai") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/xai"))
|
||||
@@ -73,7 +72,7 @@ yield *
|
||||
)
|
||||
|
||||
yield *
|
||||
ctx.aisdk.language((event) => {
|
||||
ctx.aisdk.hook("language", (event) => {
|
||||
if (event.model.providerID !== "xai") return
|
||||
event.language = event.sdk.responses(event.model.api.id)
|
||||
})
|
||||
@@ -81,6 +80,16 @@ yield *
|
||||
|
||||
Hooks run sequentially in registration order. Later hooks observe mutations made by earlier hooks.
|
||||
|
||||
Session request context is mutable immediately before provider dispatch:
|
||||
|
||||
```ts
|
||||
yield *
|
||||
ctx.session.hook("request", (event) => {
|
||||
event.tools.read.description = "Read a file using narrow line ranges."
|
||||
delete event.tools.write
|
||||
})
|
||||
```
|
||||
|
||||
## Reloading A Domain
|
||||
|
||||
When data captured by a transform changes, reload the affected domain:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { AgentApi } from "@opencode-ai/client/effect/api"
|
||||
import type { AgentV2Info } from "@opencode-ai/sdk/v2/types"
|
||||
import type { Effect } from "effect"
|
||||
import type { TransformHook } from "./registration.js"
|
||||
import type { Transform } from "./registration.js"
|
||||
|
||||
export interface AgentDraft {
|
||||
list(): readonly AgentV2Info[]
|
||||
@@ -11,7 +11,7 @@ export interface AgentDraft {
|
||||
remove(id: string): void
|
||||
}
|
||||
|
||||
export interface AgentHooks extends AgentApi<unknown> {
|
||||
readonly transform: TransformHook<AgentDraft>
|
||||
export interface AgentDomain extends AgentApi<unknown> {
|
||||
readonly transform: Transform<AgentDraft>
|
||||
readonly reload: () => Effect.Effect<void>
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import type { Model } from "@opencode-ai/schema/model"
|
||||
import type { Hooks } from "./registration.js"
|
||||
|
||||
export type AISDKHooks = Hooks<{
|
||||
export interface AISDKHooks {
|
||||
sdk: {
|
||||
readonly model: Model.Info
|
||||
readonly package: string
|
||||
@@ -15,4 +15,8 @@ export type AISDKHooks = Hooks<{
|
||||
readonly options: Record<string, any>
|
||||
language?: LanguageModelV3
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
export interface AISDKDomain {
|
||||
readonly hook: Hooks<AISDKHooks>
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ModelV2Info, ProviderV2Info } from "@opencode-ai/sdk/v2/types"
|
||||
import type { CatalogApi } from "@opencode-ai/client/effect/api"
|
||||
import type { Effect } from "effect"
|
||||
import type { TransformHook } from "./registration.js"
|
||||
import type { Transform } from "./registration.js"
|
||||
|
||||
export interface CatalogProviderRecord {
|
||||
readonly provider: ProviderV2Info
|
||||
@@ -26,7 +26,7 @@ export interface CatalogDraft {
|
||||
}
|
||||
}
|
||||
|
||||
export interface CatalogHooks extends CatalogApi<unknown> {
|
||||
readonly transform: TransformHook<CatalogDraft>
|
||||
export interface CatalogDomain extends CatalogApi<unknown> {
|
||||
readonly transform: Transform<CatalogDraft>
|
||||
readonly reload: () => Effect.Effect<void>
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { CommandV2Info } from "@opencode-ai/sdk/v2/types"
|
||||
import type { CommandApi } from "@opencode-ai/client/effect/api"
|
||||
import type { Effect } from "effect"
|
||||
import type { TransformHook } from "./registration.js"
|
||||
import type { Transform } from "./registration.js"
|
||||
|
||||
export interface CommandDraft {
|
||||
list(): readonly CommandV2Info[]
|
||||
@@ -10,7 +10,7 @@ export interface CommandDraft {
|
||||
remove(name: string): void
|
||||
}
|
||||
|
||||
export interface CommandHooks extends CommandApi<unknown> {
|
||||
readonly transform: TransformHook<CommandDraft>
|
||||
export interface CommandDomain extends CommandApi<unknown> {
|
||||
readonly transform: Transform<CommandDraft>
|
||||
readonly reload: () => Effect.Effect<void>
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { PluginOptions } from "../options.js"
|
||||
import type { AgentHooks } from "./agent.js"
|
||||
import type { AISDKHooks } from "./aisdk.js"
|
||||
import type { CatalogHooks } from "./catalog.js"
|
||||
import type { CommandHooks } from "./command.js"
|
||||
import type { EventHooks } from "./event.js"
|
||||
import type { IntegrationHooks } from "./integration.js"
|
||||
import type { PluginDomain } from "./plugin.js"
|
||||
import type { ReferenceHooks } from "./reference.js"
|
||||
import type { SkillHooks } from "./skill.js"
|
||||
import type { ToolDomain } from "./tool.js"
|
||||
import type { SessionHooks } from "./runtime.js"
|
||||
|
||||
export interface PluginContext {
|
||||
readonly options: PluginOptions
|
||||
readonly agent: AgentHooks
|
||||
readonly aisdk: AISDKHooks
|
||||
readonly catalog: CatalogHooks
|
||||
readonly command: CommandHooks
|
||||
readonly event: EventHooks
|
||||
readonly integration: IntegrationHooks
|
||||
readonly plugin: PluginDomain
|
||||
readonly reference: ReferenceHooks
|
||||
readonly skill: SkillHooks
|
||||
readonly tool: ToolDomain
|
||||
readonly session: SessionHooks
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
import type { EventApi } from "@opencode-ai/client/effect/api"
|
||||
|
||||
export interface EventHooks extends Pick<EventApi<unknown>, "subscribe"> {}
|
||||
export interface EventDomain extends Pick<EventApi<unknown>, "subscribe"> {}
|
||||
|
||||
@@ -1,17 +1,4 @@
|
||||
export type { PluginContext } from "./context.js"
|
||||
export { define } from "./plugin.js"
|
||||
export type { Plugin, PluginDomain } from "./plugin.js"
|
||||
export type { AgentDraft, AgentHooks } from "./agent.js"
|
||||
export type { AISDKHooks } from "./aisdk.js"
|
||||
export type { CatalogDraft, CatalogHooks, CatalogProviderRecord } from "./catalog.js"
|
||||
export type { CommandDraft, CommandHooks } from "./command.js"
|
||||
export type { EventHooks } from "./event.js"
|
||||
export type { IntegrationDraft, IntegrationHooks, IntegrationMethodRegistration } from "./integration.js"
|
||||
export type { ReferenceDraft, ReferenceHooks } from "./reference.js"
|
||||
export type { SkillDraft, SkillHooks } from "./skill.js"
|
||||
export * as Tool from "./tool.js"
|
||||
export type { ToolDomain, ToolDraft, ToolExecuteBeforeEvent, ToolExecuteAfterEvent } from "./tool.js"
|
||||
export type { SessionHooks } from "./runtime.js"
|
||||
export * as Plugin from "./plugin.js"
|
||||
|
||||
export { Agent } from "@opencode-ai/schema/agent"
|
||||
export { Command } from "@opencode-ai/schema/command"
|
||||
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
} from "@opencode-ai/sdk/v2/types"
|
||||
import type { IntegrationApi } from "@opencode-ai/client/effect/api"
|
||||
import type { Effect, Scope } from "effect"
|
||||
import type { TransformHook } from "./registration.js"
|
||||
import type { Transform } from "./registration.js"
|
||||
|
||||
export type IntegrationOAuthAuthorization = {
|
||||
readonly url: string
|
||||
@@ -56,8 +56,8 @@ export interface IntegrationDraft {
|
||||
}
|
||||
}
|
||||
|
||||
export interface IntegrationHooks extends IntegrationApi<unknown> {
|
||||
readonly transform: TransformHook<IntegrationDraft>
|
||||
export interface IntegrationDomain extends IntegrationApi<unknown> {
|
||||
readonly transform: Transform<IntegrationDraft>
|
||||
readonly reload: () => Effect.Effect<void>
|
||||
readonly connection: {
|
||||
readonly active: (integrationID: string) => Effect.Effect<ConnectionInfo | undefined>
|
||||
|
||||
@@ -1,14 +1,37 @@
|
||||
import type { PluginApi } from "@opencode-ai/client/effect/api"
|
||||
import type { Effect, Scope } from "effect"
|
||||
import type { PluginContext } from "./context.js"
|
||||
import type { PluginOptions } from "../options.js"
|
||||
import type { AgentDomain } from "./agent.js"
|
||||
import type { AISDKDomain } from "./aisdk.js"
|
||||
import type { CatalogDomain } from "./catalog.js"
|
||||
import type { CommandDomain } from "./command.js"
|
||||
import type { EventDomain } from "./event.js"
|
||||
import type { IntegrationDomain } from "./integration.js"
|
||||
import type { ReferenceDomain } from "./reference.js"
|
||||
import type { SessionDomain } from "./session.js"
|
||||
import type { SkillDomain } from "./skill.js"
|
||||
import type { ToolDomain } from "./tool.js"
|
||||
|
||||
export interface Context {
|
||||
readonly options: PluginOptions
|
||||
readonly agent: AgentDomain
|
||||
readonly aisdk: AISDKDomain
|
||||
readonly catalog: CatalogDomain
|
||||
readonly command: CommandDomain
|
||||
readonly event: EventDomain
|
||||
readonly integration: IntegrationDomain
|
||||
readonly plugin: PluginApi<unknown>
|
||||
readonly reference: ReferenceDomain
|
||||
readonly session: SessionDomain
|
||||
readonly skill: SkillDomain
|
||||
readonly tool: ToolDomain
|
||||
}
|
||||
|
||||
export interface Plugin<R = Scope.Scope> {
|
||||
readonly id: string
|
||||
readonly effect: (context: PluginContext) => Effect.Effect<void, never, R>
|
||||
readonly effect: (context: Context) => Effect.Effect<void, never, R>
|
||||
}
|
||||
|
||||
export function define<R = Scope.Scope>(plugin: Plugin<R>) {
|
||||
return plugin
|
||||
}
|
||||
|
||||
export interface PluginDomain extends PluginApi<unknown> {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ReferenceGitSource, ReferenceLocalSource } from "@opencode-ai/sdk/v2/types"
|
||||
import type { ReferenceApi } from "@opencode-ai/client/effect/api"
|
||||
import type { Effect } from "effect"
|
||||
import type { TransformHook } from "./registration.js"
|
||||
import type { Transform } from "./registration.js"
|
||||
|
||||
export interface ReferenceDraft {
|
||||
add(name: string, source: ReferenceLocalSource | ReferenceGitSource): void
|
||||
@@ -9,7 +9,7 @@ export interface ReferenceDraft {
|
||||
list(): readonly (readonly [string, ReferenceLocalSource | ReferenceGitSource])[]
|
||||
}
|
||||
|
||||
export interface ReferenceHooks extends ReferenceApi<unknown> {
|
||||
readonly transform: TransformHook<ReferenceDraft>
|
||||
export interface ReferenceDomain extends ReferenceApi<unknown> {
|
||||
readonly transform: Transform<ReferenceDraft>
|
||||
readonly reload: () => Effect.Effect<void>
|
||||
}
|
||||
|
||||
@@ -4,10 +4,9 @@ export interface Registration {
|
||||
readonly dispose: Effect.Effect<void>
|
||||
}
|
||||
|
||||
export type Hooks<Spec> = {
|
||||
readonly [Name in keyof Spec]: (
|
||||
callback: (input: Spec[Name]) => Effect.Effect<void> | void,
|
||||
) => Effect.Effect<Registration, never, Scope.Scope>
|
||||
}
|
||||
export type Hooks<Spec> = <Name extends keyof Spec>(
|
||||
name: Name,
|
||||
callback: (input: Spec[Name]) => Effect.Effect<void>,
|
||||
) => Effect.Effect<Registration, never, Scope.Scope>
|
||||
|
||||
export type TransformHook<Input> = (callback: (input: Input) => void) => Effect.Effect<Registration, never, Scope.Scope>
|
||||
export type Transform<Input> = (callback: (input: Input) => void) => Effect.Effect<Registration, never, Scope.Scope>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import type { SessionApi } from "@opencode-ai/client/effect/api"
|
||||
|
||||
export interface SessionHooks
|
||||
extends Pick<SessionApi<unknown>, "create" | "get" | "prompt" | "command" | "interrupt"> {}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { SessionApi } from "@opencode-ai/client/effect/api"
|
||||
import type { Message, SystemPart } from "@opencode-ai/llm"
|
||||
import type { Agent } from "@opencode-ai/schema/agent"
|
||||
import type { Model } from "@opencode-ai/schema/model"
|
||||
import type { Session } from "@opencode-ai/schema/session"
|
||||
import type { JsonSchema } from "effect"
|
||||
import type { Hooks } from "./registration.js"
|
||||
|
||||
export interface SessionRequestBeforeEvent {
|
||||
readonly sessionID: Session.ID
|
||||
readonly agent: Agent.ID
|
||||
readonly model: Model.Ref
|
||||
system: Array<SystemPart>
|
||||
messages: Array<Message>
|
||||
tools: Record<string, { description: string; input: JsonSchema.JsonSchema }>
|
||||
}
|
||||
|
||||
export interface SessionHooks {
|
||||
readonly request: SessionRequestBeforeEvent
|
||||
}
|
||||
|
||||
export interface SessionDomain
|
||||
extends Pick<SessionApi<unknown>, "create" | "get" | "prompt" | "command" | "interrupt"> {
|
||||
readonly hook: Hooks<SessionHooks>
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { SkillV2Source } from "@opencode-ai/sdk/v2/types"
|
||||
import type { SkillApi } from "@opencode-ai/client/effect/api"
|
||||
import type { Effect } from "effect"
|
||||
import type { TransformHook } from "./registration.js"
|
||||
import type { Transform } from "./registration.js"
|
||||
|
||||
export interface SkillDraft {
|
||||
source(source: SkillV2Source): void
|
||||
list(): readonly SkillV2Source[]
|
||||
}
|
||||
|
||||
export interface SkillHooks extends SkillApi<unknown> {
|
||||
readonly transform: TransformHook<SkillDraft>
|
||||
export interface SkillDomain extends SkillApi<unknown> {
|
||||
readonly transform: Transform<SkillDraft>
|
||||
readonly reload: () => Effect.Effect<void>
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Agent } from "@opencode-ai/schema/agent"
|
||||
import { Session } from "@opencode-ai/schema/session"
|
||||
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||
import { Effect, JsonSchema, Schema, type Scope } from "effect"
|
||||
import type { Hooks } from "./registration.js"
|
||||
import type { Hooks, Transform } from "./registration.js"
|
||||
|
||||
export interface Context {
|
||||
readonly sessionID: Session.ID
|
||||
@@ -253,7 +253,12 @@ export interface ToolDraft {
|
||||
add(name: string, tool: AnyTool, options?: RegisterOptions): void
|
||||
}
|
||||
|
||||
export interface ToolDomain {
|
||||
readonly transform: (callback: (draft: ToolDraft) => void) => Effect.Effect<void, RegistrationError, Scope.Scope>
|
||||
readonly execute: Hooks<{ before: ToolExecuteBeforeEvent; after: ToolExecuteAfterEvent }>
|
||||
export interface ToolHooks {
|
||||
readonly "execute.before": ToolExecuteBeforeEvent
|
||||
readonly "execute.after": ToolExecuteAfterEvent
|
||||
}
|
||||
|
||||
export interface ToolDomain {
|
||||
readonly transform: Transform<ToolDraft>
|
||||
readonly hook: Hooks<ToolHooks>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# OpenCode V2 Promise Plugin API
|
||||
|
||||
The Promise plugin API is the async/await equivalent of `@opencode-ai/plugin/v2/effect`. It grants plugins the same two in-process capabilities:
|
||||
The Promise plugin API at `@opencode-ai/plugin/v2` is the async/await equivalent of `@opencode-ai/plugin/v2/effect`. It grants plugins the same two in-process capabilities:
|
||||
|
||||
- `hook` installs behavior at an OpenCode extension point.
|
||||
- `reload` reruns every transform hook for a stateful domain.
|
||||
@@ -10,9 +10,9 @@ The only difference from the Effect API is the async boundary: hook callbacks, h
|
||||
## Defining A Plugin
|
||||
|
||||
```ts
|
||||
import { define } from "@opencode-ai/plugin/v2/promise"
|
||||
import { Plugin } from "@opencode-ai/plugin/v2"
|
||||
|
||||
export const Plugin = define({
|
||||
export default Plugin.define({
|
||||
id: "example",
|
||||
setup: async (ctx) => {
|
||||
await ctx.catalog.transform((catalog) => {
|
||||
@@ -24,7 +24,7 @@ export const Plugin = define({
|
||||
})
|
||||
```
|
||||
|
||||
Plugin setup registers hooks imperatively. It does not return a hook object.
|
||||
Plugin setup registers hooks imperatively through each domain's `hook` method.
|
||||
|
||||
Configuration supplied for the plugin is available as `ctx.options`.
|
||||
|
||||
@@ -64,18 +64,43 @@ ctx.skill.transform
|
||||
Runtime hooks intercept live operations:
|
||||
|
||||
```ts
|
||||
await ctx.aisdk.sdk(async (event) => {
|
||||
await ctx.aisdk.hook("sdk", async (event) => {
|
||||
if (event.package !== "@ai-sdk/xai") return
|
||||
const mod = await import("@ai-sdk/xai")
|
||||
event.sdk = mod.createXai(event.options)
|
||||
})
|
||||
|
||||
await ctx.aisdk.language((event) => {
|
||||
await ctx.aisdk.hook("language", (event) => {
|
||||
if (event.model.providerID !== "xai") return
|
||||
event.language = event.sdk.responses(event.model.api.id)
|
||||
})
|
||||
```
|
||||
|
||||
Session request context is mutable immediately before provider dispatch:
|
||||
|
||||
```ts
|
||||
await ctx.session.hook("request", (event) => {
|
||||
event.tools.read.description = "Read a file using narrow line ranges."
|
||||
delete event.tools.write
|
||||
})
|
||||
```
|
||||
|
||||
Promise tools use the same schemas and registration model as Effect tools, with async executors:
|
||||
|
||||
```ts
|
||||
import { Schema } from "effect"
|
||||
import { Tool } from "@opencode-ai/plugin/v2/tool"
|
||||
|
||||
const echo = Tool.make({
|
||||
description: "Echo text",
|
||||
input: Schema.Struct({ text: Schema.String }),
|
||||
output: Schema.Struct({ text: Schema.String }),
|
||||
execute: async ({ text }) => ({ text }),
|
||||
})
|
||||
|
||||
await ctx.tool.transform((tools) => tools.add("echo", echo))
|
||||
```
|
||||
|
||||
## Reloading A Domain
|
||||
|
||||
When data captured by a transform changes, reload the affected domain:
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { AgentApi } from "@opencode-ai/client/promise/api"
|
||||
import type { AgentDraft } from "../effect/agent.js"
|
||||
import type { TransformHook } from "./registration.js"
|
||||
import type { Transform } from "./registration.js"
|
||||
|
||||
export type { AgentDraft }
|
||||
|
||||
export interface AgentHooks extends AgentApi {
|
||||
readonly transform: TransformHook<AgentDraft>
|
||||
export interface AgentDomain extends AgentApi {
|
||||
readonly transform: Transform<AgentDraft>
|
||||
readonly reload: () => Promise<void>
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import type { Model } from "@opencode-ai/schema/model"
|
||||
import type { Hooks } from "./registration.js"
|
||||
|
||||
export type AISDKHooks = Hooks<{
|
||||
export interface AISDKHooks {
|
||||
sdk: {
|
||||
readonly model: Model.Info
|
||||
readonly package: string
|
||||
@@ -15,4 +15,8 @@ export type AISDKHooks = Hooks<{
|
||||
readonly options: Record<string, any>
|
||||
language?: LanguageModelV3
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
export interface AISDKDomain {
|
||||
readonly hook: Hooks<AISDKHooks>
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user