feat(core): port v2 command invocation (#34849)

This commit is contained in:
Aiden Cline
2026-07-02 11:22:04 -05:00
committed by GitHub
parent 016d296f7c
commit c176baee82
26 changed files with 1448 additions and 182 deletions
+114 -81
View File
@@ -151,49 +151,81 @@ const Endpoint4_8 = (raw: RawClient["server.session"]) => (input: Endpoint4_8Inp
Effect.map((value) => value.data), Effect.map((value) => value.data),
) )
type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.skill"]>[0] type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.command"]>[0]
type Endpoint4_9Input = { type Endpoint4_9Input = {
readonly sessionID: Endpoint4_9Request["params"]["sessionID"] readonly sessionID: Endpoint4_9Request["params"]["sessionID"]
readonly id?: Endpoint4_9Request["payload"]["id"] readonly id?: Endpoint4_9Request["payload"]["id"]
readonly skill: Endpoint4_9Request["payload"]["skill"] readonly command: Endpoint4_9Request["payload"]["command"]
readonly arguments?: Endpoint4_9Request["payload"]["arguments"]
readonly agent?: Endpoint4_9Request["payload"]["agent"]
readonly model?: Endpoint4_9Request["payload"]["model"]
readonly files?: Endpoint4_9Request["payload"]["files"]
readonly agents?: Endpoint4_9Request["payload"]["agents"]
readonly delivery?: Endpoint4_9Request["payload"]["delivery"]
readonly resume?: Endpoint4_9Request["payload"]["resume"] readonly resume?: Endpoint4_9Request["payload"]["resume"]
} }
const Endpoint4_9 = (raw: RawClient["server.session"]) => (input: Endpoint4_9Input) => const Endpoint4_9 = (raw: RawClient["server.session"]) => (input: Endpoint4_9Input) =>
raw["session.command"]({
params: { sessionID: input["sessionID"] },
payload: {
id: input["id"],
command: input["command"],
arguments: input["arguments"],
agent: input["agent"],
model: input["model"],
files: input["files"],
agents: input["agents"],
delivery: input["delivery"],
resume: input["resume"],
},
}).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
)
type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
type Endpoint4_10Input = {
readonly sessionID: Endpoint4_10Request["params"]["sessionID"]
readonly id?: Endpoint4_10Request["payload"]["id"]
readonly skill: Endpoint4_10Request["payload"]["skill"]
readonly resume?: Endpoint4_10Request["payload"]["resume"]
}
const Endpoint4_10 = (raw: RawClient["server.session"]) => (input: Endpoint4_10Input) =>
raw["session.skill"]({ raw["session.skill"]({
params: { sessionID: input["sessionID"] }, params: { sessionID: input["sessionID"] },
payload: { id: input["id"], skill: input["skill"], resume: input["resume"] }, payload: { id: input["id"], skill: input["skill"], resume: input["resume"] },
}).pipe(Effect.mapError(mapClientError)) }).pipe(Effect.mapError(mapClientError))
type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0] type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
type Endpoint4_10Input = { type Endpoint4_11Input = {
readonly sessionID: Endpoint4_10Request["params"]["sessionID"] readonly sessionID: Endpoint4_11Request["params"]["sessionID"]
readonly text: Endpoint4_10Request["payload"]["text"] readonly text: Endpoint4_11Request["payload"]["text"]
readonly description?: Endpoint4_10Request["payload"]["description"] readonly description?: Endpoint4_11Request["payload"]["description"]
readonly metadata?: Endpoint4_10Request["payload"]["metadata"] readonly metadata?: Endpoint4_11Request["payload"]["metadata"]
} }
const Endpoint4_10 = (raw: RawClient["server.session"]) => (input: Endpoint4_10Input) => const Endpoint4_11 = (raw: RawClient["server.session"]) => (input: Endpoint4_11Input) =>
raw["session.synthetic"]({ raw["session.synthetic"]({
params: { sessionID: input["sessionID"] }, params: { sessionID: input["sessionID"] },
payload: { text: input["text"], description: input["description"], metadata: input["metadata"] }, payload: { text: input["text"], description: input["description"], metadata: input["metadata"] },
}).pipe(Effect.mapError(mapClientError)) }).pipe(Effect.mapError(mapClientError))
type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.compact"]>[0] type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
type Endpoint4_11Input = { readonly sessionID: Endpoint4_11Request["params"]["sessionID"] }
const Endpoint4_11 = (raw: RawClient["server.session"]) => (input: Endpoint4_11Input) =>
raw["session.compact"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
type Endpoint4_12Input = { readonly sessionID: Endpoint4_12Request["params"]["sessionID"] } type Endpoint4_12Input = { readonly sessionID: Endpoint4_12Request["params"]["sessionID"] }
const Endpoint4_12 = (raw: RawClient["server.session"]) => (input: Endpoint4_12Input) => const Endpoint4_12 = (raw: RawClient["server.session"]) => (input: Endpoint4_12Input) =>
raw["session.compact"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
type Endpoint4_13Input = { readonly sessionID: Endpoint4_13Request["params"]["sessionID"] }
const Endpoint4_13 = (raw: RawClient["server.session"]) => (input: Endpoint4_13Input) =>
raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0] type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
type Endpoint4_13Input = { type Endpoint4_14Input = {
readonly sessionID: Endpoint4_13Request["params"]["sessionID"] readonly sessionID: Endpoint4_14Request["params"]["sessionID"]
readonly messageID: Endpoint4_13Request["payload"]["messageID"] readonly messageID: Endpoint4_14Request["payload"]["messageID"]
readonly files?: Endpoint4_13Request["payload"]["files"] readonly files?: Endpoint4_14Request["payload"]["files"]
} }
const Endpoint4_13 = (raw: RawClient["server.session"]) => (input: Endpoint4_13Input) => const Endpoint4_14 = (raw: RawClient["server.session"]) => (input: Endpoint4_14Input) =>
raw["session.revert.stage"]({ raw["session.revert.stage"]({
params: { sessionID: input["sessionID"] }, params: { sessionID: input["sessionID"] },
payload: { messageID: input["messageID"], files: input["files"] }, payload: { messageID: input["messageID"], files: input["files"] },
@@ -202,72 +234,72 @@ const Endpoint4_13 = (raw: RawClient["server.session"]) => (input: Endpoint4_13I
Effect.map((value) => value.data), Effect.map((value) => value.data),
) )
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0] type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
type Endpoint4_14Input = { readonly sessionID: Endpoint4_14Request["params"]["sessionID"] }
const Endpoint4_14 = (raw: RawClient["server.session"]) => (input: Endpoint4_14Input) =>
raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
type Endpoint4_15Input = { readonly sessionID: Endpoint4_15Request["params"]["sessionID"] } type Endpoint4_15Input = { readonly sessionID: Endpoint4_15Request["params"]["sessionID"] }
const Endpoint4_15 = (raw: RawClient["server.session"]) => (input: Endpoint4_15Input) => const Endpoint4_15 = (raw: RawClient["server.session"]) => (input: Endpoint4_15Input) =>
raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.context"]>[0] type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
type Endpoint4_16Input = { readonly sessionID: Endpoint4_16Request["params"]["sessionID"] } type Endpoint4_16Input = { readonly sessionID: Endpoint4_16Request["params"]["sessionID"] }
const Endpoint4_16 = (raw: RawClient["server.session"]) => (input: Endpoint4_16Input) => const Endpoint4_16 = (raw: RawClient["server.session"]) => (input: Endpoint4_16Input) =>
raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.context"]>[0]
type Endpoint4_17Input = { readonly sessionID: Endpoint4_17Request["params"]["sessionID"] }
const Endpoint4_17 = (raw: RawClient["server.session"]) => (input: Endpoint4_17Input) =>
raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe( raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
Effect.map((value) => value.data), Effect.map((value) => value.data),
) )
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.context.entry.list"]>[0] type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.context.entry.list"]>[0]
type Endpoint4_17Input = { readonly sessionID: Endpoint4_17Request["params"]["sessionID"] } type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
const Endpoint4_17 = (raw: RawClient["server.session"]) => (input: Endpoint4_17Input) => const Endpoint4_18 = (raw: RawClient["server.session"]) => (input: Endpoint4_18Input) =>
raw["session.context.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe( raw["session.context.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
Effect.map((value) => value.data), Effect.map((value) => value.data),
) )
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.context.entry.put"]>[0] type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.context.entry.put"]>[0]
type Endpoint4_18Input = { type Endpoint4_19Input = {
readonly sessionID: Endpoint4_18Request["params"]["sessionID"] readonly sessionID: Endpoint4_19Request["params"]["sessionID"]
readonly key: Endpoint4_18Request["params"]["key"] readonly key: Endpoint4_19Request["params"]["key"]
readonly value: Endpoint4_18Request["payload"]["value"] readonly value: Endpoint4_19Request["payload"]["value"]
} }
const Endpoint4_18 = (raw: RawClient["server.session"]) => (input: Endpoint4_18Input) => const Endpoint4_19 = (raw: RawClient["server.session"]) => (input: Endpoint4_19Input) =>
raw["session.context.entry.put"]({ raw["session.context.entry.put"]({
params: { sessionID: input["sessionID"], key: input["key"] }, params: { sessionID: input["sessionID"], key: input["key"] },
payload: { value: input["value"] }, payload: { value: input["value"] },
}).pipe(Effect.mapError(mapClientError)) }).pipe(Effect.mapError(mapClientError))
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.context.entry.remove"]>[0] type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.context.entry.remove"]>[0]
type Endpoint4_19Input = { type Endpoint4_20Input = {
readonly sessionID: Endpoint4_19Request["params"]["sessionID"] readonly sessionID: Endpoint4_20Request["params"]["sessionID"]
readonly key: Endpoint4_19Request["params"]["key"] readonly key: Endpoint4_20Request["params"]["key"]
} }
const Endpoint4_19 = (raw: RawClient["server.session"]) => (input: Endpoint4_19Input) => const Endpoint4_20 = (raw: RawClient["server.session"]) => (input: Endpoint4_20Input) =>
raw["session.context.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe( raw["session.context.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
) )
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.history"]>[0] type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.history"]>[0]
type Endpoint4_20Input = { type Endpoint4_21Input = {
readonly sessionID: Endpoint4_20Request["params"]["sessionID"] readonly sessionID: Endpoint4_21Request["params"]["sessionID"]
readonly limit?: Endpoint4_20Request["query"]["limit"] readonly limit?: Endpoint4_21Request["query"]["limit"]
readonly after?: Endpoint4_20Request["query"]["after"] readonly after?: Endpoint4_21Request["query"]["after"]
} }
const Endpoint4_20 = (raw: RawClient["server.session"]) => (input: Endpoint4_20Input) => const Endpoint4_21 = (raw: RawClient["server.session"]) => (input: Endpoint4_21Input) =>
raw["session.history"]({ raw["session.history"]({
params: { sessionID: input["sessionID"] }, params: { sessionID: input["sessionID"] },
query: { limit: input["limit"], after: input["after"] }, query: { limit: input["limit"], after: input["after"] },
}).pipe(Effect.mapError(mapClientError)) }).pipe(Effect.mapError(mapClientError))
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.events"]>[0] type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.events"]>[0]
type Endpoint4_21Input = { type Endpoint4_22Input = {
readonly sessionID: Endpoint4_21Request["params"]["sessionID"] readonly sessionID: Endpoint4_22Request["params"]["sessionID"]
readonly after?: Endpoint4_21Request["query"]["after"] readonly after?: Endpoint4_22Request["query"]["after"]
} }
const Endpoint4_21 = (raw: RawClient["server.session"]) => (input: Endpoint4_21Input) => const Endpoint4_22 = (raw: RawClient["server.session"]) => (input: Endpoint4_22Input) =>
Stream.unwrap( Stream.unwrap(
raw["session.events"]({ params: { sessionID: input["sessionID"] }, query: { after: input["after"] } }).pipe( raw["session.events"]({ params: { sessionID: input["sessionID"] }, query: { after: input["after"] } }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
@@ -275,22 +307,22 @@ const Endpoint4_21 = (raw: RawClient["server.session"]) => (input: Endpoint4_21I
), ),
) )
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0] type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
type Endpoint4_22Input = { readonly sessionID: Endpoint4_22Request["params"]["sessionID"] }
const Endpoint4_22 = (raw: RawClient["server.session"]) => (input: Endpoint4_22Input) =>
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.background"]>[0]
type Endpoint4_23Input = { readonly sessionID: Endpoint4_23Request["params"]["sessionID"] } type Endpoint4_23Input = { readonly sessionID: Endpoint4_23Request["params"]["sessionID"] }
const Endpoint4_23 = (raw: RawClient["server.session"]) => (input: Endpoint4_23Input) => const Endpoint4_23 = (raw: RawClient["server.session"]) => (input: Endpoint4_23Input) =>
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.background"]>[0]
type Endpoint4_24Input = { readonly sessionID: Endpoint4_24Request["params"]["sessionID"] }
const Endpoint4_24 = (raw: RawClient["server.session"]) => (input: Endpoint4_24Input) =>
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.message"]>[0] type Endpoint4_25Request = Parameters<RawClient["server.session"]["session.message"]>[0]
type Endpoint4_24Input = { type Endpoint4_25Input = {
readonly sessionID: Endpoint4_24Request["params"]["sessionID"] readonly sessionID: Endpoint4_25Request["params"]["sessionID"]
readonly messageID: Endpoint4_24Request["params"]["messageID"] readonly messageID: Endpoint4_25Request["params"]["messageID"]
} }
const Endpoint4_24 = (raw: RawClient["server.session"]) => (input: Endpoint4_24Input) => const Endpoint4_25 = (raw: RawClient["server.session"]) => (input: Endpoint4_25Input) =>
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe( raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
Effect.map((value) => value.data), Effect.map((value) => value.data),
@@ -306,22 +338,23 @@ const adaptGroup4 = (raw: RawClient["server.session"]) => ({
switchModel: Endpoint4_6(raw), switchModel: Endpoint4_6(raw),
rename: Endpoint4_7(raw), rename: Endpoint4_7(raw),
prompt: Endpoint4_8(raw), prompt: Endpoint4_8(raw),
skill: Endpoint4_9(raw), command: Endpoint4_9(raw),
synthetic: Endpoint4_10(raw), skill: Endpoint4_10(raw),
compact: Endpoint4_11(raw), synthetic: Endpoint4_11(raw),
wait: Endpoint4_12(raw), compact: Endpoint4_12(raw),
revertStage: Endpoint4_13(raw), wait: Endpoint4_13(raw),
revertClear: Endpoint4_14(raw), revertStage: Endpoint4_14(raw),
revertCommit: Endpoint4_15(raw), revertClear: Endpoint4_15(raw),
context: Endpoint4_16(raw), revertCommit: Endpoint4_16(raw),
listContextEntries: Endpoint4_17(raw), context: Endpoint4_17(raw),
putContextEntry: Endpoint4_18(raw), listContextEntries: Endpoint4_18(raw),
removeContextEntry: Endpoint4_19(raw), putContextEntry: Endpoint4_19(raw),
history: Endpoint4_20(raw), removeContextEntry: Endpoint4_20(raw),
events: Endpoint4_21(raw), history: Endpoint4_21(raw),
interrupt: Endpoint4_22(raw), events: Endpoint4_22(raw),
background: Endpoint4_23(raw), interrupt: Endpoint4_23(raw),
message: Endpoint4_24(raw), background: Endpoint4_24(raw),
message: Endpoint4_25(raw),
}) })
type Endpoint5_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0] type Endpoint5_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0]
+24
View File
@@ -23,6 +23,8 @@ import type {
SessionRenameOutput, SessionRenameOutput,
SessionPromptInput, SessionPromptInput,
SessionPromptOutput, SessionPromptOutput,
SessionCommandInput,
SessionCommandOutput,
SessionSkillInput, SessionSkillInput,
SessionSkillOutput, SessionSkillOutput,
SessionSyntheticInput, SessionSyntheticInput,
@@ -459,6 +461,28 @@ export function make(options: ClientOptions) {
}, },
requestOptions, requestOptions,
).then((value) => value.data), ).then((value) => value.data),
command: (input: SessionCommandInput, requestOptions?: RequestOptions) =>
request<{ readonly data: SessionCommandOutput }>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/command`,
body: {
id: input["id"],
command: input["command"],
arguments: input["arguments"],
agent: input["agent"],
model: input["model"],
files: input["files"],
agents: input["agents"],
delivery: input["delivery"],
resume: input["resume"],
},
successStatus: 200,
declaredStatuses: [409, 404, 500, 400, 401],
empty: false,
},
requestOptions,
).then((value) => value.data),
skill: (input: SessionSkillInput, requestOptions?: RequestOptions) => skill: (input: SessionSkillInput, requestOptions?: RequestOptions) =>
request<SessionSkillOutput>( request<SessionSkillOutput>(
{ {
+224
View File
@@ -50,6 +50,22 @@ export type ConflictError = {
export const isConflictError = (value: unknown): value is ConflictError => export const isConflictError = (value: unknown): value is ConflictError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ConflictError" typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ConflictError"
export type CommandNotFoundError = {
readonly _tag: "CommandNotFoundError"
readonly command: string
readonly message: string
}
export const isCommandNotFoundError = (value: unknown): value is CommandNotFoundError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "CommandNotFoundError"
export type CommandEvaluationError = {
readonly _tag: "CommandEvaluationError"
readonly command: string
readonly message: string
}
export const isCommandEvaluationError = (value: unknown): value is CommandEvaluationError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "CommandEvaluationError"
export type SkillNotFoundError = { export type SkillNotFoundError = {
readonly _tag: "SkillNotFoundError" readonly _tag: "SkillNotFoundError"
readonly skill: string readonly skill: string
@@ -564,6 +580,206 @@ export type SessionPromptOutput = {
} }
}["data"] }["data"]
export type SessionCommandInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"]
readonly id?: {
readonly id?: string | null
readonly command: string
readonly arguments?: string | null
readonly agent?: string | null
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
readonly files?: ReadonlyArray<{
readonly uri: string
readonly name?: string
readonly description?: string
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly agents?: ReadonlyArray<{
readonly name: string
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly resume?: boolean | null
}["id"]
readonly command: {
readonly id?: string | null
readonly command: string
readonly arguments?: string | null
readonly agent?: string | null
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
readonly files?: ReadonlyArray<{
readonly uri: string
readonly name?: string
readonly description?: string
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly agents?: ReadonlyArray<{
readonly name: string
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly resume?: boolean | null
}["command"]
readonly arguments?: {
readonly id?: string | null
readonly command: string
readonly arguments?: string | null
readonly agent?: string | null
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
readonly files?: ReadonlyArray<{
readonly uri: string
readonly name?: string
readonly description?: string
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly agents?: ReadonlyArray<{
readonly name: string
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly resume?: boolean | null
}["arguments"]
readonly agent?: {
readonly id?: string | null
readonly command: string
readonly arguments?: string | null
readonly agent?: string | null
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
readonly files?: ReadonlyArray<{
readonly uri: string
readonly name?: string
readonly description?: string
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly agents?: ReadonlyArray<{
readonly name: string
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly resume?: boolean | null
}["agent"]
readonly model?: {
readonly id?: string | null
readonly command: string
readonly arguments?: string | null
readonly agent?: string | null
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
readonly files?: ReadonlyArray<{
readonly uri: string
readonly name?: string
readonly description?: string
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly agents?: ReadonlyArray<{
readonly name: string
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly resume?: boolean | null
}["model"]
readonly files?: {
readonly id?: string | null
readonly command: string
readonly arguments?: string | null
readonly agent?: string | null
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
readonly files?: ReadonlyArray<{
readonly uri: string
readonly name?: string
readonly description?: string
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly agents?: ReadonlyArray<{
readonly name: string
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly resume?: boolean | null
}["files"]
readonly agents?: {
readonly id?: string | null
readonly command: string
readonly arguments?: string | null
readonly agent?: string | null
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
readonly files?: ReadonlyArray<{
readonly uri: string
readonly name?: string
readonly description?: string
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly agents?: ReadonlyArray<{
readonly name: string
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly resume?: boolean | null
}["agents"]
readonly delivery?: {
readonly id?: string | null
readonly command: string
readonly arguments?: string | null
readonly agent?: string | null
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
readonly files?: ReadonlyArray<{
readonly uri: string
readonly name?: string
readonly description?: string
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly agents?: ReadonlyArray<{
readonly name: string
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly resume?: boolean | null
}["delivery"]
readonly resume?: {
readonly id?: string | null
readonly command: string
readonly arguments?: string | null
readonly agent?: string | null
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
readonly files?: ReadonlyArray<{
readonly uri: string
readonly name?: string
readonly description?: string
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly agents?: ReadonlyArray<{
readonly name: string
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly resume?: boolean | null
}["resume"]
}
export type SessionCommandOutput = {
readonly data: {
readonly admittedSeq: number
readonly id: string
readonly sessionID: string
readonly prompt: {
readonly text: string
readonly files?: ReadonlyArray<{
readonly uri: string
readonly mime: string
readonly name?: string
readonly description?: string
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly agents?: ReadonlyArray<{
readonly name: string
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
}>
}
readonly delivery: "steer" | "queue"
readonly timeCreated: number
readonly promotedSeq?: number
}
}["data"]
export type SessionSkillInput = { export type SessionSkillInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"] readonly sessionID: { readonly sessionID: string }["sessionID"]
readonly id?: { readonly id?: {
@@ -4114,6 +4330,14 @@ export type EventSubscribeOutput =
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly projectID: string } readonly data: { readonly projectID: string }
} }
| {
readonly id: string
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "command.updated"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {}
}
| { | {
readonly id: string readonly id: string
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
+189 -5
View File
@@ -1,17 +1,39 @@
export * as CommandV2 from "./command" export * as CommandV2 from "./command"
import { makeLocationNode } from "./effect/app-node" import { makeLocationNode } from "./effect/app-node"
import { Context, Effect, Layer, Types } from "effect" import { Context, Effect, Layer, Schema, Types } from "effect"
import { Command } from "@opencode-ai/schema/command" import { Command } from "@opencode-ai/schema/command"
import { State } from "./state" import { State } from "./state"
import { MCP } from "./mcp/index"
import { EventV2 } from "./event"
import { AppProcess } from "./process"
import { ChildProcess } from "effect/unstable/process"
import { Config } from "./config"
import { Location } from "./location"
import { ShellSelect } from "./shell/select"
export const Info = Command.Info export const Info = Command.Info
export type Info = Command.Info export type Info = Command.Info
export const Event = Command.Event
export type Evaluation = {
readonly text: string
}
export type Data = { export type Data = {
commands: Map<string, Types.DeepMutable<Info>> commands: Map<string, Types.DeepMutable<Info>>
} }
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Command.NotFoundError", {
command: Schema.String,
message: Schema.String,
}) {}
export class EvaluationError extends Schema.TaggedErrorClass<EvaluationError>()("Command.EvaluationError", {
command: Schema.String,
message: Schema.String,
}) {}
export type Draft = { export type Draft = {
list: () => readonly Info[] list: () => readonly Info[]
get: (name: string) => Info | undefined get: (name: string) => Info | undefined
@@ -22,13 +44,22 @@ export type Draft = {
export interface Interface extends State.Transformable<Draft> { export interface Interface extends State.Transformable<Draft> {
readonly get: (name: string) => Effect.Effect<Info | undefined> readonly get: (name: string) => Effect.Effect<Info | undefined>
readonly list: () => Effect.Effect<Info[]> readonly list: () => Effect.Effect<Info[]>
readonly evaluate: (input: {
readonly name: string
readonly arguments?: string
}) => Effect.Effect<Evaluation, NotFoundError | EvaluationError>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Command") {} export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Command") {}
const layer = Layer.effect( const layer = Layer.effect(
Service, Service,
Effect.sync(() => { Effect.gen(function* () {
const mcp = yield* MCP.Service
const events = yield* EventV2.Service
const processes = yield* AppProcess.Service
const config = yield* Config.Service
const location = yield* Location.Service
const state = State.create<Data, Draft>({ const state = State.create<Data, Draft>({
initial: () => ({ commands: new Map() }), initial: () => ({ commands: new Map() }),
draft: (draft) => ({ draft: (draft) => ({
@@ -44,19 +75,172 @@ const layer = Layer.effect(
draft.commands.delete(name) draft.commands.delete(name)
}, },
}), }),
finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid),
})
const staticCommand = (name: string) => state.get().commands.get(name) as Info | undefined
const mcpCommands = Effect.fnUntraced(function* () {
return (yield* mcp.prompts()).map((prompt) =>
Info.make({
name: mcpCommandName(prompt.server, prompt.name),
template: "",
description: prompt.description,
}),
)
}) })
return Service.of({ return Service.of({
reload: state.reload, reload: state.reload,
transform: state.transform, transform: state.transform,
get: Effect.fn("CommandV2.get")(function* (name) { get: Effect.fn("CommandV2.get")(function* (name) {
return state.get().commands.get(name) const command = staticCommand(name)
if (command) return command
return (yield* mcpCommands()).find((command) => command.name === name)
}), }),
list: Effect.fn("CommandV2.list")(function* () { list: Effect.fn("CommandV2.list")(function* () {
return Array.from(state.get().commands.values()) const commands = Array.from(state.get().commands.values()) as Info[]
const names = new Set(commands.map((command) => command.name))
return [
...commands,
...(yield* mcpCommands()).filter((command) => !names.has(command.name)),
]
}),
evaluate: Effect.fn("CommandV2.evaluate")(function* (input) {
const command = staticCommand(input.name)
if (command) return yield* evaluateTemplate(input.name, command.template, input.arguments ?? "", {
config,
location,
processes,
})
const prompt = (yield* mcp.prompts()).find((prompt) => mcpCommandName(prompt.server, prompt.name) === input.name)
if (!prompt) return yield* new NotFoundError({ command: input.name, message: `Command not found: ${input.name}` })
const result = yield* mcp
.prompt({
server: prompt.server,
name: prompt.name,
args: Object.fromEntries(
(prompt.arguments ?? []).map((argument, index) => [
argument.name,
parseArguments(input.arguments ?? "")[index] ?? "",
]),
),
})
.pipe(
Effect.catchTag(
"MCP.NotFoundError",
() =>
Effect.fail(
new EvaluationError({
command: input.name,
message: `MCP server could not be found while evaluating prompt: ${prompt.server}`,
}),
),
),
)
if (!result)
return yield* new EvaluationError({
command: input.name,
message: `MCP prompt could not be evaluated: ${prompt.server}:${prompt.name}`,
})
return { text: result.messages.map((message) => promptMessageText(message.content)).join("\n").trim() }
}), }),
}) })
}), }),
) )
export const node = makeLocationNode({ service: Service, layer, deps: [] }) function evaluateTemplate(
command: string,
template: string,
input: string,
services: {
readonly config: Config.Interface
readonly location: Location.Info
readonly processes: AppProcess.Interface
},
) {
return Effect.gen(function* () {
const expanded = evaluateArguments(template, input)
return { text: yield* evaluateShell(command, expanded, services) }
})
}
function evaluateArguments(template: string, input: string) {
const args = parseArguments(input)
const placeholders = template.match(placeholderRegex) ?? []
const last = Math.max(0, ...placeholders.map((item) => Number(item.slice(1))))
const expanded = template.replaceAll(placeholderRegex, (_, index) => {
const position = Number(index)
const argIndex = position - 1
if (argIndex >= args.length) return ""
if (position === last) return args.slice(argIndex).join(" ")
return args[argIndex]
})
const withArguments = expanded.replaceAll("$ARGUMENTS", input)
if (placeholders.length === 0 && !template.includes("$ARGUMENTS") && input.trim()) return `${withArguments}\n\n${input}`.trim()
return withArguments.trim()
}
const evaluateShell = Effect.fnUntraced(function* (
command: string,
text: string,
services: {
readonly config: Config.Interface
readonly location: Location.Info
readonly processes: AppProcess.Interface
},
) {
const matches = Array.from(text.matchAll(shellRegex))
if (matches.length === 0) return text
const shell = ShellSelect.preferred(Config.latest(yield* services.config.entries(), "shell"))
const outputs = yield* Effect.forEach(
matches,
(match) => {
const source = match[1] ?? ""
return services.processes
.run(ChildProcess.make(shell, ShellSelect.args(shell, source), { cwd: services.location.directory, stdin: "ignore" }), {
combineOutput: true,
})
.pipe(
Effect.map((result) => (result.output ?? Buffer.concat([result.stdout, result.stderr])).toString("utf8")),
Effect.mapError(
(error) =>
new EvaluationError({ command, message: `Shell interpolation failed for ${JSON.stringify(source)}: ${error.message}` }),
),
)
},
{ concurrency: 2 },
)
const iterator = outputs[Symbol.iterator]()
return text.replace(shellRegex, () => iterator.next().value ?? "")
})
function parseArguments(input: string) {
return (input.match(argsRegex) ?? []).map((arg) => arg.replace(quoteTrimRegex, ""))
}
function promptMessageText(content: unknown) {
if (typeof content === "string") return content
if (!content || typeof content !== "object") return ""
if (!("type" in content) || content.type !== "text") return ""
if (!("text" in content) || typeof content.text !== "string") return ""
return content.text
}
function mcpCommandName(server: string, prompt: string) {
return `${sanitize(server)}:${sanitize(prompt)}`
}
function sanitize(value: string) {
return value.replace(/[^a-zA-Z0-9_-]/g, "_")
}
const argsRegex = /(?:\[Image\s+\d+\]|"[^"]*"|'[^']*'|[^\s"']+)/gi
const placeholderRegex = /\$(\d+)/g
const quoteTrimRegex = /^["']|["']$/g
const shellRegex = /!`([^`]+)`/g
export const node = makeLocationNode({
service: Service,
layer,
deps: [MCP.node, EventV2.node, AppProcess.node, Config.node, Location.node],
})
+81
View File
@@ -9,8 +9,12 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
import { UnauthorizedError, type OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js" import { UnauthorizedError, type OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"
import { import {
CallToolResultSchema, CallToolResultSchema,
GetPromptResultSchema,
ListPromptsResultSchema,
ListRootsRequestSchema, ListRootsRequestSchema,
ListToolsResultSchema, ListToolsResultSchema,
PromptListChangedNotificationSchema,
PromptSchema,
type LoggingMessageNotification, type LoggingMessageNotification,
LoggingMessageNotificationSchema, LoggingMessageNotificationSchema,
ToolListChangedNotificationSchema, ToolListChangedNotificationSchema,
@@ -30,6 +34,9 @@ type Transport = StdioClientTransport | StreamableHTTPClientTransport
const TolerantListToolsResult = ListToolsResultSchema.extend({ const TolerantListToolsResult = ListToolsResultSchema.extend({
tools: ToolSchema.omit({ outputSchema: true }).array(), tools: ToolSchema.omit({ outputSchema: true }).array(),
}) })
const TolerantListPromptsResult = ListPromptsResultSchema.extend({
prompts: PromptSchema.array(),
})
export class NeedsAuthError extends Schema.TaggedErrorClass<NeedsAuthError>()("MCP.NeedsAuthError", { export class NeedsAuthError extends Schema.TaggedErrorClass<NeedsAuthError>()("MCP.NeedsAuthError", {
server: Schema.String, server: Schema.String,
@@ -46,6 +53,25 @@ export interface ToolDefinition {
readonly inputSchema: unknown readonly inputSchema: unknown
} }
export interface PromptDefinition {
readonly name: string
readonly description: string | undefined
readonly arguments: ReadonlyArray<{
readonly name: string
readonly description: string | undefined
readonly required: boolean | undefined
}> | undefined
}
export interface PromptMessage {
readonly role: string
readonly content: unknown
}
export interface PromptResult {
readonly messages: ReadonlyArray<PromptMessage>
}
export type CallToolContent = export type CallToolContent =
| { readonly type: "text"; readonly text: string } | { readonly type: "text"; readonly text: string }
| { readonly type: "media"; readonly data: string; readonly mimeType: string } | { readonly type: "media"; readonly data: string; readonly mimeType: string }
@@ -68,6 +94,13 @@ export interface Connection {
readonly instructions: string | undefined readonly instructions: string | undefined
/** Lists the server's tools; returns [] when the server doesn't advertise tool support, fails on a transport error. */ /** Lists the server's tools; returns [] when the server doesn't advertise tool support, fails on a transport error. */
readonly tools: () => Effect.Effect<ToolDefinition[], Error> readonly tools: () => Effect.Effect<ToolDefinition[], Error>
/** Lists the server's prompts; returns [] when the server doesn't advertise prompt support, fails on a transport error. */
readonly prompts: () => Effect.Effect<PromptDefinition[], Error>
/** Invokes a prompt on the server. Interruption aborts the in-flight request. */
readonly prompt: (input: {
readonly name: string
readonly args?: Record<string, string>
}) => Effect.Effect<PromptResult, Error>
/** Invokes a tool on the server. Interruption aborts the in-flight request. */ /** Invokes a tool on the server. Interruption aborts the in-flight request. */
readonly callTool: (input: { readonly callTool: (input: {
readonly name: string readonly name: string
@@ -78,6 +111,8 @@ export interface Connection {
readonly onLog: (callback: (message: LogMessage) => void) => void readonly onLog: (callback: (message: LogMessage) => void) => void
/** Registers a callback fired when the server announces its tool list changed; no-op if unsupported. */ /** Registers a callback fired when the server announces its tool list changed; no-op if unsupported. */
readonly onToolsChanged: (callback: () => void) => void readonly onToolsChanged: (callback: () => void) => void
/** Registers a callback fired when the server announces its prompt list changed; no-op if unsupported. */
readonly onPromptsChanged: (callback: () => void) => void
} }
/** Connects an MCP server; closing the calling scope tears down the transport and any spawned process. */ /** Connects an MCP server; closing the calling scope tears down the transport and any spawned process. */
@@ -166,6 +201,48 @@ export const connect = Effect.fnUntraced(function* (
inputSchema: tool.inputSchema, inputSchema: tool.inputSchema,
})) }))
}), }),
prompts: () =>
Effect.gen(function* () {
if (!client.getServerCapabilities()?.prompts) return []
const prompts = yield* Effect.tryPromise({
try: () =>
paginate(
async (cursor) => {
const params = cursor === undefined ? undefined : { cursor }
return client.request({ method: "prompts/list", params }, TolerantListPromptsResult, {
timeout: requestTimeout,
})
},
(result) => result.prompts,
),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
}).pipe(
Effect.tapError((error) => Effect.logWarning("failed to list MCP prompts", { server, error: error.message })),
)
return prompts.map((prompt) => ({
name: prompt.name,
description: prompt.description,
arguments: prompt.arguments?.map((argument) => ({
name: argument.name,
description: argument.description,
required: argument.required,
})),
}))
}),
prompt: (input) =>
Effect.tryPromise({
try: (signal) =>
client.request(
{ method: "prompts/get", params: { name: input.name, arguments: input.args ?? {} } },
GetPromptResultSchema,
{ signal, timeout: requestTimeout, resetTimeoutOnProgress: true, onprogress: () => {} },
),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
}).pipe(
Effect.map((result) => ({
messages: result.messages.map((message) => ({ role: message.role, content: message.content })),
})),
),
callTool: (input) => callTool: (input) =>
Effect.tryPromise({ Effect.tryPromise({
try: (signal) => try: (signal) =>
@@ -207,6 +284,10 @@ export const connect = Effect.fnUntraced(function* (
if (!client.getServerCapabilities()?.tools?.listChanged) return if (!client.getServerCapabilities()?.tools?.listChanged) return
client.setNotificationHandler(ToolListChangedNotificationSchema, async () => callback()) client.setNotificationHandler(ToolListChangedNotificationSchema, async () => callback())
}, },
onPromptsChanged: (callback) => {
if (!client.getServerCapabilities()?.prompts?.listChanged) return
client.setNotificationHandler(PromptListChangedNotificationSchema, async () => callback())
},
} satisfies Connection } satisfies Connection
} }
+56 -6
View File
@@ -2,6 +2,7 @@ export * as MCP from "./index"
import { Mcp } from "@opencode-ai/schema/mcp" import { Mcp } from "@opencode-ai/schema/mcp"
import { McpEvent } from "@opencode-ai/schema/mcp-event" import { McpEvent } from "@opencode-ai/schema/mcp-event"
import { Command } from "@opencode-ai/schema/command"
import { createHash } from "node:crypto" import { createHash } from "node:crypto"
import { Cause, Context, Deferred, Effect, Exit, FiberSet, Layer, Schema, Scope, Stream } from "effect" import { Cause, Context, Deferred, Effect, Exit, FiberSet, Layer, Schema, Scope, Stream } from "effect"
import { makeLocationNode } from "../effect/app-node" import { makeLocationNode } from "../effect/app-node"
@@ -139,6 +140,7 @@ type ServerEntry = {
scope?: Scope.Closeable scope?: Scope.Closeable
client?: MCPClient.Connection client?: MCPClient.Connection
tools?: ReadonlyArray<Tool> tools?: ReadonlyArray<Tool>
prompts?: ReadonlyArray<Prompt>
// Set when a remote server is registered as an OAuth integration; the credential lives in the global store. // Set when a remote server is registered as an OAuth integration; the credential lives in the global store.
integrationID?: Integration.ID integrationID?: Integration.ID
} }
@@ -309,6 +311,21 @@ export const layer = Layer.effect(
const toTool = (server: ServerName, def: MCPClient.ToolDefinition) => const toTool = (server: ServerName, def: MCPClient.ToolDefinition) =>
new Tool({ server, name: def.name, description: def.description, inputSchema: def.inputSchema }) new Tool({ server, name: def.name, description: def.description, inputSchema: def.inputSchema })
const toPrompt = (server: ServerName, def: MCPClient.PromptDefinition) =>
new Prompt({
server,
name: def.name,
description: def.description,
arguments: def.arguments?.map(
(argument) =>
new PromptArgument({
name: argument.name,
description: argument.description,
required: argument.required,
}),
),
})
const refreshTools = (name: ServerName, entry: ServerEntry, connection: MCPClient.Connection) => const refreshTools = (name: ServerName, entry: ServerEntry, connection: MCPClient.Connection) =>
connection.tools().pipe( connection.tools().pipe(
Effect.map((defs) => { Effect.map((defs) => {
@@ -316,6 +333,17 @@ export const layer = Layer.effect(
}), }),
) )
const refreshPrompts = (name: ServerName, entry: ServerEntry, connection: MCPClient.Connection) =>
connection.prompts().pipe(
Effect.map((defs) => {
entry.prompts = defs.map((def) => toPrompt(name, def))
}),
Effect.andThen(events.publish(Command.Event.Updated, {})),
Effect.catch(() =>
Effect.sync(() => (entry.prompts = [])).pipe(Effect.andThen(events.publish(Command.Event.Updated, {}))),
),
)
const watch = (name: ServerName, entry: ServerEntry, connection: MCPClient.Connection) => { const watch = (name: ServerName, entry: ServerEntry, connection: MCPClient.Connection) => {
connection.onClose(() => { connection.onClose(() => {
// A reconnect closes the previous scope, but the SDK may fire this onclose after the new // A reconnect closes the previous scope, but the SDK may fire this onclose after the new
@@ -323,8 +351,10 @@ export const layer = Layer.effect(
if (entry.client !== connection) return if (entry.client !== connection) return
entry.client = undefined entry.client = undefined
entry.tools = undefined entry.tools = undefined
entry.prompts = undefined
entry.status = { status: "failed", error: "Connection closed" } entry.status = { status: "failed", error: "Connection closed" }
fork(events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)) fork(events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore))
fork(events.publish(Command.Event.Updated, {}).pipe(Effect.ignore))
fork(events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)) fork(events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore))
}) })
connection.onLog((message) => fork(serverLog(name, message).pipe(Effect.ignore))) connection.onLog((message) => fork(serverLog(name, message).pipe(Effect.ignore)))
@@ -336,6 +366,9 @@ export const layer = Layer.effect(
), ),
) )
}) })
connection.onPromptsChanged(() => {
fork(refreshPrompts(name, entry, connection).pipe(Effect.ignore))
})
} }
const serverLog = (server: ServerName, message: MCPClient.LogMessage) => { const serverLog = (server: ServerName, message: MCPClient.LogMessage) => {
@@ -364,13 +397,14 @@ export const layer = Layer.effect(
// List tools as part of connect so a failure here marks the server failed rather than // List tools as part of connect so a failure here marks the server failed rather than
// leaving it connected with a silently empty tool list and no path to recover. // leaving it connected with a silently empty tool list and no path to recover.
const result = yield* MCPClient.connect(name, entry.config, location.directory, authProvider).pipe( const result = yield* MCPClient.connect(name, entry.config, location.directory, authProvider).pipe(
Effect.flatMap((connection) => connection.tools().pipe(Effect.map((defs) => ({ connection, defs })))), Effect.flatMap((connection) => connection.tools().pipe(Effect.map((tools) => ({ connection, tools })))),
Scope.provide(scope), Scope.provide(scope),
Effect.exit, Effect.exit,
) )
if (Exit.isSuccess(result)) { if (Exit.isSuccess(result)) {
entry.client = result.value.connection entry.client = result.value.connection
entry.tools = result.value.defs.map((def) => toTool(name, def)) entry.tools = result.value.tools.map((def) => toTool(name, def))
entry.prompts = []
entry.status = { status: "connected" } entry.status = { status: "connected" }
watch(name, entry, result.value.connection) watch(name, entry, result.value.connection)
yield* Effect.logInfo("mcp connected", { server: name, tools: entry.tools.length }) yield* Effect.logInfo("mcp connected", { server: name, tools: entry.tools.length })
@@ -379,6 +413,7 @@ export const layer = Layer.effect(
// stay invisible to the model. // stay invisible to the model.
yield* events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore) yield* events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
fork(refreshPrompts(name, entry, result.value.connection).pipe(Effect.ignore))
return return
} }
yield* Scope.close(scope, Exit.void) yield* Scope.close(scope, Exit.void)
@@ -416,6 +451,8 @@ export const layer = Layer.effect(
entry.scope = undefined entry.scope = undefined
entry.client = undefined entry.client = undefined
entry.tools = undefined entry.tools = undefined
entry.prompts = undefined
yield* events.publish(Command.Event.Updated, {}).pipe(Effect.ignore)
} }
yield* startServer(name, entry) yield* startServer(name, entry)
}) })
@@ -489,12 +526,25 @@ export const layer = Layer.effect(
.toSorted((a, b) => a.server.localeCompare(b.server)) .toSorted((a, b) => a.server.localeCompare(b.server))
}), }),
prompts: Effect.fn("MCP.prompts")(function* () { prompts: Effect.fn("MCP.prompts")(function* () {
yield* whenAllReady return Array.from(runtime.values())
return [] .flatMap((entry) => entry.prompts ?? [])
.toSorted((a, b) => a.server.localeCompare(b.server) || a.name.localeCompare(b.name))
}), }),
prompt: Effect.fn("MCP.prompt")(function* (input) { prompt: Effect.fn("MCP.prompt")(function* (input) {
yield* gate(input.server) const target = yield* requireServer(input.server)
return undefined yield* Deferred.await(target.entry.startup)
if (!target.entry.client) return undefined
const result = yield* target.entry.client
.prompt({ name: input.name, args: input.args })
.pipe(Effect.catch(() => Effect.succeed(undefined)))
if (!result) return undefined
return new PromptResult({
server: target.name,
name: input.name,
messages: result.messages.map(
(message) => new PromptMessage({ role: message.role, content: message.content }),
),
})
}), }),
resourceCatalog: Effect.fn("MCP.resourceCatalog")(function* () { resourceCatalog: Effect.fn("MCP.resourceCatalog")(function* () {
yield* whenAllReady yield* whenAllReady
-1
View File
@@ -18,7 +18,6 @@ export const Plugin = define({
draft.update("review", (command) => { draft.update("review", (command) => {
command.template = PROMPT_REVIEW.replace("${path}", location.project.directory) command.template = PROMPT_REVIEW.replace("${path}", location.project.directory)
command.description = "review changes [commit|branch|pr], defaults to uncommitted" command.description = "review changes [commit|branch|pr], defaults to uncommitted"
command.subtask = true
}) })
}) })
}), }),
+1
View File
@@ -302,6 +302,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
}), }),
get: (input) => runtime.session.get(input.sessionID), get: (input) => runtime.session.get(input.sessionID),
prompt: runtime.session.prompt, prompt: runtime.session.prompt,
command: runtime.session.command,
interrupt: (input) => runtime.session.interrupt(input.sessionID), interrupt: (input) => runtime.session.interrupt(input.sessionID),
}, },
} satisfies Interface } satisfies Interface
+2 -1
View File
@@ -11,7 +11,7 @@ import { SessionV2 } from "../session"
export interface Interface { export interface Interface {
readonly session: Pick< readonly session: Pick<
SessionV2.Interface, SessionV2.Interface,
"get" | "create" | "messages" | "prompt" | "resume" | "interrupt" | "synthetic" "get" | "create" | "messages" | "prompt" | "command" | "resume" | "interrupt" | "synthetic"
> >
readonly job: Pick<Job.Interface, "start" | "wait" | "block" | "background" | "cancel"> readonly job: Pick<Job.Interface, "start" | "wait" | "block" | "background" | "cancel">
readonly location: { readonly location: {
@@ -50,6 +50,7 @@ export const layerWithCell = (cell: Cell) =>
create: (input) => require(cell, (runtime) => runtime.session.create(input)), create: (input) => require(cell, (runtime) => runtime.session.create(input)),
messages: (input) => require(cell, (runtime) => runtime.session.messages(input)), messages: (input) => require(cell, (runtime) => runtime.session.messages(input)),
prompt: (input) => require(cell, (runtime) => runtime.session.prompt(input)), prompt: (input) => require(cell, (runtime) => runtime.session.prompt(input)),
command: (input) => require(cell, (runtime) => runtime.session.command(input)),
resume: (sessionID) => require(cell, (runtime) => runtime.session.resume(sessionID)), resume: (sessionID) => require(cell, (runtime) => runtime.session.resume(sessionID)),
interrupt: (sessionID) => require(cell, (runtime) => runtime.session.interrupt(sessionID)), interrupt: (sessionID) => require(cell, (runtime) => runtime.session.interrupt(sessionID)),
synthetic: (input) => require(cell, (runtime) => runtime.session.synthetic(input)), synthetic: (input) => require(cell, (runtime) => runtime.session.synthetic(input)),
+46
View File
@@ -40,6 +40,7 @@ import { FSUtil } from "./fs-util"
import { SessionDurable } from "@opencode-ai/schema/durable-event-manifest" import { SessionDurable } from "@opencode-ai/schema/durable-event-manifest"
import { SkillV2 } from "./skill" import { SkillV2 } from "./skill"
import { Job } from "./job" import { Job } from "./job"
import { CommandV2 } from "./command"
export const RevertState = Revert.State export const RevertState = Revert.State
export type RevertState = Revert.State export type RevertState = Revert.State
@@ -130,6 +131,8 @@ export type Error =
| PromptConflictError | PromptConflictError
| BusyError | BusyError
| SkillNotFoundError | SkillNotFoundError
| CommandV2.NotFoundError
| CommandV2.EvaluationError
| MessageNotFoundError | MessageNotFoundError
export interface Interface { export interface Interface {
@@ -175,6 +178,18 @@ export interface Interface {
delivery?: SessionInput.Delivery delivery?: SessionInput.Delivery
resume?: boolean resume?: boolean
}) => Effect.Effect<SessionInput.Admitted, NotFoundError | PromptConflictError> }) => Effect.Effect<SessionInput.Admitted, NotFoundError | PromptConflictError>
readonly command: (input: {
id?: SessionMessage.ID
sessionID: SessionSchema.ID
command: string
arguments?: string
agent?: string
model?: ModelV2.Ref
files?: PromptInput.Prompt["files"]
agents?: PromptInput.Prompt["agents"]
delivery?: SessionInput.Delivery
resume?: boolean
}) => Effect.Effect<SessionInput.Admitted, NotFoundError | PromptConflictError | CommandV2.NotFoundError | CommandV2.EvaluationError>
readonly shell: (input: { readonly shell: (input: {
id?: EventV2.ID id?: EventV2.ID
sessionID: SessionSchema.ID sessionID: SessionSchema.ID
@@ -450,6 +465,37 @@ const layer = Layer.effect(
}), }),
), ),
), ),
command: Effect.fn("V2Session.command")(function* (input) {
const session = yield* result.get(input.sessionID)
const commands = yield* CommandV2.Service.pipe(Effect.provide(locations.get(session.location)))
const command = yield* commands.get(input.command)
if (!command)
return yield* new CommandV2.NotFoundError({
command: input.command,
message: `Command not found: ${input.command}`,
})
const evaluated = yield* commands.evaluate({ name: input.command, arguments: input.arguments })
// TODO(v2 commands): decide whether command-level subtask/background execution belongs in v2 commands.
const agent = command.agent ?? input.agent
const commandAgent = yield* Effect.gen(function* () {
if (!command.agent) return undefined
const agents = yield* AgentV2.Service.pipe(Effect.provide(locations.get(session.location)))
return yield* agents.get(AgentV2.ID.make(command.agent))
})
const model = command.model ?? commandAgent?.model ?? input.model
if (agent !== undefined && session.agent !== AgentV2.ID.make(agent))
yield* result.switchAgent({ sessionID: input.sessionID, agent })
if (model !== undefined) yield* result.switchModel({ sessionID: input.sessionID, model })
return yield* result.prompt({
id: input.id,
sessionID: input.sessionID,
prompt: { text: evaluated.text, files: input.files, agents: input.agents },
delivery: input.delivery,
resume: input.resume,
})
}),
shell: Effect.fn("V2Session.shell")(function* () { shell: Effect.fn("V2Session.shell")(function* () {
return yield* new OperationUnavailableError({ operation: "shell" }) return yield* new OperationUnavailableError({ operation: "shell" })
}), }),
+25 -1
View File
@@ -1,12 +1,22 @@
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Effect } from "effect" import { Effect } from "effect"
import { CommandV2 } from "@opencode-ai/core/command" import { CommandV2 } from "@opencode-ai/core/command"
import { Config } from "@opencode-ai/core/config"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { Location } from "@opencode-ai/core/location"
import { MCP } from "@opencode-ai/core/mcp/index"
import { ModelV2 } from "@opencode-ai/core/model" import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider" import { ProviderV2 } from "@opencode-ai/core/provider"
import { emptyConfigLayer, emptyMcpLayer, testLocationLayer } from "./fixture/mcp"
import { testEffect } from "./lib/effect" import { testEffect } from "./lib/effect"
const it = testEffect(AppNodeBuilder.build(CommandV2.node)) const it = testEffect(
AppNodeBuilder.build(CommandV2.node, [
[MCP.node, emptyMcpLayer],
[Config.node, emptyConfigLayer],
[Location.node, testLocationLayer],
]),
)
describe("CommandV2", () => { describe("CommandV2", () => {
it.effect("applies command transforms and preserves later overrides", () => it.effect("applies command transforms and preserves later overrides", () =>
@@ -53,4 +63,18 @@ describe("CommandV2", () => {
]) ])
}), }),
) )
it.effect("evaluates command template shell blocks", () =>
Effect.gen(function* () {
const command = yield* CommandV2.Service
yield* command.transform((editor) => {
editor.update("review", (command) => {
command.template = "Output: !`printf command-output`"
})
})
expect(yield* command.evaluate({ name: "review" })).toEqual({ text: "Output: command-output" })
}),
)
}) })
+10 -1
View File
@@ -8,14 +8,23 @@ import { ConfigCommandPlugin } from "@opencode-ai/core/config/plugin/command"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FSUtil } from "@opencode-ai/core/fs-util" import { FSUtil } from "@opencode-ai/core/fs-util"
import { Location } from "@opencode-ai/core/location"
import { MCP } from "@opencode-ai/core/mcp/index"
import { ModelV2 } from "@opencode-ai/core/model" import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider" import { ProviderV2 } from "@opencode-ai/core/provider"
import { AbsolutePath } from "@opencode-ai/core/schema" import { AbsolutePath } from "@opencode-ai/core/schema"
import { emptyConfigLayer, emptyMcpLayer, testLocationLayer } from "../fixture/mcp"
import { tmpdir } from "../fixture/tmpdir" import { tmpdir } from "../fixture/tmpdir"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
import { host } from "../plugin/host" import { host } from "../plugin/host"
const it = testEffect(AppNodeBuilder.build(LayerNode.group([CommandV2.node, FSUtil.node]))) const it = testEffect(
AppNodeBuilder.build(LayerNode.group([CommandV2.node, FSUtil.node]), [
[MCP.node, emptyMcpLayer],
[Config.node, emptyConfigLayer],
[Location.node, testLocationLayer],
]),
)
const decode = Schema.decodeUnknownSync(Config.Info) const decode = Schema.decodeUnknownSync(Config.Info)
describe("ConfigCommandPlugin.Plugin", () => { describe("ConfigCommandPlugin.Plugin", () => {
+30
View File
@@ -0,0 +1,30 @@
import { Effect, Layer } from "effect"
import { Config } from "@opencode-ai/core/config"
import { Location } from "@opencode-ai/core/location"
import { MCP } from "@opencode-ai/core/mcp/index"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { location } from "./location"
export const emptyMcpLayer = Layer.succeed(
MCP.Service,
MCP.Service.of({
servers: () => Effect.succeed([]),
tools: () => Effect.succeed([]),
callTool: () => Effect.die("unused mcp.callTool"),
instructions: () => Effect.succeed([]),
prompts: () => Effect.succeed([]),
prompt: () => Effect.succeed(undefined),
resourceCatalog: () => Effect.succeed(new MCP.ResourceCatalog({ resources: [], templates: [] })),
readResource: () => Effect.succeed(undefined),
}),
)
export const emptyConfigLayer = Layer.succeed(
Config.Service,
Config.Service.of({ entries: () => Effect.succeed([]) }),
)
export const testLocationLayer = Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make(process.cwd()) })),
)
@@ -40,7 +40,6 @@ describe("CommandPlugin.Plugin", () => {
expect(yield* command.get("review")).toMatchObject({ expect(yield* command.get("review")).toMatchObject({
name: "review", name: "review",
description: "review changes [commit|branch|pr], defaults to uncommitted", description: "review changes [commit|branch|pr], defaults to uncommitted",
subtask: true,
}) })
}), }),
) )
+1
View File
@@ -61,6 +61,7 @@ export function host(overrides: Overrides = {}): PluginContext {
create: () => Effect.die("unused session.create"), create: () => Effect.die("unused session.create"),
get: () => Effect.die("unused session.get"), get: () => Effect.die("unused session.get"),
prompt: () => Effect.die("unused session.prompt"), prompt: () => Effect.die("unused session.prompt"),
command: () => Effect.die("unused session.command"),
interrupt: () => Effect.die("unused session.interrupt"), interrupt: () => Effect.die("unused session.interrupt"),
}, },
} }
+92 -75
View File
@@ -116,124 +116,140 @@ export type Endpoint4_8Input = {
export type Endpoint4_8Output = EffectValue<ReturnType<RawClient["server.session"]["session.prompt"]>>["data"] export type Endpoint4_8Output = EffectValue<ReturnType<RawClient["server.session"]["session.prompt"]>>["data"]
export type SessionPromptOperation<E = never> = (input: Endpoint4_8Input) => Effect.Effect<Endpoint4_8Output, E> export type SessionPromptOperation<E = never> = (input: Endpoint4_8Input) => Effect.Effect<Endpoint4_8Output, E>
type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.skill"]>[0] type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.command"]>[0]
export type Endpoint4_9Input = { export type Endpoint4_9Input = {
readonly sessionID: Endpoint4_9Request["params"]["sessionID"] readonly sessionID: Endpoint4_9Request["params"]["sessionID"]
readonly id?: Endpoint4_9Request["payload"]["id"] readonly id?: Endpoint4_9Request["payload"]["id"]
readonly skill: Endpoint4_9Request["payload"]["skill"] readonly command: Endpoint4_9Request["payload"]["command"]
readonly arguments?: Endpoint4_9Request["payload"]["arguments"]
readonly agent?: Endpoint4_9Request["payload"]["agent"]
readonly model?: Endpoint4_9Request["payload"]["model"]
readonly files?: Endpoint4_9Request["payload"]["files"]
readonly agents?: Endpoint4_9Request["payload"]["agents"]
readonly delivery?: Endpoint4_9Request["payload"]["delivery"]
readonly resume?: Endpoint4_9Request["payload"]["resume"] readonly resume?: Endpoint4_9Request["payload"]["resume"]
} }
export type Endpoint4_9Output = EffectValue<ReturnType<RawClient["server.session"]["session.skill"]>> export type Endpoint4_9Output = EffectValue<ReturnType<RawClient["server.session"]["session.command"]>>["data"]
export type SessionSkillOperation<E = never> = (input: Endpoint4_9Input) => Effect.Effect<Endpoint4_9Output, E> export type SessionCommandOperation<E = never> = (input: Endpoint4_9Input) => Effect.Effect<Endpoint4_9Output, E>
type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0] type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
export type Endpoint4_10Input = { export type Endpoint4_10Input = {
readonly sessionID: Endpoint4_10Request["params"]["sessionID"] readonly sessionID: Endpoint4_10Request["params"]["sessionID"]
readonly text: Endpoint4_10Request["payload"]["text"] readonly id?: Endpoint4_10Request["payload"]["id"]
readonly description?: Endpoint4_10Request["payload"]["description"] readonly skill: Endpoint4_10Request["payload"]["skill"]
readonly metadata?: Endpoint4_10Request["payload"]["metadata"] readonly resume?: Endpoint4_10Request["payload"]["resume"]
} }
export type Endpoint4_10Output = EffectValue<ReturnType<RawClient["server.session"]["session.synthetic"]>> export type Endpoint4_10Output = EffectValue<ReturnType<RawClient["server.session"]["session.skill"]>>
export type SessionSyntheticOperation<E = never> = (input: Endpoint4_10Input) => Effect.Effect<Endpoint4_10Output, E> export type SessionSkillOperation<E = never> = (input: Endpoint4_10Input) => Effect.Effect<Endpoint4_10Output, E>
type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.compact"]>[0] type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
export type Endpoint4_11Input = { readonly sessionID: Endpoint4_11Request["params"]["sessionID"] } export type Endpoint4_11Input = {
export type Endpoint4_11Output = EffectValue<ReturnType<RawClient["server.session"]["session.compact"]>> readonly sessionID: Endpoint4_11Request["params"]["sessionID"]
export type SessionCompactOperation<E = never> = (input: Endpoint4_11Input) => Effect.Effect<Endpoint4_11Output, E> readonly text: Endpoint4_11Request["payload"]["text"]
readonly description?: Endpoint4_11Request["payload"]["description"]
readonly metadata?: Endpoint4_11Request["payload"]["metadata"]
}
export type Endpoint4_11Output = EffectValue<ReturnType<RawClient["server.session"]["session.synthetic"]>>
export type SessionSyntheticOperation<E = never> = (input: Endpoint4_11Input) => Effect.Effect<Endpoint4_11Output, E>
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.wait"]>[0] type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
export type Endpoint4_12Input = { readonly sessionID: Endpoint4_12Request["params"]["sessionID"] } export type Endpoint4_12Input = { readonly sessionID: Endpoint4_12Request["params"]["sessionID"] }
export type Endpoint4_12Output = EffectValue<ReturnType<RawClient["server.session"]["session.wait"]>> export type Endpoint4_12Output = EffectValue<ReturnType<RawClient["server.session"]["session.compact"]>>
export type SessionWaitOperation<E = never> = (input: Endpoint4_12Input) => Effect.Effect<Endpoint4_12Output, E> export type SessionCompactOperation<E = never> = (input: Endpoint4_12Input) => Effect.Effect<Endpoint4_12Output, E>
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0] type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
export type Endpoint4_13Input = { export type Endpoint4_13Input = { readonly sessionID: Endpoint4_13Request["params"]["sessionID"] }
readonly sessionID: Endpoint4_13Request["params"]["sessionID"] export type Endpoint4_13Output = EffectValue<ReturnType<RawClient["server.session"]["session.wait"]>>
readonly messageID: Endpoint4_13Request["payload"]["messageID"] export type SessionWaitOperation<E = never> = (input: Endpoint4_13Input) => Effect.Effect<Endpoint4_13Output, E>
readonly files?: Endpoint4_13Request["payload"]["files"]
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
export type Endpoint4_14Input = {
readonly sessionID: Endpoint4_14Request["params"]["sessionID"]
readonly messageID: Endpoint4_14Request["payload"]["messageID"]
readonly files?: Endpoint4_14Request["payload"]["files"]
} }
export type Endpoint4_13Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.stage"]>>["data"] export type Endpoint4_14Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.stage"]>>["data"]
export type SessionRevertStageOperation<E = never> = (input: Endpoint4_13Input) => Effect.Effect<Endpoint4_13Output, E> export type SessionRevertStageOperation<E = never> = (input: Endpoint4_14Input) => Effect.Effect<Endpoint4_14Output, E>
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0] type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
export type Endpoint4_14Input = { readonly sessionID: Endpoint4_14Request["params"]["sessionID"] }
export type Endpoint4_14Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.clear"]>>
export type SessionRevertClearOperation<E = never> = (input: Endpoint4_14Input) => Effect.Effect<Endpoint4_14Output, E>
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
export type Endpoint4_15Input = { readonly sessionID: Endpoint4_15Request["params"]["sessionID"] } export type Endpoint4_15Input = { readonly sessionID: Endpoint4_15Request["params"]["sessionID"] }
export type Endpoint4_15Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.commit"]>> export type Endpoint4_15Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.clear"]>>
export type SessionRevertCommitOperation<E = never> = (input: Endpoint4_15Input) => Effect.Effect<Endpoint4_15Output, E> export type SessionRevertClearOperation<E = never> = (input: Endpoint4_15Input) => Effect.Effect<Endpoint4_15Output, E>
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.context"]>[0] type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
export type Endpoint4_16Input = { readonly sessionID: Endpoint4_16Request["params"]["sessionID"] } export type Endpoint4_16Input = { readonly sessionID: Endpoint4_16Request["params"]["sessionID"] }
export type Endpoint4_16Output = EffectValue<ReturnType<RawClient["server.session"]["session.context"]>>["data"] export type Endpoint4_16Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.commit"]>>
export type SessionContextOperation<E = never> = (input: Endpoint4_16Input) => Effect.Effect<Endpoint4_16Output, E> export type SessionRevertCommitOperation<E = never> = (input: Endpoint4_16Input) => Effect.Effect<Endpoint4_16Output, E>
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.context.entry.list"]>[0] type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.context"]>[0]
export type Endpoint4_17Input = { readonly sessionID: Endpoint4_17Request["params"]["sessionID"] } export type Endpoint4_17Input = { readonly sessionID: Endpoint4_17Request["params"]["sessionID"] }
export type Endpoint4_17Output = EffectValue< export type Endpoint4_17Output = EffectValue<ReturnType<RawClient["server.session"]["session.context"]>>["data"]
export type SessionContextOperation<E = never> = (input: Endpoint4_17Input) => Effect.Effect<Endpoint4_17Output, E>
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.context.entry.list"]>[0]
export type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
export type Endpoint4_18Output = EffectValue<
ReturnType<RawClient["server.session"]["session.context.entry.list"]> ReturnType<RawClient["server.session"]["session.context.entry.list"]>
>["data"] >["data"]
export type SessionListContextEntriesOperation<E = never> = ( export type SessionListContextEntriesOperation<E = never> = (
input: Endpoint4_17Input,
) => Effect.Effect<Endpoint4_17Output, E>
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.context.entry.put"]>[0]
export type Endpoint4_18Input = {
readonly sessionID: Endpoint4_18Request["params"]["sessionID"]
readonly key: Endpoint4_18Request["params"]["key"]
readonly value: Endpoint4_18Request["payload"]["value"]
}
export type Endpoint4_18Output = EffectValue<ReturnType<RawClient["server.session"]["session.context.entry.put"]>>
export type SessionPutContextEntryOperation<E = never> = (
input: Endpoint4_18Input, input: Endpoint4_18Input,
) => Effect.Effect<Endpoint4_18Output, E> ) => Effect.Effect<Endpoint4_18Output, E>
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.context.entry.remove"]>[0] type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.context.entry.put"]>[0]
export type Endpoint4_19Input = { export type Endpoint4_19Input = {
readonly sessionID: Endpoint4_19Request["params"]["sessionID"] readonly sessionID: Endpoint4_19Request["params"]["sessionID"]
readonly key: Endpoint4_19Request["params"]["key"] readonly key: Endpoint4_19Request["params"]["key"]
readonly value: Endpoint4_19Request["payload"]["value"]
} }
export type Endpoint4_19Output = EffectValue<ReturnType<RawClient["server.session"]["session.context.entry.remove"]>> export type Endpoint4_19Output = EffectValue<ReturnType<RawClient["server.session"]["session.context.entry.put"]>>
export type SessionRemoveContextEntryOperation<E = never> = ( export type SessionPutContextEntryOperation<E = never> = (
input: Endpoint4_19Input, input: Endpoint4_19Input,
) => Effect.Effect<Endpoint4_19Output, E> ) => Effect.Effect<Endpoint4_19Output, E>
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.history"]>[0] type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.context.entry.remove"]>[0]
export type Endpoint4_20Input = { export type Endpoint4_20Input = {
readonly sessionID: Endpoint4_20Request["params"]["sessionID"] readonly sessionID: Endpoint4_20Request["params"]["sessionID"]
readonly limit?: Endpoint4_20Request["query"]["limit"] readonly key: Endpoint4_20Request["params"]["key"]
readonly after?: Endpoint4_20Request["query"]["after"]
} }
export type Endpoint4_20Output = EffectValue<ReturnType<RawClient["server.session"]["session.history"]>> export type Endpoint4_20Output = EffectValue<ReturnType<RawClient["server.session"]["session.context.entry.remove"]>>
export type SessionHistoryOperation<E = never> = (input: Endpoint4_20Input) => Effect.Effect<Endpoint4_20Output, E> export type SessionRemoveContextEntryOperation<E = never> = (
input: Endpoint4_20Input,
) => Effect.Effect<Endpoint4_20Output, E>
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.events"]>[0] type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.history"]>[0]
export type Endpoint4_21Input = { export type Endpoint4_21Input = {
readonly sessionID: Endpoint4_21Request["params"]["sessionID"] readonly sessionID: Endpoint4_21Request["params"]["sessionID"]
readonly limit?: Endpoint4_21Request["query"]["limit"]
readonly after?: Endpoint4_21Request["query"]["after"] readonly after?: Endpoint4_21Request["query"]["after"]
} }
export type Endpoint4_21Output = StreamValue<EffectValue<ReturnType<RawClient["server.session"]["session.events"]>>> export type Endpoint4_21Output = EffectValue<ReturnType<RawClient["server.session"]["session.history"]>>
export type SessionEventsOperation<E = never> = (input: Endpoint4_21Input) => Stream.Stream<Endpoint4_21Output, E> export type SessionHistoryOperation<E = never> = (input: Endpoint4_21Input) => Effect.Effect<Endpoint4_21Output, E>
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0] type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.events"]>[0]
export type Endpoint4_22Input = { readonly sessionID: Endpoint4_22Request["params"]["sessionID"] } export type Endpoint4_22Input = {
export type Endpoint4_22Output = EffectValue<ReturnType<RawClient["server.session"]["session.interrupt"]>> readonly sessionID: Endpoint4_22Request["params"]["sessionID"]
export type SessionInterruptOperation<E = never> = (input: Endpoint4_22Input) => Effect.Effect<Endpoint4_22Output, E> readonly after?: Endpoint4_22Request["query"]["after"]
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.background"]>[0]
export type Endpoint4_23Input = { readonly sessionID: Endpoint4_23Request["params"]["sessionID"] }
export type Endpoint4_23Output = EffectValue<ReturnType<RawClient["server.session"]["session.background"]>>
export type SessionBackgroundOperation<E = never> = (input: Endpoint4_23Input) => Effect.Effect<Endpoint4_23Output, E>
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.message"]>[0]
export type Endpoint4_24Input = {
readonly sessionID: Endpoint4_24Request["params"]["sessionID"]
readonly messageID: Endpoint4_24Request["params"]["messageID"]
} }
export type Endpoint4_24Output = EffectValue<ReturnType<RawClient["server.session"]["session.message"]>>["data"] export type Endpoint4_22Output = StreamValue<EffectValue<ReturnType<RawClient["server.session"]["session.events"]>>>
export type SessionMessageOperation<E = never> = (input: Endpoint4_24Input) => Effect.Effect<Endpoint4_24Output, E> export type SessionEventsOperation<E = never> = (input: Endpoint4_22Input) => Stream.Stream<Endpoint4_22Output, E>
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
export type Endpoint4_23Input = { readonly sessionID: Endpoint4_23Request["params"]["sessionID"] }
export type Endpoint4_23Output = EffectValue<ReturnType<RawClient["server.session"]["session.interrupt"]>>
export type SessionInterruptOperation<E = never> = (input: Endpoint4_23Input) => Effect.Effect<Endpoint4_23Output, E>
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.background"]>[0]
export type Endpoint4_24Input = { readonly sessionID: Endpoint4_24Request["params"]["sessionID"] }
export type Endpoint4_24Output = EffectValue<ReturnType<RawClient["server.session"]["session.background"]>>
export type SessionBackgroundOperation<E = never> = (input: Endpoint4_24Input) => Effect.Effect<Endpoint4_24Output, E>
type Endpoint4_25Request = Parameters<RawClient["server.session"]["session.message"]>[0]
export type Endpoint4_25Input = {
readonly sessionID: Endpoint4_25Request["params"]["sessionID"]
readonly messageID: Endpoint4_25Request["params"]["messageID"]
}
export type Endpoint4_25Output = EffectValue<ReturnType<RawClient["server.session"]["session.message"]>>["data"]
export type SessionMessageOperation<E = never> = (input: Endpoint4_25Input) => Effect.Effect<Endpoint4_25Output, E>
export interface SessionApi<E = never> { export interface SessionApi<E = never> {
readonly list: SessionListOperation<E> readonly list: SessionListOperation<E>
@@ -245,6 +261,7 @@ export interface SessionApi<E = never> {
readonly switchModel: SessionSwitchModelOperation<E> readonly switchModel: SessionSwitchModelOperation<E>
readonly rename: SessionRenameOperation<E> readonly rename: SessionRenameOperation<E>
readonly prompt: SessionPromptOperation<E> readonly prompt: SessionPromptOperation<E>
readonly command: SessionCommandOperation<E>
readonly skill: SessionSkillOperation<E> readonly skill: SessionSkillOperation<E>
readonly synthetic: SessionSyntheticOperation<E> readonly synthetic: SessionSyntheticOperation<E>
readonly compact: SessionCompactOperation<E> readonly compact: SessionCompactOperation<E>
+1 -1
View File
@@ -1,3 +1,3 @@
import type { SessionApi } from "./generated/api.js" import type { SessionApi } from "./generated/api.js"
export type SessionDomain = Pick<SessionApi<unknown>, "create" | "get" | "prompt" | "interrupt"> export type SessionDomain = Pick<SessionApi<unknown>, "create" | "get" | "prompt" | "command" | "interrupt">
+18
View File
@@ -89,6 +89,24 @@ export class SkillNotFoundError extends Schema.TaggedErrorClass<SkillNotFoundErr
{ httpApiStatus: 404 }, { httpApiStatus: 404 },
) {} ) {}
export class CommandNotFoundError extends Schema.TaggedErrorClass<CommandNotFoundError>()(
"CommandNotFoundError",
{
command: Schema.String,
message: Schema.String,
},
{ httpApiStatus: 404 },
) {}
export class CommandEvaluationError extends Schema.TaggedErrorClass<CommandEvaluationError>()(
"CommandEvaluationError",
{
command: Schema.String,
message: Schema.String,
},
{ httpApiStatus: 500 },
) {}
export class InvalidCursorError extends Schema.TaggedErrorClass<InvalidCursorError>()( export class InvalidCursorError extends Schema.TaggedErrorClass<InvalidCursorError>()(
"InvalidCursorError", "InvalidCursorError",
{ message: Schema.String }, { message: Schema.String },
+28
View File
@@ -10,6 +10,8 @@ import { Context, Effect, Encoding, Result, Schema, Struct } from "effect"
import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { import {
ConflictError, ConflictError,
CommandEvaluationError,
CommandNotFoundError,
InvalidCursorError, InvalidCursorError,
InvalidRequestError, InvalidRequestError,
MessageNotFoundError, MessageNotFoundError,
@@ -258,6 +260,32 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
}), }),
), ),
) )
.add(
HttpApiEndpoint.post("session.command", "/api/session/:sessionID/command", {
params: { sessionID: Session.ID },
payload: Schema.Struct({
id: SessionMessage.ID.pipe(Schema.optional),
command: Schema.String,
arguments: Schema.String.pipe(Schema.optional),
agent: Schema.String.pipe(Schema.optional),
model: Model.Ref.pipe(Schema.optional),
files: PromptInput.Prompt.fields.files,
agents: PromptInput.Prompt.fields.agents,
delivery: SessionInput.Delivery.pipe(Schema.optional),
resume: Schema.Boolean.pipe(Schema.optional),
}),
success: Schema.Struct({ data: SessionInput.Admitted }),
error: [ConflictError, SessionNotFoundError, CommandNotFoundError, CommandEvaluationError],
})
.middleware(sessionLocationMiddleware)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.session.command",
summary: "Run command",
description: "Resolve a slash command into prompt input, admit it durably, and schedule execution unless resume is false.",
}),
),
)
.add( .add(
HttpApiEndpoint.post("session.skill", "/api/session/:sessionID/skill", { HttpApiEndpoint.post("session.skill", "/api/session/:sessionID/skill", {
params: { sessionID: Session.ID }, params: { sessionID: Session.ID },
+8
View File
@@ -1,9 +1,12 @@
export * as Command from "./command.js" export * as Command from "./command.js"
import { Schema } from "effect" import { Schema } from "effect"
import { define, inventory } from "./event.js"
import { optional } from "./schema.js" import { optional } from "./schema.js"
import { Model } from "./model.js" import { Model } from "./model.js"
const Updated = define({ type: "command.updated", schema: {} })
export interface Info extends Schema.Schema.Type<typeof Info> {} export interface Info extends Schema.Schema.Type<typeof Info> {}
export const Info = Schema.Struct({ export const Info = Schema.Struct({
name: Schema.String, name: Schema.String,
@@ -13,3 +16,8 @@ export const Info = Schema.Struct({
model: Model.Ref.pipe(optional), model: Model.Ref.pipe(optional),
subtask: Schema.Boolean.pipe(optional), subtask: Schema.Boolean.pipe(optional),
}).annotate({ identifier: "CommandV2.Info" }) }).annotate({ identifier: "CommandV2.Info" })
export const Event = {
Updated,
Definitions: inventory(Updated),
}
+2
View File
@@ -2,6 +2,7 @@ export * as EventManifest from "./event-manifest.js"
import { Agent } from "./agent.js" import { Agent } from "./agent.js"
import { Catalog } from "./catalog.js" import { Catalog } from "./catalog.js"
import { Command } from "./command.js"
import { Durable } from "./durable-event-manifest.js" import { Durable } from "./durable-event-manifest.js"
import { Event } from "./event.js" import { Event } from "./event.js"
import { FileSystem } from "./filesystem.js" import { FileSystem } from "./filesystem.js"
@@ -53,6 +54,7 @@ const featureDefinitions = Event.inventory(
...Permission.Event.Definitions, ...Permission.Event.Definitions,
...Plugin.Event.Definitions, ...Plugin.Event.Definitions,
...ProjectDirectories.Event.Definitions, ...ProjectDirectories.Event.Definitions,
...Command.Event.Definitions,
...Skill.Event.Definitions, ...Skill.Event.Definitions,
...FileSystemWatcher.Event.Definitions, ...FileSystemWatcher.Event.Definitions,
...Pty.Event.Definitions, ...Pty.Event.Definitions,
+174
View File
@@ -142,7 +142,9 @@ import type {
ProjectListResponses, ProjectListResponses,
ProjectUpdateErrors, ProjectUpdateErrors,
ProjectUpdateResponses, ProjectUpdateResponses,
PromptAgentAttachment,
PromptInput, PromptInput,
PromptInputFileAttachment,
ProviderAuthErrors, ProviderAuthErrors,
ProviderAuthResponses, ProviderAuthResponses,
ProviderListErrors, ProviderListErrors,
@@ -181,6 +183,7 @@ import type {
SessionChildrenResponses, SessionChildrenResponses,
SessionCommandErrors, SessionCommandErrors,
SessionCommandResponses, SessionCommandResponses,
SessionContextEntryKey,
SessionCreateErrors, SessionCreateErrors,
SessionCreateResponses, SessionCreateResponses,
SessionDeleteErrors, SessionDeleteErrors,
@@ -349,8 +352,16 @@ import type {
V2SessionActiveResponses, V2SessionActiveResponses,
V2SessionBackgroundErrors, V2SessionBackgroundErrors,
V2SessionBackgroundResponses, V2SessionBackgroundResponses,
V2SessionCommandErrors,
V2SessionCommandResponses,
V2SessionCompactErrors, V2SessionCompactErrors,
V2SessionCompactResponses, V2SessionCompactResponses,
V2SessionContextEntryListErrors,
V2SessionContextEntryListResponses,
V2SessionContextEntryPutErrors,
V2SessionContextEntryPutResponses,
V2SessionContextEntryRemoveErrors,
V2SessionContextEntryRemoveResponses,
V2SessionContextErrors, V2SessionContextErrors,
V2SessionContextResponses, V2SessionContextResponses,
V2SessionCreateErrors, V2SessionCreateErrors,
@@ -5224,6 +5235,113 @@ export class Revert extends HeyApiClient {
} }
} }
export class Entry extends HeyApiClient {
/**
* List context entries
*
* List API-managed context entries attached to the session's system context.
*/
public list<ThrowOnError extends boolean = false>(
parameters: {
sessionID: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }])
return (options?.client ?? this.client).get<
V2SessionContextEntryListResponses,
V2SessionContextEntryListErrors,
ThrowOnError
>({
url: "/api/session/{sessionID}/context-entry",
...options,
...params,
})
}
/**
* Remove context entry
*
* Remove one context entry; the removal is announced to the model at the next turn boundary.
*/
public remove<ThrowOnError extends boolean = false>(
parameters: {
sessionID: string
key: SessionContextEntryKey
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "sessionID" },
{ in: "path", key: "key" },
],
},
],
)
return (options?.client ?? this.client).delete<
V2SessionContextEntryRemoveResponses,
V2SessionContextEntryRemoveErrors,
ThrowOnError
>({
url: "/api/session/{sessionID}/context-entry/{key}",
...options,
...params,
})
}
/**
* Put context entry
*
* Attach or replace one durable context entry. The value is rendered into the session's system context; changes announce as updates at the next turn boundary.
*/
public put<ThrowOnError extends boolean = false>(
parameters: {
sessionID: string
key: SessionContextEntryKey
value?: unknown
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "sessionID" },
{ in: "path", key: "key" },
{ in: "body", key: "value" },
],
},
],
)
return (options?.client ?? this.client).put<
V2SessionContextEntryPutResponses,
V2SessionContextEntryPutErrors,
ThrowOnError
>({
url: "/api/session/{sessionID}/context-entry/{key}",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
}
export class Context extends HeyApiClient {
private _entry?: Entry
get entry(): Entry {
return (this._entry ??= new Entry({ client: this.client }))
}
}
export class Permission2 extends HeyApiClient { export class Permission2 extends HeyApiClient {
/** /**
* List session permission requests * List session permission requests
@@ -5781,6 +5899,57 @@ export class Session3 extends HeyApiClient {
}) })
} }
/**
* Run command
*
* Resolve a slash command into prompt input, admit it durably, and schedule execution unless resume is false.
*/
public command<ThrowOnError extends boolean = false>(
parameters: {
sessionID: string
id?: string
command?: string
arguments?: string
agent?: string
model?: ModelRef
files?: Array<PromptInputFileAttachment>
agents?: Array<PromptAgentAttachment>
delivery?: "steer" | "queue"
resume?: boolean
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "sessionID" },
{ in: "body", key: "id" },
{ in: "body", key: "command" },
{ in: "body", key: "arguments" },
{ in: "body", key: "agent" },
{ in: "body", key: "model" },
{ in: "body", key: "files" },
{ in: "body", key: "agents" },
{ in: "body", key: "delivery" },
{ in: "body", key: "resume" },
],
},
],
)
return (options?.client ?? this.client).post<V2SessionCommandResponses, V2SessionCommandErrors, ThrowOnError>({
url: "/api/session/{sessionID}/command",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
/** /**
* Activate skill * Activate skill
* *
@@ -6089,6 +6258,11 @@ export class Session3 extends HeyApiClient {
return (this._revert ??= new Revert({ client: this.client })) return (this._revert ??= new Revert({ client: this.client }))
} }
private _context?: Context
get context2(): Context {
return (this._context ??= new Context({ client: this.client }))
}
private _permission?: Permission2 private _permission?: Permission2
get permission(): Permission2 { get permission(): Permission2 {
return (this._permission ??= new Permission2({ client: this.client })) return (this._permission ??= new Permission2({ client: this.client }))
+223
View File
@@ -64,6 +64,7 @@ export type Event =
| EventPermissionV2Replied | EventPermissionV2Replied
| EventPluginAdded | EventPluginAdded
| EventProjectDirectoriesUpdated | EventProjectDirectoriesUpdated
| EventCommandUpdated
| EventSkillUpdated | EventSkillUpdated
| EventFileWatcherUpdated | EventFileWatcherUpdated
| EventPtyCreated | EventPtyCreated
@@ -1370,6 +1371,13 @@ export type GlobalEvent = {
projectID: string projectID: string
} }
} }
| {
id: string
type: "command.updated"
properties: {
[key: string]: unknown
}
}
| { | {
id: string id: string
type: "skill.updated" type: "skill.updated"
@@ -2840,6 +2848,18 @@ export type ConflictError = {
resource?: string resource?: string
} }
export type CommandNotFoundError = {
_tag: "CommandNotFoundError"
command: string
message: string
}
export type CommandEvaluationError = {
_tag: "CommandEvaluationError"
command: string
message: string
}
export type SkillNotFoundError = { export type SkillNotFoundError = {
_tag: "SkillNotFoundError" _tag: "SkillNotFoundError"
skill: string skill: string
@@ -3061,6 +3081,7 @@ export type V2Event =
| PermissionV2Replied | PermissionV2Replied
| PluginAdded | PluginAdded
| ProjectDirectoriesUpdated | ProjectDirectoriesUpdated
| CommandUpdated
| SkillUpdated | SkillUpdated
| FileWatcherUpdated | FileWatcherUpdated
| PtyCreated | PtyCreated
@@ -4427,6 +4448,13 @@ export type SessionMessage =
| SessionMessageAssistant | SessionMessageAssistant
| SessionMessageCompaction | SessionMessageCompaction
export type SessionContextEntryKey = string
export type SessionContextEntryInfo = {
key: SessionContextEntryKey
value: unknown
}
export type SessionNextAgentSwitched = { export type SessionNextAgentSwitched = {
id: string id: string
metadata?: { metadata?: {
@@ -5923,6 +5951,23 @@ export type ProjectDirectoriesUpdated = {
} }
} }
export type CommandUpdated = {
id: string
metadata?: {
[key: string]: unknown
}
type: "command.updated"
durable?: {
aggregateID: string
seq: number
version: number
}
location?: LocationRef
data: {
[key: string]: unknown
}
}
export type SkillUpdated = { export type SkillUpdated = {
id: string id: string
metadata?: { metadata?: {
@@ -7320,6 +7365,14 @@ export type EventProjectDirectoriesUpdated = {
} }
} }
export type EventCommandUpdated = {
id: string
type: "command.updated"
properties: {
[key: string]: unknown
}
}
export type EventSkillUpdated = { export type EventSkillUpdated = {
id: string id: string
type: "skill.updated" type: "skill.updated"
@@ -12307,6 +12360,61 @@ export type V2SessionPromptResponses = {
export type V2SessionPromptResponse = V2SessionPromptResponses[keyof V2SessionPromptResponses] export type V2SessionPromptResponse = V2SessionPromptResponses[keyof V2SessionPromptResponses]
export type V2SessionCommandData = {
body: {
id?: string
command: string
arguments?: string
agent?: string
model?: ModelRef
files?: Array<PromptInputFileAttachment>
agents?: Array<PromptAgentAttachment>
delivery?: "steer" | "queue"
resume?: boolean
}
path: {
sessionID: string
}
query?: never
url: "/api/session/{sessionID}/command"
}
export type V2SessionCommandErrors = {
/**
* InvalidRequestError
*/
400: InvalidRequestError
/**
* UnauthorizedError
*/
401: UnauthorizedError
/**
* SessionNotFoundError | CommandNotFoundError
*/
404: CommandNotFoundError | SessionNotFoundError
/**
* ConflictError
*/
409: ConflictError
/**
* CommandEvaluationError
*/
500: CommandEvaluationError
}
export type V2SessionCommandError = V2SessionCommandErrors[keyof V2SessionCommandErrors]
export type V2SessionCommandResponses = {
/**
* Success
*/
200: {
data: SessionInputAdmitted
}
}
export type V2SessionCommandResponse = V2SessionCommandResponses[keyof V2SessionCommandResponses]
export type V2SessionSkillData = { export type V2SessionSkillData = {
body: { body: {
id?: string id?: string
@@ -12644,6 +12752,121 @@ export type V2SessionContextResponses = {
export type V2SessionContextResponse = V2SessionContextResponses[keyof V2SessionContextResponses] export type V2SessionContextResponse = V2SessionContextResponses[keyof V2SessionContextResponses]
export type V2SessionContextEntryListData = {
body?: never
path: {
sessionID: string
}
query?: never
url: "/api/session/{sessionID}/context-entry"
}
export type V2SessionContextEntryListErrors = {
/**
* InvalidRequestError
*/
400: InvalidRequestError
/**
* UnauthorizedError
*/
401: UnauthorizedError
/**
* SessionNotFoundError
*/
404: SessionNotFoundError
}
export type V2SessionContextEntryListError = V2SessionContextEntryListErrors[keyof V2SessionContextEntryListErrors]
export type V2SessionContextEntryListResponses = {
/**
* Success
*/
200: {
data: Array<SessionContextEntryInfo>
}
}
export type V2SessionContextEntryListResponse =
V2SessionContextEntryListResponses[keyof V2SessionContextEntryListResponses]
export type V2SessionContextEntryRemoveData = {
body?: never
path: {
sessionID: string
key: SessionContextEntryKey
}
query?: never
url: "/api/session/{sessionID}/context-entry/{key}"
}
export type V2SessionContextEntryRemoveErrors = {
/**
* InvalidRequestError
*/
400: InvalidRequestError
/**
* UnauthorizedError
*/
401: UnauthorizedError
/**
* SessionNotFoundError
*/
404: SessionNotFoundError
}
export type V2SessionContextEntryRemoveError =
V2SessionContextEntryRemoveErrors[keyof V2SessionContextEntryRemoveErrors]
export type V2SessionContextEntryRemoveResponses = {
/**
* <No Content>
*/
204: void
}
export type V2SessionContextEntryRemoveResponse =
V2SessionContextEntryRemoveResponses[keyof V2SessionContextEntryRemoveResponses]
export type V2SessionContextEntryPutData = {
body: {
value: unknown
}
path: {
sessionID: string
key: SessionContextEntryKey
}
query?: never
url: "/api/session/{sessionID}/context-entry/{key}"
}
export type V2SessionContextEntryPutErrors = {
/**
* InvalidRequestError
*/
400: InvalidRequestError
/**
* UnauthorizedError
*/
401: UnauthorizedError
/**
* SessionNotFoundError
*/
404: SessionNotFoundError
}
export type V2SessionContextEntryPutError = V2SessionContextEntryPutErrors[keyof V2SessionContextEntryPutErrors]
export type V2SessionContextEntryPutResponses = {
/**
* <No Content>
*/
204: void
}
export type V2SessionContextEntryPutResponse =
V2SessionContextEntryPutResponses[keyof V2SessionContextEntryPutResponses]
export type V2SessionHistoryData = { export type V2SessionHistoryData = {
body?: never body?: never
path: { path: {
+56
View File
@@ -6,6 +6,8 @@ import { Api } from "../api"
import { SessionsCursor } from "@opencode-ai/protocol/groups/session" import { SessionsCursor } from "@opencode-ai/protocol/groups/session"
import { import {
ConflictError, ConflictError,
CommandEvaluationError,
CommandNotFoundError,
InvalidCursorError, InvalidCursorError,
MessageNotFoundError, MessageNotFoundError,
ServiceUnavailableError, ServiceUnavailableError,
@@ -216,6 +218,60 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
} }
}), }),
) )
.handle(
"session.command",
Effect.fn(function* (ctx) {
return {
data: yield* session
.command({
sessionID: ctx.params.sessionID,
id: ctx.payload.id,
command: ctx.payload.command,
arguments: ctx.payload.arguments,
agent: ctx.payload.agent,
model: ctx.payload.model,
files: ctx.payload.files,
agents: ctx.payload.agents,
delivery: ctx.payload.delivery,
resume: ctx.payload.resume,
})
.pipe(
Effect.catchTag("Session.NotFoundError", (error) =>
Effect.fail(
new SessionNotFoundError({
sessionID: error.sessionID,
message: `Session not found: ${error.sessionID}`,
}),
),
),
Effect.catchTag("Command.NotFoundError", (error) =>
Effect.fail(
new CommandNotFoundError({
command: error.command,
message: error.message,
}),
),
),
Effect.catchTag("Command.EvaluationError", (error) =>
Effect.fail(
new CommandEvaluationError({
command: error.command,
message: error.message,
}),
),
),
Effect.catchTag("Session.PromptConflictError", (error) =>
Effect.fail(
new ConflictError({
message: `Prompt message ID conflicts with an existing durable record: ${error.messageID}`,
resource: error.messageID,
}),
),
),
),
}
}),
)
.handle( .handle(
"session.skill", "session.skill",
Effect.fn(function* (ctx) { Effect.fn(function* (ctx) {
+40 -9
View File
@@ -1125,15 +1125,46 @@ export function Prompt(props: PromptProps) {
const restOfInput = firstLineEnd === -1 ? "" : inputText.slice(firstLineEnd + 1) const restOfInput = firstLineEnd === -1 ? "" : inputText.slice(firstLineEnd + 1)
const args = firstLineArgs.join(" ") + (restOfInput ? "\n" + restOfInput : "") const args = firstLineArgs.join(" ") + (restOfInput ? "\n" + restOfInput : "")
void sdk.client.session.command({ void sdk.api.session
sessionID, .command({
command: command.slice(1), sessionID,
arguments: args, command: command.slice(1),
agent: agent.id, arguments: args,
model: `${selectedModel.providerID}/${selectedModel.modelID}`, agent: agent.id,
variant, model: { providerID: selectedModel.providerID, id: selectedModel.modelID, variant },
parts: nonTextParts.filter((x) => x.type === "file"), files: nonTextParts.flatMap((part) =>
}) part.type === "file"
? [
{
uri: part.url,
name: part.filename,
source: part.source
? {
start: part.source.text.start,
end: part.source.text.end,
text: part.source.text.value,
}
: undefined,
},
]
: [],
),
agents: nonTextParts.flatMap((part) =>
part.type === "agent"
? [
{
name: part.name,
source: part.source
? { start: part.source.start, end: part.source.end, text: part.source.value }
: undefined,
},
]
: [],
),
})
.catch((error) => {
toast.show({ title: "Failed to run command", message: errorMessage(error), variant: "error" })
})
} else if ( } else if (
inputText.startsWith("/") && inputText.startsWith("/") &&
(data.location.skill.list(currentLocation()) ?? []).some( (data.location.skill.list(currentLocation()) ?? []).some(
+3
View File
@@ -208,6 +208,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
case "agent.updated": case "agent.updated":
void result.location.agent.refresh(event.location) void result.location.agent.refresh(event.location)
break break
case "command.updated":
void result.location.command.refresh(event.location)
break
case "skill.updated": case "skill.updated":
void result.location.skill.refresh(event.location) void result.location.skill.refresh(event.location)
break break