feat(core): generalize session input inbox (#36005)
This commit is contained in:
parent
7eea97184a
commit
984cab7938
47 changed files with 1590 additions and 767 deletions
|
|
@ -153,7 +153,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
||||||
if (!("sessionID" in event.data) || event.data.sessionID !== input.sessionID) continue
|
if (!("sessionID" in event.data) || event.data.sessionID !== input.sessionID) continue
|
||||||
const time = toMillis("created" in event ? event.created : undefined)
|
const time = toMillis("created" in event ? event.created : undefined)
|
||||||
|
|
||||||
if (event.type === "session.prompt.promoted") {
|
if (event.type === "session.input.promoted") {
|
||||||
if (event.data.inputID === messageID) {
|
if (event.data.inputID === messageID) {
|
||||||
promoted = true
|
promoted = true
|
||||||
continue
|
continue
|
||||||
|
|
@ -409,10 +409,8 @@ export async function runNonInteractivePrompt(input: Input) {
|
||||||
{
|
{
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
id: messageID,
|
id: messageID,
|
||||||
prompt: {
|
text: [input.message, ...prepared.flatMap((file) => (file.text ? [file.text] : []))].join("\n\n"),
|
||||||
text: [input.message, ...prepared.flatMap((file) => (file.text ? [file.text] : []))].join("\n\n"),
|
files: prepared.flatMap((file) => (file.attachment ? [file.attachment] : [])),
|
||||||
files: prepared.flatMap((file) => (file.attachment ? [file.attachment] : [])),
|
|
||||||
},
|
|
||||||
delivery: "steer",
|
delivery: "steer",
|
||||||
},
|
},
|
||||||
{ signal: admission.signal },
|
{ signal: admission.signal },
|
||||||
|
|
|
||||||
|
|
@ -457,12 +457,13 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||||
}
|
}
|
||||||
|
|
||||||
const reduce = (child: ChildState, event: V2Event) => {
|
const reduce = (child: ChildState, event: V2Event) => {
|
||||||
if (event.type === "session.prompt.admitted") {
|
if (event.type === "session.input.admitted") {
|
||||||
child.prompts.set(event.data.inputID, event.data.prompt.text)
|
if (event.data.input.type === "user") child.prompts.set(event.data.inputID, event.data.input.data.text)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.type === "session.prompt.promoted") {
|
if (event.type === "session.input.promoted") {
|
||||||
const prompt = child.prompts.get(event.data.inputID) ?? ""
|
const prompt = child.prompts.get(event.data.inputID)
|
||||||
|
if (prompt === undefined) return
|
||||||
child.prompts.delete(event.data.inputID)
|
child.prompts.delete(event.data.inputID)
|
||||||
if (userFrame(child, event.data.inputID, prompt)) {
|
if (userFrame(child, event.data.inputID, prompt)) {
|
||||||
touch(child, event.created)
|
touch(child, event.created)
|
||||||
|
|
|
||||||
|
|
@ -562,7 +562,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||||
}
|
}
|
||||||
input.trace?.write("recv.event", event)
|
input.trace?.write("recv.event", event)
|
||||||
subagents.main(event)
|
subagents.main(event)
|
||||||
if (event.type === "session.prompt.promoted") {
|
if (event.type === "session.input.promoted") {
|
||||||
if (state.wait?.messageID === event.data.inputID) state.wait.promoted = true
|
if (state.wait?.messageID === event.data.inputID) state.wait.promoted = true
|
||||||
state.messageIDs.add(event.data.inputID)
|
state.messageIDs.add(event.data.inputID)
|
||||||
write([], { phase: "running", status: "waiting for assistant" })
|
write([], { phase: "running", status: "waiting for assistant" })
|
||||||
|
|
@ -1052,11 +1052,9 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||||
{
|
{
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
id: messageID,
|
id: messageID,
|
||||||
prompt: {
|
text: [next.prompt.text, ...prepared.flatMap((file) => (file.text ? [file.text] : []))].join("\n\n"),
|
||||||
text: [next.prompt.text, ...prepared.flatMap((file) => (file.text ? [file.text] : []))].join("\n\n"),
|
files: attachments.length ? attachments : undefined,
|
||||||
files: attachments.length ? attachments : undefined,
|
agents: agents.length ? agents : undefined,
|
||||||
agents: agents.length ? agents : undefined,
|
|
||||||
},
|
|
||||||
delivery: "steer",
|
delivery: "steer",
|
||||||
},
|
},
|
||||||
{ signal: next.signal },
|
{ signal: next.signal },
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,10 @@ type Endpoint5_10Request = Parameters<RawClient["server.session"]["session.promp
|
||||||
export type Endpoint5_10Input = {
|
export type Endpoint5_10Input = {
|
||||||
readonly sessionID: Endpoint5_10Request["params"]["sessionID"]
|
readonly sessionID: Endpoint5_10Request["params"]["sessionID"]
|
||||||
readonly id?: Endpoint5_10Request["payload"]["id"]
|
readonly id?: Endpoint5_10Request["payload"]["id"]
|
||||||
readonly prompt: Endpoint5_10Request["payload"]["prompt"]
|
readonly text: Endpoint5_10Request["payload"]["text"]
|
||||||
|
readonly files?: Endpoint5_10Request["payload"]["files"]
|
||||||
|
readonly agents?: Endpoint5_10Request["payload"]["agents"]
|
||||||
|
readonly metadata?: Endpoint5_10Request["payload"]["metadata"]
|
||||||
readonly delivery?: Endpoint5_10Request["payload"]["delivery"]
|
readonly delivery?: Endpoint5_10Request["payload"]["delivery"]
|
||||||
readonly resume?: Endpoint5_10Request["payload"]["resume"]
|
readonly resume?: Endpoint5_10Request["payload"]["resume"]
|
||||||
}
|
}
|
||||||
|
|
@ -167,12 +170,14 @@ export type SessionSkillOperation<E = never> = (input: Endpoint5_12Input) => Eff
|
||||||
type Endpoint5_13Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
|
type Endpoint5_13Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
|
||||||
export type Endpoint5_13Input = {
|
export type Endpoint5_13Input = {
|
||||||
readonly sessionID: Endpoint5_13Request["params"]["sessionID"]
|
readonly sessionID: Endpoint5_13Request["params"]["sessionID"]
|
||||||
|
readonly id?: Endpoint5_13Request["payload"]["id"]
|
||||||
readonly text: Endpoint5_13Request["payload"]["text"]
|
readonly text: Endpoint5_13Request["payload"]["text"]
|
||||||
readonly description?: Endpoint5_13Request["payload"]["description"]
|
readonly description?: Endpoint5_13Request["payload"]["description"]
|
||||||
readonly metadata?: Endpoint5_13Request["payload"]["metadata"]
|
readonly metadata?: Endpoint5_13Request["payload"]["metadata"]
|
||||||
|
readonly delivery?: Endpoint5_13Request["payload"]["delivery"]
|
||||||
readonly resume?: Endpoint5_13Request["payload"]["resume"]
|
readonly resume?: Endpoint5_13Request["payload"]["resume"]
|
||||||
}
|
}
|
||||||
export type Endpoint5_13Output = EffectValue<ReturnType<RawClient["server.session"]["session.synthetic"]>>
|
export type Endpoint5_13Output = EffectValue<ReturnType<RawClient["server.session"]["session.synthetic"]>>["data"]
|
||||||
export type SessionSyntheticOperation<E = never> = (input: Endpoint5_13Input) => Effect.Effect<Endpoint5_13Output, E>
|
export type SessionSyntheticOperation<E = never> = (input: Endpoint5_13Input) => Effect.Effect<Endpoint5_13Output, E>
|
||||||
|
|
||||||
type Endpoint5_14Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
|
type Endpoint5_14Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
|
||||||
|
|
|
||||||
|
|
@ -162,14 +162,25 @@ type Endpoint5_10Request = Parameters<RawClient["server.session"]["session.promp
|
||||||
type Endpoint5_10Input = {
|
type Endpoint5_10Input = {
|
||||||
readonly sessionID: Endpoint5_10Request["params"]["sessionID"]
|
readonly sessionID: Endpoint5_10Request["params"]["sessionID"]
|
||||||
readonly id?: Endpoint5_10Request["payload"]["id"]
|
readonly id?: Endpoint5_10Request["payload"]["id"]
|
||||||
readonly prompt: Endpoint5_10Request["payload"]["prompt"]
|
readonly text: Endpoint5_10Request["payload"]["text"]
|
||||||
|
readonly files?: Endpoint5_10Request["payload"]["files"]
|
||||||
|
readonly agents?: Endpoint5_10Request["payload"]["agents"]
|
||||||
|
readonly metadata?: Endpoint5_10Request["payload"]["metadata"]
|
||||||
readonly delivery?: Endpoint5_10Request["payload"]["delivery"]
|
readonly delivery?: Endpoint5_10Request["payload"]["delivery"]
|
||||||
readonly resume?: Endpoint5_10Request["payload"]["resume"]
|
readonly resume?: Endpoint5_10Request["payload"]["resume"]
|
||||||
}
|
}
|
||||||
const Endpoint5_10 = (raw: RawClient["server.session"]) => (input: Endpoint5_10Input) =>
|
const Endpoint5_10 = (raw: RawClient["server.session"]) => (input: Endpoint5_10Input) =>
|
||||||
raw["session.prompt"]({
|
raw["session.prompt"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
payload: { id: input["id"], prompt: input["prompt"], delivery: input["delivery"], resume: input["resume"] },
|
payload: {
|
||||||
|
id: input["id"],
|
||||||
|
text: input["text"],
|
||||||
|
files: input["files"],
|
||||||
|
agents: input["agents"],
|
||||||
|
metadata: input["metadata"],
|
||||||
|
delivery: input["delivery"],
|
||||||
|
resume: input["resume"],
|
||||||
|
},
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
|
|
@ -223,21 +234,28 @@ const Endpoint5_12 = (raw: RawClient["server.session"]) => (input: Endpoint5_12I
|
||||||
type Endpoint5_13Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
|
type Endpoint5_13Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
|
||||||
type Endpoint5_13Input = {
|
type Endpoint5_13Input = {
|
||||||
readonly sessionID: Endpoint5_13Request["params"]["sessionID"]
|
readonly sessionID: Endpoint5_13Request["params"]["sessionID"]
|
||||||
|
readonly id?: Endpoint5_13Request["payload"]["id"]
|
||||||
readonly text: Endpoint5_13Request["payload"]["text"]
|
readonly text: Endpoint5_13Request["payload"]["text"]
|
||||||
readonly description?: Endpoint5_13Request["payload"]["description"]
|
readonly description?: Endpoint5_13Request["payload"]["description"]
|
||||||
readonly metadata?: Endpoint5_13Request["payload"]["metadata"]
|
readonly metadata?: Endpoint5_13Request["payload"]["metadata"]
|
||||||
|
readonly delivery?: Endpoint5_13Request["payload"]["delivery"]
|
||||||
readonly resume?: Endpoint5_13Request["payload"]["resume"]
|
readonly resume?: Endpoint5_13Request["payload"]["resume"]
|
||||||
}
|
}
|
||||||
const Endpoint5_13 = (raw: RawClient["server.session"]) => (input: Endpoint5_13Input) =>
|
const Endpoint5_13 = (raw: RawClient["server.session"]) => (input: Endpoint5_13Input) =>
|
||||||
raw["session.synthetic"]({
|
raw["session.synthetic"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
payload: {
|
payload: {
|
||||||
|
id: input["id"],
|
||||||
text: input["text"],
|
text: input["text"],
|
||||||
description: input["description"],
|
description: input["description"],
|
||||||
metadata: input["metadata"],
|
metadata: input["metadata"],
|
||||||
|
delivery: input["delivery"],
|
||||||
resume: input["resume"],
|
resume: input["resume"],
|
||||||
},
|
},
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(
|
||||||
|
Effect.mapError(mapClientError),
|
||||||
|
Effect.map((value) => value.data),
|
||||||
|
)
|
||||||
|
|
||||||
type Endpoint5_14Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
|
type Endpoint5_14Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
|
||||||
type Endpoint5_14Input = {
|
type Endpoint5_14Input = {
|
||||||
|
|
|
||||||
|
|
@ -516,7 +516,15 @@ export function make(options: ClientOptions) {
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/prompt`,
|
path: `/api/session/${encodeURIComponent(input.sessionID)}/prompt`,
|
||||||
body: { id: input["id"], prompt: input["prompt"], delivery: input["delivery"], resume: input["resume"] },
|
body: {
|
||||||
|
id: input["id"],
|
||||||
|
text: input["text"],
|
||||||
|
files: input["files"],
|
||||||
|
agents: input["agents"],
|
||||||
|
metadata: input["metadata"],
|
||||||
|
delivery: input["delivery"],
|
||||||
|
resume: input["resume"],
|
||||||
|
},
|
||||||
successStatus: 200,
|
successStatus: 200,
|
||||||
declaredStatuses: [409, 400, 404, 401],
|
declaredStatuses: [409, 400, 404, 401],
|
||||||
empty: false,
|
empty: false,
|
||||||
|
|
@ -558,22 +566,24 @@ export function make(options: ClientOptions) {
|
||||||
requestOptions,
|
requestOptions,
|
||||||
),
|
),
|
||||||
synthetic: (input: SessionSyntheticInput, requestOptions?: RequestOptions) =>
|
synthetic: (input: SessionSyntheticInput, requestOptions?: RequestOptions) =>
|
||||||
request<SessionSyntheticOutput>(
|
request<{ readonly data: SessionSyntheticOutput }>(
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/synthetic`,
|
path: `/api/session/${encodeURIComponent(input.sessionID)}/synthetic`,
|
||||||
body: {
|
body: {
|
||||||
|
id: input["id"],
|
||||||
text: input["text"],
|
text: input["text"],
|
||||||
description: input["description"],
|
description: input["description"],
|
||||||
metadata: input["metadata"],
|
metadata: input["metadata"],
|
||||||
|
delivery: input["delivery"],
|
||||||
resume: input["resume"],
|
resume: input["resume"],
|
||||||
},
|
},
|
||||||
successStatus: 204,
|
successStatus: 200,
|
||||||
declaredStatuses: [404, 400, 401],
|
declaredStatuses: [409, 404, 400, 401],
|
||||||
empty: true,
|
empty: false,
|
||||||
},
|
},
|
||||||
requestOptions,
|
requestOptions,
|
||||||
),
|
).then((value) => value.data),
|
||||||
shell: (input: SessionShellInput, requestOptions?: RequestOptions) =>
|
shell: (input: SessionShellInput, requestOptions?: RequestOptions) =>
|
||||||
request<SessionShellOutput>(
|
request<SessionShellOutput>(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -543,73 +543,120 @@ export type SessionPromptInput = {
|
||||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||||
readonly id?: {
|
readonly id?: {
|
||||||
readonly id?: string | null
|
readonly id?: string | null
|
||||||
readonly prompt: {
|
readonly text: string
|
||||||
readonly text: string
|
readonly files?: ReadonlyArray<{
|
||||||
readonly files?: ReadonlyArray<{
|
readonly uri: string
|
||||||
readonly uri: string
|
readonly name?: string
|
||||||
readonly name?: string
|
readonly description?: string
|
||||||
readonly description?: string
|
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
}>
|
||||||
}>
|
readonly agents?: ReadonlyArray<{
|
||||||
readonly agents?: ReadonlyArray<{
|
readonly name: string
|
||||||
readonly name: string
|
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
}>
|
||||||
}>
|
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||||
}
|
|
||||||
readonly delivery?: "steer" | "queue" | null
|
readonly delivery?: "steer" | "queue" | null
|
||||||
readonly resume?: boolean | null
|
readonly resume?: boolean | null
|
||||||
}["id"]
|
}["id"]
|
||||||
readonly prompt: {
|
readonly text: {
|
||||||
readonly id?: string | null
|
readonly id?: string | null
|
||||||
readonly prompt: {
|
readonly text: string
|
||||||
readonly text: string
|
readonly files?: ReadonlyArray<{
|
||||||
readonly files?: ReadonlyArray<{
|
readonly uri: string
|
||||||
readonly uri: string
|
readonly name?: string
|
||||||
readonly name?: string
|
readonly description?: string
|
||||||
readonly description?: string
|
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
}>
|
||||||
}>
|
readonly agents?: ReadonlyArray<{
|
||||||
readonly agents?: ReadonlyArray<{
|
readonly name: string
|
||||||
readonly name: string
|
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
}>
|
||||||
}>
|
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||||
}
|
|
||||||
readonly delivery?: "steer" | "queue" | null
|
readonly delivery?: "steer" | "queue" | null
|
||||||
readonly resume?: boolean | null
|
readonly resume?: boolean | null
|
||||||
}["prompt"]
|
}["text"]
|
||||||
|
readonly files?: {
|
||||||
|
readonly id?: string | null
|
||||||
|
readonly text: string
|
||||||
|
readonly files?: ReadonlyArray<{
|
||||||
|
readonly uri: string
|
||||||
|
readonly name?: string
|
||||||
|
readonly description?: string
|
||||||
|
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||||
|
}>
|
||||||
|
readonly agents?: ReadonlyArray<{
|
||||||
|
readonly name: string
|
||||||
|
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||||
|
}>
|
||||||
|
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||||
|
readonly delivery?: "steer" | "queue" | null
|
||||||
|
readonly resume?: boolean | null
|
||||||
|
}["files"]
|
||||||
|
readonly agents?: {
|
||||||
|
readonly id?: string | null
|
||||||
|
readonly text: string
|
||||||
|
readonly files?: ReadonlyArray<{
|
||||||
|
readonly uri: string
|
||||||
|
readonly name?: string
|
||||||
|
readonly description?: string
|
||||||
|
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||||
|
}>
|
||||||
|
readonly agents?: ReadonlyArray<{
|
||||||
|
readonly name: string
|
||||||
|
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||||
|
}>
|
||||||
|
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||||
|
readonly delivery?: "steer" | "queue" | null
|
||||||
|
readonly resume?: boolean | null
|
||||||
|
}["agents"]
|
||||||
|
readonly metadata?: {
|
||||||
|
readonly id?: string | null
|
||||||
|
readonly text: string
|
||||||
|
readonly files?: ReadonlyArray<{
|
||||||
|
readonly uri: string
|
||||||
|
readonly name?: string
|
||||||
|
readonly description?: string
|
||||||
|
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||||
|
}>
|
||||||
|
readonly agents?: ReadonlyArray<{
|
||||||
|
readonly name: string
|
||||||
|
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||||
|
}>
|
||||||
|
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||||
|
readonly delivery?: "steer" | "queue" | null
|
||||||
|
readonly resume?: boolean | null
|
||||||
|
}["metadata"]
|
||||||
readonly delivery?: {
|
readonly delivery?: {
|
||||||
readonly id?: string | null
|
readonly id?: string | null
|
||||||
readonly prompt: {
|
readonly text: string
|
||||||
readonly text: string
|
readonly files?: ReadonlyArray<{
|
||||||
readonly files?: ReadonlyArray<{
|
readonly uri: string
|
||||||
readonly uri: string
|
readonly name?: string
|
||||||
readonly name?: string
|
readonly description?: string
|
||||||
readonly description?: string
|
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
}>
|
||||||
}>
|
readonly agents?: ReadonlyArray<{
|
||||||
readonly agents?: ReadonlyArray<{
|
readonly name: string
|
||||||
readonly name: string
|
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
}>
|
||||||
}>
|
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||||
}
|
|
||||||
readonly delivery?: "steer" | "queue" | null
|
readonly delivery?: "steer" | "queue" | null
|
||||||
readonly resume?: boolean | null
|
readonly resume?: boolean | null
|
||||||
}["delivery"]
|
}["delivery"]
|
||||||
readonly resume?: {
|
readonly resume?: {
|
||||||
readonly id?: string | null
|
readonly id?: string | null
|
||||||
readonly prompt: {
|
readonly text: string
|
||||||
readonly text: string
|
readonly files?: ReadonlyArray<{
|
||||||
readonly files?: ReadonlyArray<{
|
readonly uri: string
|
||||||
readonly uri: string
|
readonly name?: string
|
||||||
readonly name?: string
|
readonly description?: string
|
||||||
readonly description?: string
|
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
}>
|
||||||
}>
|
readonly agents?: ReadonlyArray<{
|
||||||
readonly agents?: ReadonlyArray<{
|
readonly name: string
|
||||||
readonly name: string
|
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
}>
|
||||||
}>
|
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||||
}
|
|
||||||
readonly delivery?: "steer" | "queue" | null
|
readonly delivery?: "steer" | "queue" | null
|
||||||
readonly resume?: boolean | null
|
readonly resume?: boolean | null
|
||||||
}["resume"]
|
}["resume"]
|
||||||
|
|
@ -620,7 +667,10 @@ export type SessionPromptOutput = {
|
||||||
readonly admittedSeq: number
|
readonly admittedSeq: number
|
||||||
readonly id: string
|
readonly id: string
|
||||||
readonly sessionID: string
|
readonly sessionID: string
|
||||||
readonly prompt: {
|
readonly timeCreated: number
|
||||||
|
readonly promotedSeq?: number
|
||||||
|
readonly type: "user"
|
||||||
|
readonly data: {
|
||||||
readonly text: string
|
readonly text: string
|
||||||
readonly files?: ReadonlyArray<{
|
readonly files?: ReadonlyArray<{
|
||||||
readonly data: string
|
readonly data: string
|
||||||
|
|
@ -634,10 +684,9 @@ export type SessionPromptOutput = {
|
||||||
readonly name: string
|
readonly name: string
|
||||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||||
}>
|
}>
|
||||||
|
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||||
}
|
}
|
||||||
readonly delivery: "steer" | "queue"
|
readonly delivery: "steer" | "queue"
|
||||||
readonly timeCreated: number
|
|
||||||
readonly promotedSeq?: number
|
|
||||||
}
|
}
|
||||||
}["data"]
|
}["data"]
|
||||||
|
|
||||||
|
|
@ -821,7 +870,10 @@ export type SessionCommandOutput = {
|
||||||
readonly admittedSeq: number
|
readonly admittedSeq: number
|
||||||
readonly id: string
|
readonly id: string
|
||||||
readonly sessionID: string
|
readonly sessionID: string
|
||||||
readonly prompt: {
|
readonly timeCreated: number
|
||||||
|
readonly promotedSeq?: number
|
||||||
|
readonly type: "user"
|
||||||
|
readonly data: {
|
||||||
readonly text: string
|
readonly text: string
|
||||||
readonly files?: ReadonlyArray<{
|
readonly files?: ReadonlyArray<{
|
||||||
readonly data: string
|
readonly data: string
|
||||||
|
|
@ -835,10 +887,9 @@ export type SessionCommandOutput = {
|
||||||
readonly name: string
|
readonly name: string
|
||||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||||
}>
|
}>
|
||||||
|
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||||
}
|
}
|
||||||
readonly delivery: "steer" | "queue"
|
readonly delivery: "steer" | "queue"
|
||||||
readonly timeCreated: number
|
|
||||||
readonly promotedSeq?: number
|
|
||||||
}
|
}
|
||||||
}["data"]
|
}["data"]
|
||||||
|
|
||||||
|
|
@ -865,33 +916,72 @@ export type SessionSkillOutput = void
|
||||||
|
|
||||||
export type SessionSyntheticInput = {
|
export type SessionSyntheticInput = {
|
||||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||||
readonly text: {
|
readonly id?: {
|
||||||
|
readonly id?: string | null
|
||||||
readonly text: string
|
readonly text: string
|
||||||
readonly description?: string | null
|
readonly description?: string | null
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||||
|
readonly delivery?: "steer" | "queue" | null
|
||||||
|
readonly resume?: boolean | null
|
||||||
|
}["id"]
|
||||||
|
readonly text: {
|
||||||
|
readonly id?: string | null
|
||||||
|
readonly text: string
|
||||||
|
readonly description?: string | null
|
||||||
|
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||||
|
readonly delivery?: "steer" | "queue" | null
|
||||||
readonly resume?: boolean | null
|
readonly resume?: boolean | null
|
||||||
}["text"]
|
}["text"]
|
||||||
readonly description?: {
|
readonly description?: {
|
||||||
|
readonly id?: string | null
|
||||||
readonly text: string
|
readonly text: string
|
||||||
readonly description?: string | null
|
readonly description?: string | null
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||||
|
readonly delivery?: "steer" | "queue" | null
|
||||||
readonly resume?: boolean | null
|
readonly resume?: boolean | null
|
||||||
}["description"]
|
}["description"]
|
||||||
readonly metadata?: {
|
readonly metadata?: {
|
||||||
|
readonly id?: string | null
|
||||||
readonly text: string
|
readonly text: string
|
||||||
readonly description?: string | null
|
readonly description?: string | null
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||||
|
readonly delivery?: "steer" | "queue" | null
|
||||||
readonly resume?: boolean | null
|
readonly resume?: boolean | null
|
||||||
}["metadata"]
|
}["metadata"]
|
||||||
readonly resume?: {
|
readonly delivery?: {
|
||||||
|
readonly id?: string | null
|
||||||
readonly text: string
|
readonly text: string
|
||||||
readonly description?: string | null
|
readonly description?: string | null
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||||
|
readonly delivery?: "steer" | "queue" | null
|
||||||
|
readonly resume?: boolean | null
|
||||||
|
}["delivery"]
|
||||||
|
readonly resume?: {
|
||||||
|
readonly id?: string | null
|
||||||
|
readonly text: string
|
||||||
|
readonly description?: string | null
|
||||||
|
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||||
|
readonly delivery?: "steer" | "queue" | null
|
||||||
readonly resume?: boolean | null
|
readonly resume?: boolean | null
|
||||||
}["resume"]
|
}["resume"]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionSyntheticOutput = void
|
export type SessionSyntheticOutput = {
|
||||||
|
readonly data: {
|
||||||
|
readonly admittedSeq: number
|
||||||
|
readonly id: string
|
||||||
|
readonly sessionID: string
|
||||||
|
readonly timeCreated: number
|
||||||
|
readonly promotedSeq?: number
|
||||||
|
readonly type: "synthetic"
|
||||||
|
readonly data: {
|
||||||
|
readonly text: string
|
||||||
|
readonly description?: string
|
||||||
|
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||||
|
}
|
||||||
|
readonly delivery: "steer" | "queue"
|
||||||
|
}
|
||||||
|
}["data"]
|
||||||
|
|
||||||
export type SessionShellInput = {
|
export type SessionShellInput = {
|
||||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||||
|
|
@ -908,11 +998,11 @@ export type SessionCompactInput = {
|
||||||
|
|
||||||
export type SessionCompactOutput = {
|
export type SessionCompactOutput = {
|
||||||
readonly data: {
|
readonly data: {
|
||||||
readonly type: "compaction"
|
|
||||||
readonly admittedSeq: number
|
readonly admittedSeq: number
|
||||||
readonly id: string
|
readonly id: string
|
||||||
readonly sessionID: string
|
readonly sessionID: string
|
||||||
readonly timeCreated: number
|
readonly timeCreated: number
|
||||||
|
readonly type: "compaction"
|
||||||
readonly handledSeq?: number
|
readonly handledSeq?: number
|
||||||
}
|
}
|
||||||
}["data"]
|
}["data"]
|
||||||
|
|
@ -1229,7 +1319,7 @@ export type SessionLogOutput =
|
||||||
readonly id: string
|
readonly id: string
|
||||||
readonly created: number
|
readonly created: number
|
||||||
readonly metadata?: { readonly [x: string]: unknown }
|
readonly metadata?: { readonly [x: string]: unknown }
|
||||||
readonly type: "session.prompt.promoted"
|
readonly type: "session.input.promoted"
|
||||||
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
|
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
|
||||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||||
readonly data: { readonly sessionID: string; readonly inputID: string }
|
readonly data: { readonly sessionID: string; readonly inputID: string }
|
||||||
|
|
@ -1238,28 +1328,42 @@ export type SessionLogOutput =
|
||||||
readonly id: string
|
readonly id: string
|
||||||
readonly created: number
|
readonly created: number
|
||||||
readonly metadata?: { readonly [x: string]: unknown }
|
readonly metadata?: { readonly [x: string]: unknown }
|
||||||
readonly type: "session.prompt.admitted"
|
readonly type: "session.input.admitted"
|
||||||
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
|
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
|
||||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||||
readonly data: {
|
readonly data: {
|
||||||
readonly sessionID: string
|
readonly sessionID: string
|
||||||
readonly inputID: string
|
readonly inputID: string
|
||||||
readonly prompt: {
|
readonly input:
|
||||||
readonly text: string
|
| {
|
||||||
readonly files?: ReadonlyArray<{
|
readonly type: "user"
|
||||||
readonly data: string
|
readonly data: {
|
||||||
readonly mime: string
|
readonly text: string
|
||||||
readonly source: { readonly type: "inline" } | { readonly type: "uri"; readonly uri: string }
|
readonly files?: ReadonlyArray<{
|
||||||
readonly name?: string
|
readonly data: string
|
||||||
readonly description?: string
|
readonly mime: string
|
||||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
readonly source: { readonly type: "inline" } | { readonly type: "uri"; readonly uri: string }
|
||||||
}>
|
readonly name?: string
|
||||||
readonly agents?: ReadonlyArray<{
|
readonly description?: string
|
||||||
readonly name: string
|
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
}>
|
||||||
}>
|
readonly agents?: ReadonlyArray<{
|
||||||
}
|
readonly name: string
|
||||||
readonly delivery: "steer" | "queue"
|
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||||
|
}>
|
||||||
|
readonly metadata?: { readonly [x: string]: unknown }
|
||||||
|
}
|
||||||
|
readonly delivery: "steer" | "queue"
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
readonly type: "synthetic"
|
||||||
|
readonly data: {
|
||||||
|
readonly text: string
|
||||||
|
readonly description?: string
|
||||||
|
readonly metadata?: { readonly [x: string]: unknown }
|
||||||
|
}
|
||||||
|
readonly delivery: "steer" | "queue"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
|
|
@ -4588,7 +4692,7 @@ export type EventSubscribeOutput =
|
||||||
readonly id: string
|
readonly id: string
|
||||||
readonly created: number
|
readonly created: number
|
||||||
readonly metadata?: { readonly [x: string]: unknown }
|
readonly metadata?: { readonly [x: string]: unknown }
|
||||||
readonly type: "session.prompt.promoted"
|
readonly type: "session.input.promoted"
|
||||||
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
|
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
|
||||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||||
readonly data: { readonly sessionID: string; readonly inputID: string }
|
readonly data: { readonly sessionID: string; readonly inputID: string }
|
||||||
|
|
@ -4597,28 +4701,42 @@ export type EventSubscribeOutput =
|
||||||
readonly id: string
|
readonly id: string
|
||||||
readonly created: number
|
readonly created: number
|
||||||
readonly metadata?: { readonly [x: string]: unknown }
|
readonly metadata?: { readonly [x: string]: unknown }
|
||||||
readonly type: "session.prompt.admitted"
|
readonly type: "session.input.admitted"
|
||||||
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
|
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
|
||||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||||
readonly data: {
|
readonly data: {
|
||||||
readonly sessionID: string
|
readonly sessionID: string
|
||||||
readonly inputID: string
|
readonly inputID: string
|
||||||
readonly prompt: {
|
readonly input:
|
||||||
readonly text: string
|
| {
|
||||||
readonly files?: ReadonlyArray<{
|
readonly type: "user"
|
||||||
readonly data: string
|
readonly data: {
|
||||||
readonly mime: string
|
readonly text: string
|
||||||
readonly source: { readonly type: "inline" } | { readonly type: "uri"; readonly uri: string }
|
readonly files?: ReadonlyArray<{
|
||||||
readonly name?: string
|
readonly data: string
|
||||||
readonly description?: string
|
readonly mime: string
|
||||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
readonly source: { readonly type: "inline" } | { readonly type: "uri"; readonly uri: string }
|
||||||
}>
|
readonly name?: string
|
||||||
readonly agents?: ReadonlyArray<{
|
readonly description?: string
|
||||||
readonly name: string
|
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
}>
|
||||||
}>
|
readonly agents?: ReadonlyArray<{
|
||||||
}
|
readonly name: string
|
||||||
readonly delivery: "steer" | "queue"
|
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||||
|
}>
|
||||||
|
readonly metadata?: { readonly [x: string]: unknown }
|
||||||
|
}
|
||||||
|
readonly delivery: "steer" | "queue"
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
readonly type: "synthetic"
|
||||||
|
readonly data: {
|
||||||
|
readonly text: string
|
||||||
|
readonly description?: string
|
||||||
|
readonly metadata?: { readonly [x: string]: unknown }
|
||||||
|
}
|
||||||
|
readonly delivery: "steer" | "queue"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,9 @@ test("Core and Server reuse the authoritative Schema and Protocol values", () =>
|
||||||
expect(ProjectV2.Current).toBe(Project.Current)
|
expect(ProjectV2.Current).toBe(Project.Current)
|
||||||
expect(ProjectV2.Directory).toBe(Project.Directory)
|
expect(ProjectV2.Directory).toBe(Project.Directory)
|
||||||
expect(ProjectV2.Directories).toBe(Project.Directories)
|
expect(ProjectV2.Directories).toBe(Project.Directories)
|
||||||
expect(CoreSessionInput.Admitted).toBe(SessionInput.Admitted)
|
expect(CoreSessionInput.Message).toBe(SessionInput.Message)
|
||||||
|
expect(CoreSessionInput.User).toBe(SessionInput.User)
|
||||||
|
expect(CoreSessionInput.Synthetic).toBe(SessionInput.Synthetic)
|
||||||
expect(CoreSessionMessage.Info).toBe(SessionMessage.Info)
|
expect(CoreSessionMessage.Info).toBe(SessionMessage.Info)
|
||||||
expect(Api.groups["server.session"].identifier).toBe("server.session")
|
expect(Api.groups["server.session"].identifier).toBe("server.session")
|
||||||
expect(Api.groups["server.project"].identifier).toBe("server.project")
|
expect(Api.groups["server.project"].identifier).toBe("server.project")
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,7 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
|
||||||
})
|
})
|
||||||
const admitted = yield* client.session.prompt({
|
const admitted = yield* client.session.prompt({
|
||||||
sessionID: Session.ID.make("ses_test"),
|
sessionID: Session.ID.make("ses_test"),
|
||||||
prompt: Prompt.make({ text: "Hello" }),
|
text: "Hello",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
yield* client.session.compact({ sessionID: Session.ID.make("ses_test") })
|
yield* client.session.compact({ sessionID: Session.ID.make("ses_test") })
|
||||||
|
|
@ -201,7 +201,7 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
|
||||||
expect(Object.getPrototypeOf(result.created)).toBe(Object.prototype)
|
expect(Object.getPrototypeOf(result.created)).toBe(Object.prototype)
|
||||||
expect(result.created.id).toBe("ses_test")
|
expect(result.created.id).toBe("ses_test")
|
||||||
expect(Object.getPrototypeOf(result.admitted)).toBe(Object.prototype)
|
expect(Object.getPrototypeOf(result.admitted)).toBe(Object.prototype)
|
||||||
expect(Object.getPrototypeOf(result.admitted.prompt)).toBe(Object.prototype)
|
expect(Object.getPrototypeOf(result.admitted.data)).toBe(Object.prototype)
|
||||||
expect(DateTime.toEpochMillis(result.admitted.timeCreated)).toBe(1_717_171_717_000)
|
expect(DateTime.toEpochMillis(result.admitted.timeCreated)).toBe(1_717_171_717_000)
|
||||||
expect(result.context).toEqual([])
|
expect(result.context).toEqual([])
|
||||||
expect(logQueries[0]).toEqual({ after: "0" })
|
expect(logQueries[0]).toEqual({ after: "0" })
|
||||||
|
|
@ -259,7 +259,8 @@ const admission = {
|
||||||
admittedSeq: 0,
|
admittedSeq: 0,
|
||||||
id: "msg_test",
|
id: "msg_test",
|
||||||
sessionID: "ses_test",
|
sessionID: "ses_test",
|
||||||
prompt: { text: "Hello" },
|
type: "user",
|
||||||
|
data: { text: "Hello" },
|
||||||
delivery: "steer",
|
delivery: "steer",
|
||||||
timeCreated: 1_717_171_717_000,
|
timeCreated: 1_717_171_717_000,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -274,6 +274,7 @@ test("session methods use the public HTTP contract", async () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (url.includes("/prompt")) return Response.json(admission)
|
if (url.includes("/prompt")) return Response.json(admission)
|
||||||
|
if (url.includes("/synthetic")) return Response.json(syntheticAdmission)
|
||||||
if (url.endsWith("/compact")) return Response.json(compactionAdmission)
|
if (url.endsWith("/compact")) return Response.json(compactionAdmission)
|
||||||
if (url.includes("/context")) return Response.json({ data: [] })
|
if (url.includes("/context")) return Response.json({ data: [] })
|
||||||
if (url.includes("/message/")) return Response.json({ data: modelSwitchedMessage })
|
if (url.includes("/message/")) return Response.json({ data: modelSwitchedMessage })
|
||||||
|
|
@ -294,7 +295,13 @@ test("session methods use the public HTTP contract", async () => {
|
||||||
})
|
})
|
||||||
const admitted = await client.session.prompt({
|
const admitted = await client.session.prompt({
|
||||||
sessionID: "ses_test",
|
sessionID: "ses_test",
|
||||||
prompt: { text: "Hello" },
|
text: "Hello",
|
||||||
|
resume: false,
|
||||||
|
})
|
||||||
|
const synthetic = await client.session.synthetic({
|
||||||
|
sessionID: "ses_test",
|
||||||
|
text: "Completed",
|
||||||
|
delivery: "queue",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
await client.session.compact({ sessionID: "ses_test" })
|
await client.session.compact({ sessionID: "ses_test" })
|
||||||
|
|
@ -309,6 +316,7 @@ test("session methods use the public HTTP contract", async () => {
|
||||||
expect(active).toEqual({ ses_test: { type: "running" } })
|
expect(active).toEqual({ ses_test: { type: "running" } })
|
||||||
expect(created.id).toBe("ses_test")
|
expect(created.id).toBe("ses_test")
|
||||||
expect(admitted.id).toBe("msg_test")
|
expect(admitted.id).toBe("msg_test")
|
||||||
|
expect(synthetic).toMatchObject({ type: "synthetic", data: { text: "Completed" }, delivery: "queue" })
|
||||||
expect(context).toEqual([])
|
expect(context).toEqual([])
|
||||||
expect(log).toEqual([modelSwitchedEvent, synced])
|
expect(log).toEqual([modelSwitchedEvent, synced])
|
||||||
expect(message).toEqual(modelSwitchedMessage)
|
expect(message).toEqual(modelSwitchedMessage)
|
||||||
|
|
@ -319,6 +327,7 @@ test("session methods use the public HTTP contract", async () => {
|
||||||
["POST", "http://localhost:3000/api/session/ses_test/agent"],
|
["POST", "http://localhost:3000/api/session/ses_test/agent"],
|
||||||
["POST", "http://localhost:3000/api/session/ses_test/model"],
|
["POST", "http://localhost:3000/api/session/ses_test/model"],
|
||||||
["POST", "http://localhost:3000/api/session/ses_test/prompt"],
|
["POST", "http://localhost:3000/api/session/ses_test/prompt"],
|
||||||
|
["POST", "http://localhost:3000/api/session/ses_test/synthetic"],
|
||||||
["POST", "http://localhost:3000/api/session/ses_test/compact"],
|
["POST", "http://localhost:3000/api/session/ses_test/compact"],
|
||||||
["POST", "http://localhost:3000/api/session/ses_test/wait"],
|
["POST", "http://localhost:3000/api/session/ses_test/wait"],
|
||||||
["GET", "http://localhost:3000/api/session/ses_test/context"],
|
["GET", "http://localhost:3000/api/session/ses_test/context"],
|
||||||
|
|
@ -329,7 +338,14 @@ test("session methods use the public HTTP contract", async () => {
|
||||||
const body = requests.find((request) => request.url.endsWith("/api/session/ses_test/prompt"))?.init?.body
|
const body = requests.find((request) => request.url.endsWith("/api/session/ses_test/prompt"))?.init?.body
|
||||||
if (typeof body !== "string") throw new Error("Expected JSON request body")
|
if (typeof body !== "string") throw new Error("Expected JSON request body")
|
||||||
expect(JSON.parse(body)).toEqual({
|
expect(JSON.parse(body)).toEqual({
|
||||||
prompt: { text: "Hello" },
|
text: "Hello",
|
||||||
|
resume: false,
|
||||||
|
})
|
||||||
|
const syntheticBody = requests.find((request) => request.url.endsWith("/synthetic"))?.init?.body
|
||||||
|
if (typeof syntheticBody !== "string") throw new Error("Expected JSON synthetic request body")
|
||||||
|
expect(JSON.parse(syntheticBody)).toEqual({
|
||||||
|
text: "Completed",
|
||||||
|
delivery: "queue",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -392,12 +408,25 @@ const admission = {
|
||||||
admittedSeq: 0,
|
admittedSeq: 0,
|
||||||
id: "msg_test",
|
id: "msg_test",
|
||||||
sessionID: "ses_test",
|
sessionID: "ses_test",
|
||||||
prompt: { text: "Hello" },
|
type: "user",
|
||||||
|
data: { text: "Hello" },
|
||||||
delivery: "steer",
|
delivery: "steer",
|
||||||
timeCreated: 1_717_171_717_000,
|
timeCreated: 1_717_171_717_000,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const syntheticAdmission = {
|
||||||
|
data: {
|
||||||
|
admittedSeq: 1,
|
||||||
|
id: "msg_synthetic",
|
||||||
|
sessionID: "ses_test",
|
||||||
|
type: "synthetic",
|
||||||
|
data: { text: "Completed" },
|
||||||
|
delivery: "queue",
|
||||||
|
timeCreated: 1_717_171_717_000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
const compactionAdmission = {
|
const compactionAdmission = {
|
||||||
data: {
|
data: {
|
||||||
type: "compaction",
|
type: "compaction",
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
{
|
{
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "sqlite",
|
"dialect": "sqlite",
|
||||||
"id": "b0355fd9-bf41-42e3-9dca-76107de27ecd",
|
"id": "beb91b32-23e2-405f-99b8-1e8763db94f8",
|
||||||
"prevIds": ["95328a41-789d-44de-9643-6ac6ecd6b4ec"],
|
"prevIds": [
|
||||||
|
"00000000-0000-0000-0000-000000000000"
|
||||||
|
],
|
||||||
"ddl": [
|
"ddl": [
|
||||||
{
|
{
|
||||||
"name": "workspace",
|
"name": "workspace",
|
||||||
|
|
@ -1016,11 +1018,11 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"notNull": false,
|
"notNull": true,
|
||||||
"autoincrement": false,
|
"autoincrement": false,
|
||||||
"default": null,
|
"default": null,
|
||||||
"generated": null,
|
"generated": null,
|
||||||
"name": "prompt",
|
"name": "data",
|
||||||
"entityType": "columns",
|
"entityType": "columns",
|
||||||
"table": "session_input"
|
"table": "session_input"
|
||||||
},
|
},
|
||||||
|
|
@ -1575,9 +1577,13 @@
|
||||||
"table": "session_share"
|
"table": "session_share"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["project_id"],
|
"columns": [
|
||||||
|
"project_id"
|
||||||
|
],
|
||||||
"tableTo": "project",
|
"tableTo": "project",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1586,9 +1592,13 @@
|
||||||
"table": "workspace"
|
"table": "workspace"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["active_account_id"],
|
"columns": [
|
||||||
|
"active_account_id"
|
||||||
|
],
|
||||||
"tableTo": "account",
|
"tableTo": "account",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "SET NULL",
|
"onDelete": "SET NULL",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1597,9 +1607,13 @@
|
||||||
"table": "account_state"
|
"table": "account_state"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["aggregate_id"],
|
"columns": [
|
||||||
|
"aggregate_id"
|
||||||
|
],
|
||||||
"tableTo": "event_sequence",
|
"tableTo": "event_sequence",
|
||||||
"columnsTo": ["aggregate_id"],
|
"columnsTo": [
|
||||||
|
"aggregate_id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1608,9 +1622,13 @@
|
||||||
"table": "event"
|
"table": "event"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["project_id"],
|
"columns": [
|
||||||
|
"project_id"
|
||||||
|
],
|
||||||
"tableTo": "project",
|
"tableTo": "project",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1619,9 +1637,13 @@
|
||||||
"table": "permission"
|
"table": "permission"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["project_id"],
|
"columns": [
|
||||||
|
"project_id"
|
||||||
|
],
|
||||||
"tableTo": "project",
|
"tableTo": "project",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1630,9 +1652,13 @@
|
||||||
"table": "project_directory"
|
"table": "project_directory"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id"],
|
"columns": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1641,9 +1667,13 @@
|
||||||
"table": "instruction_checkpoint"
|
"table": "instruction_checkpoint"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id"],
|
"columns": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1652,9 +1682,13 @@
|
||||||
"table": "instruction_entry"
|
"table": "instruction_entry"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id"],
|
"columns": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1663,9 +1697,13 @@
|
||||||
"table": "message"
|
"table": "message"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["message_id"],
|
"columns": [
|
||||||
|
"message_id"
|
||||||
|
],
|
||||||
"tableTo": "message",
|
"tableTo": "message",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1674,9 +1712,13 @@
|
||||||
"table": "part"
|
"table": "part"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id"],
|
"columns": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1685,9 +1727,13 @@
|
||||||
"table": "session_input"
|
"table": "session_input"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id"],
|
"columns": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1696,9 +1742,13 @@
|
||||||
"table": "session_message"
|
"table": "session_message"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["project_id"],
|
"columns": [
|
||||||
|
"project_id"
|
||||||
|
],
|
||||||
"tableTo": "project",
|
"tableTo": "project",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1707,9 +1757,13 @@
|
||||||
"table": "session"
|
"table": "session"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id"],
|
"columns": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1718,9 +1772,13 @@
|
||||||
"table": "todo"
|
"table": "todo"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id"],
|
"columns": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1729,140 +1787,184 @@
|
||||||
"table": "session_share"
|
"table": "session_share"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["email", "url"],
|
"columns": [
|
||||||
|
"email",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "control_account_pk",
|
"name": "control_account_pk",
|
||||||
"entityType": "pks",
|
"entityType": "pks",
|
||||||
"table": "control_account"
|
"table": "control_account"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["project_id", "directory"],
|
"columns": [
|
||||||
|
"project_id",
|
||||||
|
"directory"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "project_directory_pk",
|
"name": "project_directory_pk",
|
||||||
"entityType": "pks",
|
"entityType": "pks",
|
||||||
"table": "project_directory"
|
"table": "project_directory"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id", "key"],
|
"columns": [
|
||||||
|
"session_id",
|
||||||
|
"key"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "instruction_entry_pk",
|
"name": "instruction_entry_pk",
|
||||||
"entityType": "pks",
|
"entityType": "pks",
|
||||||
"table": "instruction_entry"
|
"table": "instruction_entry"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id", "position"],
|
"columns": [
|
||||||
|
"session_id",
|
||||||
|
"position"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "todo_pk",
|
"name": "todo_pk",
|
||||||
"entityType": "pks",
|
"entityType": "pks",
|
||||||
"table": "todo"
|
"table": "todo"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "workspace_pk",
|
"name": "workspace_pk",
|
||||||
"table": "workspace",
|
"table": "workspace",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["name"],
|
"columns": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "data_migration_pk",
|
"name": "data_migration_pk",
|
||||||
"table": "data_migration",
|
"table": "data_migration",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "account_state_pk",
|
"name": "account_state_pk",
|
||||||
"table": "account_state",
|
"table": "account_state",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "account_pk",
|
"name": "account_pk",
|
||||||
"table": "account",
|
"table": "account",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "credential_pk",
|
"name": "credential_pk",
|
||||||
"table": "credential",
|
"table": "credential",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["aggregate_id"],
|
"columns": [
|
||||||
|
"aggregate_id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "event_sequence_pk",
|
"name": "event_sequence_pk",
|
||||||
"table": "event_sequence",
|
"table": "event_sequence",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "event_pk",
|
"name": "event_pk",
|
||||||
"table": "event",
|
"table": "event",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "permission_pk",
|
"name": "permission_pk",
|
||||||
"table": "permission",
|
"table": "permission",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "project_pk",
|
"name": "project_pk",
|
||||||
"table": "project",
|
"table": "project",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id"],
|
"columns": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "instruction_checkpoint_pk",
|
"name": "instruction_checkpoint_pk",
|
||||||
"table": "instruction_checkpoint",
|
"table": "instruction_checkpoint",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "message_pk",
|
"name": "message_pk",
|
||||||
"table": "message",
|
"table": "message",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "part_pk",
|
"name": "part_pk",
|
||||||
"table": "part",
|
"table": "part",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_input_pk",
|
"name": "session_input_pk",
|
||||||
"table": "session_input",
|
"table": "session_input",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_message_pk",
|
"name": "session_message_pk",
|
||||||
"table": "session_message",
|
"table": "session_message",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_pk",
|
"name": "session_pk",
|
||||||
"table": "session",
|
"table": "session",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id"],
|
"columns": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_share_pk",
|
"name": "session_share_pk",
|
||||||
"table": "session_share",
|
"table": "session_share",
|
||||||
|
|
@ -1994,10 +2096,6 @@
|
||||||
"value": "promoted_seq",
|
"value": "promoted_seq",
|
||||||
"isExpression": false
|
"isExpression": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"value": "type",
|
|
||||||
"isExpression": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"value": "delivery",
|
"value": "delivery",
|
||||||
"isExpression": false
|
"isExpression": false
|
||||||
|
|
@ -2010,7 +2108,7 @@
|
||||||
"isUnique": false,
|
"isUnique": false,
|
||||||
"where": null,
|
"where": null,
|
||||||
"origin": "manual",
|
"origin": "manual",
|
||||||
"name": "session_input_session_pending_type_delivery_seq_idx",
|
"name": "session_input_session_pending_delivery_seq_idx",
|
||||||
"entityType": "indexes",
|
"entityType": "indexes",
|
||||||
"table": "session_input"
|
"table": "session_input"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
1
packages/core/src/database/migration.gen.ts
generated
1
packages/core/src/database/migration.gen.ts
generated
|
|
@ -49,5 +49,6 @@ export const migrations = (
|
||||||
import("./migration/20260706223930_add-session-fork"),
|
import("./migration/20260706223930_add-session-fork"),
|
||||||
import("./migration/20260707010146_durable_session_inbox"),
|
import("./migration/20260707010146_durable_session_inbox"),
|
||||||
import("./migration/20260707120000_migrate_prelaunch_v2_state"),
|
import("./migration/20260707120000_migrate_prelaunch_v2_state"),
|
||||||
|
import("./migration/20260709013000_generic_session_input"),
|
||||||
])
|
])
|
||||||
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { Effect } from "effect"
|
||||||
|
import type { DatabaseMigration } from "../migration"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
id: "20260709013000_generic_session_input",
|
||||||
|
up(tx) {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
yield* tx.run(`PRAGMA foreign_keys=OFF;`)
|
||||||
|
yield* tx.run(`
|
||||||
|
DELETE FROM \`event\`
|
||||||
|
WHERE \`type\` IN ('session.prompt.admitted.1', 'session.prompt.promoted.1')
|
||||||
|
AND json_extract(\`data\`, '$.inputID') IN (
|
||||||
|
SELECT \`id\` FROM \`session_input\` WHERE \`type\` = 'prompt' AND \`prompt\` IS NULL
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
yield* tx.run(`
|
||||||
|
CREATE TABLE \`__new_session_input\` (
|
||||||
|
\`id\` text PRIMARY KEY,
|
||||||
|
\`session_id\` text NOT NULL,
|
||||||
|
\`type\` text NOT NULL,
|
||||||
|
\`data\` text NOT NULL,
|
||||||
|
\`delivery\` text,
|
||||||
|
\`admitted_seq\` integer NOT NULL,
|
||||||
|
\`promoted_seq\` integer,
|
||||||
|
\`time_created\` integer NOT NULL,
|
||||||
|
CONSTRAINT \`fk_session_input_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
yield* tx.run(`
|
||||||
|
INSERT INTO \`__new_session_input\`(
|
||||||
|
\`id\`, \`session_id\`, \`type\`, \`data\`, \`delivery\`, \`admitted_seq\`, \`promoted_seq\`, \`time_created\`
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
\`id\`, \`session_id\`, CASE WHEN \`type\` = 'prompt' THEN 'user' ELSE \`type\` END,
|
||||||
|
CASE WHEN \`type\` = 'prompt' THEN \`prompt\` ELSE '{}' END,
|
||||||
|
\`delivery\`, \`admitted_seq\`, \`promoted_seq\`, \`time_created\`
|
||||||
|
FROM \`session_input\`
|
||||||
|
WHERE \`type\` != 'prompt' OR \`prompt\` IS NOT NULL;
|
||||||
|
`)
|
||||||
|
yield* tx.run(`DROP TABLE \`session_input\`;`)
|
||||||
|
yield* tx.run(`ALTER TABLE \`__new_session_input\` RENAME TO \`session_input\`;`)
|
||||||
|
yield* tx.run(`PRAGMA foreign_keys=ON;`)
|
||||||
|
yield* tx.run(
|
||||||
|
`CREATE INDEX \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`admitted_seq\`);`,
|
||||||
|
)
|
||||||
|
yield* tx.run(
|
||||||
|
`CREATE UNIQUE INDEX \`session_input_session_pending_compaction_idx\` ON \`session_input\` (\`session_id\`) WHERE \`type\` = 'compaction' and \`promoted_seq\` is null;`,
|
||||||
|
)
|
||||||
|
yield* tx.run(
|
||||||
|
`CREATE UNIQUE INDEX \`session_input_session_admitted_seq_idx\` ON \`session_input\` (\`session_id\`,\`admitted_seq\`);`,
|
||||||
|
)
|
||||||
|
yield* tx.run(
|
||||||
|
`CREATE UNIQUE INDEX \`session_input_session_promoted_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`);`,
|
||||||
|
)
|
||||||
|
yield* tx.run(`
|
||||||
|
UPDATE \`event\`
|
||||||
|
SET
|
||||||
|
\`type\` = 'session.input.admitted.1',
|
||||||
|
\`data\` = json_object(
|
||||||
|
'sessionID', json_extract(\`data\`, '$.sessionID'),
|
||||||
|
'inputID', json_extract(\`data\`, '$.inputID'),
|
||||||
|
'input', json_object(
|
||||||
|
'type', 'user',
|
||||||
|
'data', json_extract(\`data\`, '$.prompt'),
|
||||||
|
'delivery', json_extract(\`data\`, '$.delivery')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
WHERE \`type\` = 'session.prompt.admitted.1';
|
||||||
|
`)
|
||||||
|
yield* tx.run(`
|
||||||
|
UPDATE \`event\`
|
||||||
|
SET \`type\` = 'session.input.promoted.1'
|
||||||
|
WHERE \`type\` = 'session.prompt.promoted.1';
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
} satisfies DatabaseMigration.Migration
|
||||||
|
|
@ -171,7 +171,7 @@ export default {
|
||||||
\`id\` text PRIMARY KEY,
|
\`id\` text PRIMARY KEY,
|
||||||
\`session_id\` text NOT NULL,
|
\`session_id\` text NOT NULL,
|
||||||
\`type\` text NOT NULL,
|
\`type\` text NOT NULL,
|
||||||
\`prompt\` text,
|
\`data\` text NOT NULL,
|
||||||
\`delivery\` text,
|
\`delivery\` text,
|
||||||
\`admitted_seq\` integer NOT NULL,
|
\`admitted_seq\` integer NOT NULL,
|
||||||
\`promoted_seq\` integer,
|
\`promoted_seq\` integer,
|
||||||
|
|
@ -262,7 +262,7 @@ export default {
|
||||||
yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`)
|
yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`)
|
||||||
yield* tx.run(`CREATE INDEX \`part_session_idx\` ON \`part\` (\`session_id\`);`)
|
yield* tx.run(`CREATE INDEX \`part_session_idx\` ON \`part\` (\`session_id\`);`)
|
||||||
yield* tx.run(
|
yield* tx.run(
|
||||||
`CREATE INDEX \`session_input_session_pending_type_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`type\`,\`delivery\`,\`admitted_seq\`);`,
|
`CREATE INDEX \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`admitted_seq\`);`,
|
||||||
)
|
)
|
||||||
yield* tx.run(
|
yield* tx.run(
|
||||||
`CREATE UNIQUE INDEX \`session_input_session_pending_compaction_idx\` ON \`session_input\` (\`session_id\`) WHERE "session_input"."type" = 'compaction' and "session_input"."promoted_seq" is null;`,
|
`CREATE UNIQUE INDEX \`session_input_session_pending_compaction_idx\` ON \`session_input\` (\`session_id\`) WHERE "session_input"."type" = 'compaction' and "session_input"."promoted_seq" is null;`,
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,13 @@ export class PromptConflictError extends Schema.TaggedErrorClass<PromptConflictE
|
||||||
sessionID: SessionSchema.ID,
|
sessionID: SessionSchema.ID,
|
||||||
messageID: SessionMessage.ID,
|
messageID: SessionMessage.ID,
|
||||||
}) {}
|
}) {}
|
||||||
|
export class SyntheticConflictError extends Schema.TaggedErrorClass<SyntheticConflictError>()(
|
||||||
|
"Session.SyntheticConflictError",
|
||||||
|
{
|
||||||
|
sessionID: SessionSchema.ID,
|
||||||
|
inputID: SessionMessage.ID,
|
||||||
|
},
|
||||||
|
) {}
|
||||||
export class AttachmentError extends Schema.TaggedErrorClass<AttachmentError>()("Session.AttachmentError", {
|
export class AttachmentError extends Schema.TaggedErrorClass<AttachmentError>()("Session.AttachmentError", {
|
||||||
uri: Schema.String,
|
uri: Schema.String,
|
||||||
message: Schema.String,
|
message: Schema.String,
|
||||||
|
|
@ -147,6 +154,7 @@ export type Error =
|
||||||
| MessageDecodeError
|
| MessageDecodeError
|
||||||
| OperationUnavailableError
|
| OperationUnavailableError
|
||||||
| PromptConflictError
|
| PromptConflictError
|
||||||
|
| SyntheticConflictError
|
||||||
| AttachmentError
|
| AttachmentError
|
||||||
| CompactionConflictError
|
| CompactionConflictError
|
||||||
| BusyError
|
| BusyError
|
||||||
|
|
@ -204,10 +212,13 @@ export interface Interface {
|
||||||
readonly prompt: (input: {
|
readonly prompt: (input: {
|
||||||
id?: SessionMessage.ID
|
id?: SessionMessage.ID
|
||||||
sessionID: SessionSchema.ID
|
sessionID: SessionSchema.ID
|
||||||
prompt: PromptInput.Prompt
|
text: string
|
||||||
|
files?: PromptInput.Prompt["files"]
|
||||||
|
agents?: PromptInput.Prompt["agents"]
|
||||||
|
metadata?: Record<string, unknown>
|
||||||
delivery?: SessionInput.Delivery
|
delivery?: SessionInput.Delivery
|
||||||
resume?: boolean
|
resume?: boolean
|
||||||
}) => Effect.Effect<SessionInput.Admitted, NotFoundError | PromptConflictError | AttachmentError>
|
}) => Effect.Effect<SessionInput.User, NotFoundError | PromptConflictError | AttachmentError>
|
||||||
readonly command: (input: {
|
readonly command: (input: {
|
||||||
id?: SessionMessage.ID
|
id?: SessionMessage.ID
|
||||||
sessionID: SessionSchema.ID
|
sessionID: SessionSchema.ID
|
||||||
|
|
@ -220,7 +231,7 @@ export interface Interface {
|
||||||
delivery?: SessionInput.Delivery
|
delivery?: SessionInput.Delivery
|
||||||
resume?: boolean
|
resume?: boolean
|
||||||
}) => Effect.Effect<
|
}) => Effect.Effect<
|
||||||
SessionInput.Admitted,
|
SessionInput.User,
|
||||||
NotFoundError | PromptConflictError | AttachmentError | CommandV2.NotFoundError | CommandV2.EvaluationError
|
NotFoundError | PromptConflictError | AttachmentError | CommandV2.NotFoundError | CommandV2.EvaluationError
|
||||||
>
|
>
|
||||||
readonly shell: (input: {
|
readonly shell: (input: {
|
||||||
|
|
@ -243,12 +254,14 @@ export interface Interface {
|
||||||
readonly resume: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | SessionRunner.RunError>
|
readonly resume: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | SessionRunner.RunError>
|
||||||
readonly interrupt: (sessionID: SessionSchema.ID) => Effect.Effect<void>
|
readonly interrupt: (sessionID: SessionSchema.ID) => Effect.Effect<void>
|
||||||
readonly synthetic: (input: {
|
readonly synthetic: (input: {
|
||||||
|
id?: SessionMessage.ID
|
||||||
sessionID: SessionSchema.ID
|
sessionID: SessionSchema.ID
|
||||||
text: string
|
text: string
|
||||||
description?: string
|
description?: string
|
||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
|
delivery?: SessionInput.Delivery
|
||||||
resume?: boolean
|
resume?: boolean
|
||||||
}) => Effect.Effect<void, NotFoundError>
|
}) => Effect.Effect<SessionInput.Synthetic, NotFoundError | SyntheticConflictError>
|
||||||
readonly revert: {
|
readonly revert: {
|
||||||
readonly stage: (input: {
|
readonly stage: (input: {
|
||||||
sessionID: SessionSchema.ID
|
sessionID: SessionSchema.ID
|
||||||
|
|
@ -487,15 +500,19 @@ const layer = Layer.effect(
|
||||||
// continues from the reverted boundary rather than stale post-boundary history.
|
// continues from the reverted boundary rather than stale post-boundary history.
|
||||||
if (session.revert)
|
if (session.revert)
|
||||||
yield* SessionRevert.commit(session).pipe(Effect.provideService(EventV2.Service, events))
|
yield* SessionRevert.commit(session).pipe(Effect.provideService(EventV2.Service, events))
|
||||||
const prompt = yield* resolvePrompt(input.prompt).pipe(Effect.provideService(FSUtil.Service, fs))
|
const prompt = yield* resolvePrompt({ text: input.text, files: input.files, agents: input.agents }).pipe(
|
||||||
|
Effect.provideService(FSUtil.Service, fs),
|
||||||
|
)
|
||||||
const messageID = input.id ?? SessionMessage.ID.create()
|
const messageID = input.id ?? SessionMessage.ID.create()
|
||||||
const delivery = input.delivery ?? "steer"
|
const admittedInput = SessionInput.Message.make({
|
||||||
const expected = { sessionID: input.sessionID, messageID, prompt, delivery }
|
type: "user",
|
||||||
|
data: { ...prompt, metadata: input.metadata },
|
||||||
|
delivery: input.delivery ?? "steer",
|
||||||
|
})
|
||||||
const admitted = yield* SessionInput.admit(db, events, {
|
const admitted = yield* SessionInput.admit(db, events, {
|
||||||
id: messageID,
|
id: messageID,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
prompt,
|
input: admittedInput,
|
||||||
delivery,
|
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.catchDefect((defect) =>
|
Effect.catchDefect((defect) =>
|
||||||
defect instanceof SessionInput.LifecycleConflict
|
defect instanceof SessionInput.LifecycleConflict
|
||||||
|
|
@ -503,7 +520,10 @@ const layer = Layer.effect(
|
||||||
: Effect.die(defect),
|
: Effect.die(defect),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if (!SessionInput.equivalent(admitted, expected))
|
if (
|
||||||
|
admitted.type !== "user" ||
|
||||||
|
!SessionInput.equivalent(admitted, { sessionID: input.sessionID, input: admittedInput })
|
||||||
|
)
|
||||||
return yield* new PromptConflictError({ sessionID: input.sessionID, messageID })
|
return yield* new PromptConflictError({ sessionID: input.sessionID, messageID })
|
||||||
if (input.resume !== false) {
|
if (input.resume !== false) {
|
||||||
if (activeShells.has(admitted.sessionID)) return admitted
|
if (activeShells.has(admitted.sessionID)) return admitted
|
||||||
|
|
@ -539,7 +559,9 @@ const layer = Layer.effect(
|
||||||
return yield* result.prompt({
|
return yield* result.prompt({
|
||||||
id: input.id,
|
id: input.id,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
prompt: { text: evaluated.text, files: input.files, agents: input.agents },
|
text: evaluated.text,
|
||||||
|
files: input.files,
|
||||||
|
agents: input.agents,
|
||||||
delivery: input.delivery,
|
delivery: input.delivery,
|
||||||
resume: input.resume,
|
resume: input.resume,
|
||||||
})
|
})
|
||||||
|
|
@ -664,35 +686,60 @@ const layer = Layer.effect(
|
||||||
yield* result.get(sessionID)
|
yield* result.get(sessionID)
|
||||||
const backgrounded = yield* jobs.backgroundAll({ sessionID })
|
const backgrounded = yield* jobs.backgroundAll({ sessionID })
|
||||||
if (backgrounded.length === 0) return
|
if (backgrounded.length === 0) return
|
||||||
yield* result.synthetic({
|
yield* result
|
||||||
sessionID,
|
.synthetic({
|
||||||
text: [
|
sessionID,
|
||||||
"User requested that active blocking work be moved to the background.",
|
text: [
|
||||||
"",
|
"User requested that active blocking work be moved to the background.",
|
||||||
"Backgrounded work:",
|
"",
|
||||||
...backgrounded.map((job) => `- ${job.type}: ${job.title && job.title.length > 0 ? job.title : job.id}`),
|
"Backgrounded work:",
|
||||||
"",
|
...backgrounded.map((job) => `- ${job.type}: ${job.title && job.title.length > 0 ? job.title : job.id}`),
|
||||||
"The backgrounded work is still unfinished. Move on to other work if you can. If there is nothing else useful to do, finish your response. Do not wait, sleep, poll, or report the backgrounded work as complete until a later completion notification is added to the conversation.",
|
"",
|
||||||
].join("\n"),
|
"The backgrounded work is still unfinished. Move on to other work if you can. If there is nothing else useful to do, finish your response. Do not wait, sleep, poll, or report the backgrounded work as complete until a later completion notification is added to the conversation.",
|
||||||
})
|
].join("\n"),
|
||||||
|
})
|
||||||
|
.pipe(Effect.catchTag("Session.SyntheticConflictError", Effect.die))
|
||||||
}),
|
}),
|
||||||
resume: Effect.fn("V2Session.resume")(function* (sessionID) {
|
resume: Effect.fn("V2Session.resume")(function* (sessionID) {
|
||||||
yield* result.get(sessionID)
|
yield* result.get(sessionID)
|
||||||
yield* execution.resume(sessionID)
|
yield* execution.resume(sessionID)
|
||||||
}),
|
}),
|
||||||
synthetic: Effect.fn("V2Session.synthetic")(function* (input) {
|
synthetic: Effect.fn("V2Session.synthetic")((input) =>
|
||||||
yield* result.get(input.sessionID)
|
Effect.uninterruptible(
|
||||||
yield* events.publish(SessionEvent.Synthetic, {
|
Effect.gen(function* () {
|
||||||
sessionID: input.sessionID,
|
yield* result.get(input.sessionID)
|
||||||
text: input.text,
|
const inputID = input.id ?? SessionMessage.ID.create()
|
||||||
description: input.description,
|
const admittedInput = SessionInput.Message.make({
|
||||||
metadata: input.metadata,
|
type: "synthetic",
|
||||||
})
|
data: {
|
||||||
if (input.resume === false) return
|
text: input.text,
|
||||||
yield* execution
|
description: input.description,
|
||||||
.resume(input.sessionID)
|
metadata: input.metadata,
|
||||||
.pipe(Effect.ignore, Effect.forkIn(scope, { startImmediately: true }), Effect.asVoid)
|
},
|
||||||
}),
|
delivery: input.delivery ?? "steer",
|
||||||
|
})
|
||||||
|
const admitted = yield* SessionInput.admit(db, events, {
|
||||||
|
id: inputID,
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
input: admittedInput,
|
||||||
|
}).pipe(
|
||||||
|
Effect.catchDefect((defect) =>
|
||||||
|
defect instanceof SessionInput.LifecycleConflict
|
||||||
|
? new SyntheticConflictError({ sessionID: input.sessionID, inputID })
|
||||||
|
: Effect.die(defect),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
admitted.type !== "synthetic" ||
|
||||||
|
!SessionInput.equivalent(admitted, { sessionID: input.sessionID, input: admittedInput })
|
||||||
|
)
|
||||||
|
return yield* new SyntheticConflictError({ sessionID: input.sessionID, inputID })
|
||||||
|
if (input.resume !== false && !(yield* result.get(input.sessionID)).revert)
|
||||||
|
yield* execution.wake(input.sessionID)
|
||||||
|
return admitted
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
interrupt: Effect.fn("V2Session.interrupt")((sessionID) =>
|
interrupt: Effect.fn("V2Session.interrupt")((sessionID) =>
|
||||||
Effect.uninterruptible(execution.interrupt(sessionID)),
|
Effect.uninterruptible(execution.interrupt(sessionID)),
|
||||||
),
|
),
|
||||||
|
|
@ -710,10 +757,12 @@ const layer = Layer.effect(
|
||||||
clear: Effect.fn("V2Session.revert.clear")(function* (sessionID) {
|
clear: Effect.fn("V2Session.revert.clear")(function* (sessionID) {
|
||||||
const session = yield* result.get(sessionID)
|
const session = yield* result.get(sessionID)
|
||||||
if ((yield* execution.active).has(sessionID)) return yield* new BusyError({ sessionID })
|
if ((yield* execution.active).has(sessionID)) return yield* new BusyError({ sessionID })
|
||||||
return yield* SessionRevert.clear(session).pipe(
|
const revert = yield* SessionRevert.clear(session).pipe(
|
||||||
Effect.provideService(EventV2.Service, events),
|
Effect.provideService(EventV2.Service, events),
|
||||||
Effect.provide(locations.get(session.location)),
|
Effect.provide(locations.get(session.location)),
|
||||||
)
|
)
|
||||||
|
yield* execution.wake(sessionID)
|
||||||
|
return revert
|
||||||
}),
|
}),
|
||||||
commit: Effect.fn("V2Session.revert.commit")(function* (sessionID) {
|
commit: Effect.fn("V2Session.revert.commit")(function* (sessionID) {
|
||||||
const session = yield* result.get(sessionID)
|
const session = yield* result.get(sessionID)
|
||||||
|
|
|
||||||
|
|
@ -2,22 +2,32 @@ export * as SessionInput from "./input"
|
||||||
|
|
||||||
import { and, asc, eq, isNull } from "drizzle-orm"
|
import { and, asc, eq, isNull } from "drizzle-orm"
|
||||||
import { DateTime, Effect, Schema } from "effect"
|
import { DateTime, Effect, Schema } from "effect"
|
||||||
import { Admitted, Compaction, Delivery, Info, PromptEntry } from "@opencode-ai/schema/session-input"
|
import {
|
||||||
|
Compaction,
|
||||||
|
Delivery,
|
||||||
|
Info,
|
||||||
|
Message,
|
||||||
|
Synthetic,
|
||||||
|
SyntheticData,
|
||||||
|
User,
|
||||||
|
UserData,
|
||||||
|
} from "@opencode-ai/schema/session-input"
|
||||||
import type { Database } from "../database/database"
|
import type { Database } from "../database/database"
|
||||||
import type { EventV2 } from "../event"
|
import type { EventV2 } from "../event"
|
||||||
import { KeyedMutex } from "../effect/keyed-mutex"
|
import { KeyedMutex } from "../effect/keyed-mutex"
|
||||||
import { SessionEvent } from "./event"
|
import { SessionEvent } from "./event"
|
||||||
import { SessionMessage } from "./message"
|
import { SessionMessage } from "./message"
|
||||||
import { Prompt } from "@opencode-ai/schema/prompt"
|
|
||||||
import { SessionSchema } from "./schema"
|
import { SessionSchema } from "./schema"
|
||||||
import { SessionInputTable, SessionMessageTable } from "./sql"
|
import { SessionInputTable, SessionMessageTable } from "./sql"
|
||||||
|
|
||||||
type DatabaseService = Database.Interface["db"]
|
type DatabaseService = Database.Interface["db"]
|
||||||
|
|
||||||
export { Admitted, Compaction, Delivery, Info, PromptEntry }
|
export { Compaction, Delivery, Info, Message, Synthetic, SyntheticData, User, UserData }
|
||||||
|
|
||||||
const decodePrompt = Schema.decodeUnknownSync(Prompt)
|
const decodeUser = Schema.decodeUnknownSync(UserData)
|
||||||
const encodePrompt = Schema.encodeSync(Prompt)
|
const encodeUser = Schema.encodeSync(UserData)
|
||||||
|
const decodeSynthetic = Schema.decodeUnknownSync(SyntheticData)
|
||||||
|
const encodeSynthetic = Schema.encodeSync(SyntheticData)
|
||||||
const inboxLocks = KeyedMutex.makeUnsafe<SessionSchema.ID>()
|
const inboxLocks = KeyedMutex.makeUnsafe<SessionSchema.ID>()
|
||||||
|
|
||||||
export class LifecycleConflict extends Schema.TaggedErrorClass<LifecycleConflict>()("SessionInput.LifecycleConflict", {
|
export class LifecycleConflict extends Schema.TaggedErrorClass<LifecycleConflict>()("SessionInput.LifecycleConflict", {
|
||||||
|
|
@ -37,27 +47,26 @@ const fromRow = (row: typeof SessionInputTable.$inferSelect): Info => {
|
||||||
type: "compaction",
|
type: "compaction",
|
||||||
...(row.promoted_seq === null ? {} : { handledSeq: row.promoted_seq }),
|
...(row.promoted_seq === null ? {} : { handledSeq: row.promoted_seq }),
|
||||||
})
|
})
|
||||||
if (!row.prompt || !row.delivery) throw new LifecycleConflict({ id: base.id })
|
if (!row.delivery) throw new LifecycleConflict({ id: base.id })
|
||||||
return PromptEntry.make({
|
if (row.type === "user")
|
||||||
...base,
|
return User.make({
|
||||||
type: "prompt",
|
...base,
|
||||||
prompt: decodePrompt(row.prompt),
|
type: "user",
|
||||||
delivery: row.delivery,
|
data: decodeUser(row.data),
|
||||||
...(row.promoted_seq === null ? {} : { promotedSeq: row.promoted_seq }),
|
delivery: row.delivery,
|
||||||
})
|
...(row.promoted_seq === null ? {} : { promotedSeq: row.promoted_seq }),
|
||||||
|
})
|
||||||
|
if (row.type === "synthetic")
|
||||||
|
return Synthetic.make({
|
||||||
|
...base,
|
||||||
|
type: "synthetic",
|
||||||
|
data: decodeSynthetic(row.data),
|
||||||
|
delivery: row.delivery,
|
||||||
|
...(row.promoted_seq === null ? {} : { promotedSeq: row.promoted_seq }),
|
||||||
|
})
|
||||||
|
throw new LifecycleConflict({ id: base.id })
|
||||||
}
|
}
|
||||||
|
|
||||||
const toAdmitted = (entry: PromptEntry): Admitted =>
|
|
||||||
Admitted.make({
|
|
||||||
admittedSeq: entry.admittedSeq,
|
|
||||||
id: entry.id,
|
|
||||||
sessionID: entry.sessionID,
|
|
||||||
prompt: entry.prompt,
|
|
||||||
delivery: entry.delivery,
|
|
||||||
timeCreated: entry.timeCreated,
|
|
||||||
...(entry.promotedSeq === undefined ? {} : { promotedSeq: entry.promotedSeq }),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const find = Effect.fn("SessionInput.find")(function* (db: DatabaseService, id: SessionMessage.ID) {
|
export const find = Effect.fn("SessionInput.find")(function* (db: DatabaseService, id: SessionMessage.ID) {
|
||||||
const row = yield* db.select().from(SessionInputTable).where(eq(SessionInputTable.id, id)).get().pipe(Effect.orDie)
|
const row = yield* db.select().from(SessionInputTable).where(eq(SessionInputTable.id, id)).get().pipe(Effect.orDie)
|
||||||
return row === undefined ? undefined : fromRow(row)
|
return row === undefined ? undefined : fromRow(row)
|
||||||
|
|
@ -89,44 +98,43 @@ export const pendingCompaction = Effect.fn("SessionInput.pendingCompaction")(fun
|
||||||
export const admit = Effect.fn("SessionInput.admit")(function* (
|
export const admit = Effect.fn("SessionInput.admit")(function* (
|
||||||
db: DatabaseService,
|
db: DatabaseService,
|
||||||
events: EventV2.Interface,
|
events: EventV2.Interface,
|
||||||
input: {
|
request: {
|
||||||
readonly id: SessionMessage.ID
|
readonly id: SessionMessage.ID
|
||||||
readonly sessionID: SessionSchema.ID
|
readonly sessionID: SessionSchema.ID
|
||||||
readonly prompt: Prompt
|
readonly input: Message
|
||||||
readonly delivery: Delivery
|
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const existing = yield* find(db, input.id)
|
const existing = yield* find(db, request.id)
|
||||||
if (existing !== undefined) {
|
if (existing !== undefined) {
|
||||||
if (existing.type !== "prompt") return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
if (existing.type === "compaction") return yield* Effect.die(new LifecycleConflict({ id: request.id }))
|
||||||
return toAdmitted(existing)
|
return existing
|
||||||
}
|
}
|
||||||
return yield* events
|
return yield* events
|
||||||
.publish(SessionEvent.PromptAdmitted, {
|
.publish(SessionEvent.InputAdmitted, {
|
||||||
inputID: input.id,
|
inputID: request.id,
|
||||||
sessionID: input.sessionID,
|
sessionID: request.sessionID,
|
||||||
prompt: input.prompt,
|
input: request.input,
|
||||||
delivery: input.delivery,
|
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
Effect.flatMap((event) =>
|
Effect.flatMap((event) => {
|
||||||
event.durable === undefined
|
if (event.durable === undefined)
|
||||||
? Effect.die(new Error("Prompt admission event is missing aggregate sequence"))
|
return Effect.die(new Error("Session input admission event is missing aggregate sequence"))
|
||||||
: Effect.succeed(
|
const base = {
|
||||||
Admitted.make({
|
admittedSeq: event.durable.seq,
|
||||||
admittedSeq: event.durable.seq,
|
id: request.id,
|
||||||
id: input.id,
|
sessionID: request.sessionID,
|
||||||
sessionID: input.sessionID,
|
timeCreated: event.created,
|
||||||
prompt: input.prompt,
|
}
|
||||||
delivery: input.delivery,
|
return Effect.succeed(
|
||||||
timeCreated: event.created,
|
request.input.type === "user"
|
||||||
}),
|
? User.make({ ...base, ...request.input })
|
||||||
),
|
: Synthetic.make({ ...base, ...request.input }),
|
||||||
),
|
)
|
||||||
|
}),
|
||||||
Effect.catchDefect((defect) =>
|
Effect.catchDefect((defect) =>
|
||||||
find(db, input.id).pipe(
|
find(db, request.id).pipe(
|
||||||
Effect.flatMap((stored) =>
|
Effect.flatMap((stored) =>
|
||||||
stored?.type === "prompt" ? Effect.succeed(toAdmitted(stored)) : Effect.die(defect),
|
stored?.type === request.input.type ? Effect.succeed(stored) : Effect.die(defect),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -174,38 +182,37 @@ export const admitCompaction = Effect.fn("SessionInput.admitCompaction")(functio
|
||||||
|
|
||||||
export const projectAdmitted = Effect.fn("SessionInput.projectAdmitted")(function* (
|
export const projectAdmitted = Effect.fn("SessionInput.projectAdmitted")(function* (
|
||||||
db: DatabaseService,
|
db: DatabaseService,
|
||||||
input: {
|
request: {
|
||||||
readonly admittedSeq: number
|
readonly admittedSeq: number
|
||||||
readonly id: SessionMessage.ID
|
readonly id: SessionMessage.ID
|
||||||
readonly sessionID: SessionSchema.ID
|
readonly sessionID: SessionSchema.ID
|
||||||
readonly prompt: Prompt
|
readonly input: Message
|
||||||
readonly delivery: Delivery
|
|
||||||
readonly timeCreated: DateTime.Utc
|
readonly timeCreated: DateTime.Utc
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const message = yield* db
|
const message = yield* db
|
||||||
.select({ id: SessionMessageTable.id })
|
.select({ id: SessionMessageTable.id })
|
||||||
.from(SessionMessageTable)
|
.from(SessionMessageTable)
|
||||||
.where(eq(SessionMessageTable.id, input.id))
|
.where(eq(SessionMessageTable.id, request.id))
|
||||||
.get()
|
.get()
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
if (message !== undefined) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
if (message !== undefined) return yield* Effect.die(new LifecycleConflict({ id: request.id }))
|
||||||
const stored = yield* db
|
const stored = yield* db
|
||||||
.insert(SessionInputTable)
|
.insert(SessionInputTable)
|
||||||
.values({
|
.values({
|
||||||
id: input.id,
|
id: request.id,
|
||||||
session_id: input.sessionID,
|
session_id: request.sessionID,
|
||||||
type: "prompt",
|
type: request.input.type,
|
||||||
admitted_seq: input.admittedSeq,
|
data: request.input.type === "user" ? encodeUser(request.input.data) : encodeSynthetic(request.input.data),
|
||||||
prompt: encodePrompt(input.prompt),
|
delivery: request.input.delivery,
|
||||||
delivery: input.delivery,
|
admitted_seq: request.admittedSeq,
|
||||||
time_created: DateTime.toEpochMillis(input.timeCreated),
|
time_created: DateTime.toEpochMillis(request.timeCreated),
|
||||||
})
|
})
|
||||||
.onConflictDoNothing()
|
.onConflictDoNothing()
|
||||||
.returning({ id: SessionInputTable.id })
|
.returning({ id: SessionInputTable.id })
|
||||||
.get()
|
.get()
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
if (!stored) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
if (!stored) return yield* Effect.die(new LifecycleConflict({ id: request.id }))
|
||||||
})
|
})
|
||||||
|
|
||||||
export const projectCompactionAdmitted = Effect.fn("SessionInput.projectCompactionAdmitted")(function* (
|
export const projectCompactionAdmitted = Effect.fn("SessionInput.projectCompactionAdmitted")(function* (
|
||||||
|
|
@ -230,6 +237,7 @@ export const projectCompactionAdmitted = Effect.fn("SessionInput.projectCompacti
|
||||||
id: input.id,
|
id: input.id,
|
||||||
session_id: input.sessionID,
|
session_id: input.sessionID,
|
||||||
type: "compaction",
|
type: "compaction",
|
||||||
|
data: {},
|
||||||
admitted_seq: input.admittedSeq,
|
admitted_seq: input.admittedSeq,
|
||||||
time_created: DateTime.toEpochMillis(input.timeCreated),
|
time_created: DateTime.toEpochMillis(input.timeCreated),
|
||||||
})
|
})
|
||||||
|
|
@ -246,7 +254,7 @@ export const projectCompactionAdmitted = Effect.fn("SessionInput.projectCompacti
|
||||||
return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||||
})
|
})
|
||||||
|
|
||||||
export const projectPromptPromoted = Effect.fn("SessionInput.projectPromptPromoted")(function* (
|
export const projectPromoted = Effect.fn("SessionInput.projectPromoted")(function* (
|
||||||
db: DatabaseService,
|
db: DatabaseService,
|
||||||
input: {
|
input: {
|
||||||
readonly id: SessionMessage.ID
|
readonly id: SessionMessage.ID
|
||||||
|
|
@ -262,23 +270,16 @@ export const projectPromptPromoted = Effect.fn("SessionInput.projectPromptPromot
|
||||||
and(
|
and(
|
||||||
eq(SessionInputTable.id, input.id),
|
eq(SessionInputTable.id, input.id),
|
||||||
eq(SessionInputTable.session_id, input.sessionID),
|
eq(SessionInputTable.session_id, input.sessionID),
|
||||||
eq(SessionInputTable.type, "prompt"),
|
|
||||||
isNull(SessionInputTable.promoted_seq),
|
isNull(SessionInputTable.promoted_seq),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.returning()
|
.returning()
|
||||||
.get()
|
.get()
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
if (updated) {
|
const stored = updated ? fromRow(updated) : yield* find(db, input.id)
|
||||||
const stored = fromRow(updated)
|
|
||||||
if (stored.type !== "prompt" || stored.sessionID !== input.sessionID)
|
|
||||||
return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
|
||||||
return stored
|
|
||||||
}
|
|
||||||
const stored = yield* find(db, input.id)
|
|
||||||
if (
|
if (
|
||||||
!stored ||
|
!stored ||
|
||||||
stored.type !== "prompt" ||
|
stored.type === "compaction" ||
|
||||||
stored.sessionID !== input.sessionID ||
|
stored.sessionID !== input.sessionID ||
|
||||||
stored.promotedSeq !== input.promotedSeq
|
stored.promotedSeq !== input.promotedSeq
|
||||||
)
|
)
|
||||||
|
|
@ -322,7 +323,6 @@ export const hasPending = Effect.fn("SessionInput.hasPending")(function* (
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(SessionInputTable.session_id, sessionID),
|
eq(SessionInputTable.session_id, sessionID),
|
||||||
eq(SessionInputTable.type, "prompt"),
|
|
||||||
isNull(SessionInputTable.promoted_seq),
|
isNull(SessionInputTable.promoted_seq),
|
||||||
eq(SessionInputTable.delivery, delivery),
|
eq(SessionInputTable.delivery, delivery),
|
||||||
),
|
),
|
||||||
|
|
@ -334,16 +334,21 @@ export const hasPending = Effect.fn("SessionInput.hasPending")(function* (
|
||||||
})
|
})
|
||||||
|
|
||||||
export const equivalent = (
|
export const equivalent = (
|
||||||
input: Admitted,
|
input: User | Synthetic,
|
||||||
expected: {
|
expected: { readonly sessionID: SessionSchema.ID; readonly input: Message },
|
||||||
readonly sessionID: SessionSchema.ID
|
) => {
|
||||||
readonly prompt: Prompt
|
if (
|
||||||
readonly delivery: Delivery
|
input.type !== expected.input.type ||
|
||||||
},
|
input.delivery !== expected.input.delivery ||
|
||||||
) =>
|
input.sessionID !== expected.sessionID
|
||||||
input.delivery === expected.delivery &&
|
)
|
||||||
input.sessionID === expected.sessionID &&
|
return false
|
||||||
JSON.stringify(encodePrompt(input.prompt)) === JSON.stringify(encodePrompt(expected.prompt))
|
if (input.type === "user" && expected.input.type === "user")
|
||||||
|
return JSON.stringify(encodeUser(input.data)) === JSON.stringify(encodeUser(expected.input.data))
|
||||||
|
if (input.type === "synthetic" && expected.input.type === "synthetic")
|
||||||
|
return JSON.stringify(encodeSynthetic(input.data)) === JSON.stringify(encodeSynthetic(expected.input.data))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const publish = Effect.fn("SessionInput.publish")(function* (
|
const publish = Effect.fn("SessionInput.publish")(function* (
|
||||||
db: DatabaseService,
|
db: DatabaseService,
|
||||||
|
|
@ -358,9 +363,9 @@ const publish = Effect.fn("SessionInput.publish")(function* (
|
||||||
rows,
|
rows,
|
||||||
(row) => {
|
(row) => {
|
||||||
const entry = fromRow(row)
|
const entry = fromRow(row)
|
||||||
if (entry.type !== "prompt") return Effect.die(new LifecycleConflict({ id: entry.id }))
|
if (entry.type === "compaction") return Effect.die(new LifecycleConflict({ id: entry.id }))
|
||||||
return events
|
return events
|
||||||
.publish(SessionEvent.PromptPromoted, {
|
.publish(SessionEvent.InputPromoted, {
|
||||||
sessionID,
|
sessionID,
|
||||||
inputID: entry.id,
|
inputID: entry.id,
|
||||||
})
|
})
|
||||||
|
|
@ -369,7 +374,7 @@ const publish = Effect.fn("SessionInput.publish")(function* (
|
||||||
defect instanceof LifecycleConflict
|
defect instanceof LifecycleConflict
|
||||||
? find(db, entry.id).pipe(
|
? find(db, entry.id).pipe(
|
||||||
Effect.flatMap((stored) =>
|
Effect.flatMap((stored) =>
|
||||||
stored?.type === "prompt" && stored.promotedSeq !== undefined
|
stored?.type !== "compaction" && stored?.promotedSeq !== undefined
|
||||||
? Effect.void
|
? Effect.void
|
||||||
: Effect.die(defect),
|
: Effect.die(defect),
|
||||||
),
|
),
|
||||||
|
|
@ -397,7 +402,6 @@ export const promoteSteers = Effect.fn("SessionInput.promoteSteers")(function* (
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(SessionInputTable.session_id, sessionID),
|
eq(SessionInputTable.session_id, sessionID),
|
||||||
eq(SessionInputTable.type, "prompt"),
|
|
||||||
isNull(SessionInputTable.promoted_seq),
|
isNull(SessionInputTable.promoted_seq),
|
||||||
eq(SessionInputTable.delivery, "steer"),
|
eq(SessionInputTable.delivery, "steer"),
|
||||||
),
|
),
|
||||||
|
|
@ -420,7 +424,6 @@ export const promoteNextQueued = Effect.fn("SessionInput.promoteNextQueued")(fun
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(SessionInputTable.session_id, sessionID),
|
eq(SessionInputTable.session_id, sessionID),
|
||||||
eq(SessionInputTable.type, "prompt"),
|
|
||||||
isNull(SessionInputTable.promoted_seq),
|
isNull(SessionInputTable.promoted_seq),
|
||||||
eq(SessionInputTable.delivery, "queue"),
|
eq(SessionInputTable.delivery, "queue"),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -172,8 +172,8 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
||||||
"session.renamed": () => Effect.void,
|
"session.renamed": () => Effect.void,
|
||||||
"session.deleted": () => Effect.void,
|
"session.deleted": () => Effect.void,
|
||||||
"session.forked": () => Effect.void,
|
"session.forked": () => Effect.void,
|
||||||
"session.prompt.promoted": () => Effect.void,
|
"session.input.promoted": () => Effect.void,
|
||||||
"session.prompt.admitted": () => Effect.void,
|
"session.input.admitted": () => Effect.void,
|
||||||
"session.execution.started": () => Effect.void,
|
"session.execution.started": () => Effect.void,
|
||||||
"session.execution.succeeded": () => clearCurrentRetry,
|
"session.execution.succeeded": () => clearCurrentRetry,
|
||||||
"session.execution.failed": () => clearCurrentRetry,
|
"session.execution.failed": () => clearCurrentRetry,
|
||||||
|
|
|
||||||
|
|
@ -313,13 +313,13 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
|
||||||
.values(
|
.values(
|
||||||
inputRows.flatMap((row) => {
|
inputRows.flatMap((row) => {
|
||||||
const id = idMap.get(row.id)
|
const id = idMap.get(row.id)
|
||||||
return id && row.type === "prompt"
|
return id && row.type !== "compaction"
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
session_id: event.data.sessionID,
|
session_id: event.data.sessionID,
|
||||||
type: "prompt" as const,
|
type: row.type,
|
||||||
prompt: row.prompt,
|
data: row.data,
|
||||||
delivery: row.delivery,
|
delivery: row.delivery,
|
||||||
admitted_seq: row.admitted_seq,
|
admitted_seq: row.admitted_seq,
|
||||||
promoted_seq: row.promoted_seq,
|
promoted_seq: row.promoted_seq,
|
||||||
|
|
@ -629,27 +629,40 @@ const layer = Layer.effectDiscard(
|
||||||
.pipe(Effect.orDie),
|
.pipe(Effect.orDie),
|
||||||
)
|
)
|
||||||
yield* events.project(SessionEvent.Forked, (event) => projectFork(db, event))
|
yield* events.project(SessionEvent.Forked, (event) => projectFork(db, event))
|
||||||
yield* events.project(SessionEvent.PromptPromoted, (event) =>
|
yield* events.project(SessionEvent.InputPromoted, (event) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
if (event.durable === undefined)
|
if (event.durable === undefined)
|
||||||
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
||||||
const input = yield* SessionInput.projectPromptPromoted(db, {
|
const input = yield* SessionInput.projectPromoted(db, {
|
||||||
id: event.data.inputID,
|
id: event.data.inputID,
|
||||||
sessionID: event.data.sessionID,
|
sessionID: event.data.sessionID,
|
||||||
promotedSeq: event.durable.seq,
|
promotedSeq: event.durable.seq,
|
||||||
})
|
})
|
||||||
yield* insertMessage(db, event, {
|
yield* insertMessage(
|
||||||
id: input.id,
|
db,
|
||||||
type: "user",
|
event,
|
||||||
metadata: event.metadata,
|
input.type === "user"
|
||||||
text: input.prompt.text,
|
? {
|
||||||
files: input.prompt.files,
|
id: input.id,
|
||||||
agents: input.prompt.agents,
|
type: "user",
|
||||||
time: { created: event.created },
|
metadata: input.data.metadata,
|
||||||
})
|
text: input.data.text,
|
||||||
|
files: input.data.files,
|
||||||
|
agents: input.data.agents,
|
||||||
|
time: { created: event.created },
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
id: input.id,
|
||||||
|
type: "synthetic",
|
||||||
|
text: input.data.text,
|
||||||
|
description: input.data.description,
|
||||||
|
metadata: input.data.metadata,
|
||||||
|
time: { created: event.created },
|
||||||
|
},
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* events.project(SessionEvent.PromptAdmitted, (event) =>
|
yield* events.project(SessionEvent.InputAdmitted, (event) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
if (event.durable === undefined)
|
if (event.durable === undefined)
|
||||||
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
||||||
|
|
@ -657,8 +670,7 @@ const layer = Layer.effectDiscard(
|
||||||
admittedSeq: event.durable.seq,
|
admittedSeq: event.durable.seq,
|
||||||
id: event.data.inputID,
|
id: event.data.inputID,
|
||||||
sessionID: event.data.sessionID,
|
sessionID: event.data.sessionID,
|
||||||
prompt: event.data.prompt,
|
input: event.data.input,
|
||||||
delivery: event.data.delivery,
|
|
||||||
timeCreated: event.created,
|
timeCreated: event.created,
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { sql } from "drizzle-orm"
|
||||||
import { directoryColumn, pathColumn } from "../database/path"
|
import { directoryColumn, pathColumn } from "../database/path"
|
||||||
import { ProjectTable } from "../project/sql"
|
import { ProjectTable } from "../project/sql"
|
||||||
import type { SessionMessage } from "./message"
|
import type { SessionMessage } from "./message"
|
||||||
import type { Prompt } from "@opencode-ai/schema/prompt"
|
|
||||||
import type { SessionInput } from "./input"
|
import type { SessionInput } from "./input"
|
||||||
import type { FileDiff } from "@opencode-ai/schema/file-diff"
|
import type { FileDiff } from "@opencode-ai/schema/file-diff"
|
||||||
import { PermissionV1 } from "../v1/permission"
|
import { PermissionV1 } from "../v1/permission"
|
||||||
|
|
@ -14,6 +13,7 @@ import { WorkspaceV2 } from "../workspace"
|
||||||
import { Timestamps } from "../database/schema.sql"
|
import { Timestamps } from "../database/schema.sql"
|
||||||
import type { Instructions } from "../instructions/index"
|
import type { Instructions } from "../instructions/index"
|
||||||
import type { Session } from "@opencode-ai/schema/session"
|
import type { Session } from "@opencode-ai/schema/session"
|
||||||
|
import type { SyntheticData, UserData } from "@opencode-ai/schema/session-input"
|
||||||
import type { RevertV1 } from "@opencode-ai/schema/session-revert"
|
import type { RevertV1 } from "@opencode-ai/schema/session-revert"
|
||||||
import type { Schema } from "effect"
|
import type { Schema } from "effect"
|
||||||
|
|
||||||
|
|
@ -150,7 +150,7 @@ export const SessionInputTable = sqliteTable(
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => SessionTable.id, { onDelete: "cascade" }),
|
.references(() => SessionTable.id, { onDelete: "cascade" }),
|
||||||
type: text().$type<SessionInput.Info["type"]>().notNull(),
|
type: text().$type<SessionInput.Info["type"]>().notNull(),
|
||||||
prompt: text({ mode: "json" }).$type<Prompt>(),
|
data: text({ mode: "json" }).$type<UserData | SyntheticData | Record<string, never>>().notNull(),
|
||||||
delivery: text().$type<SessionInput.Delivery>(),
|
delivery: text().$type<SessionInput.Delivery>(),
|
||||||
admitted_seq: integer().notNull(),
|
admitted_seq: integer().notNull(),
|
||||||
promoted_seq: integer(),
|
promoted_seq: integer(),
|
||||||
|
|
@ -159,10 +159,9 @@ export const SessionInputTable = sqliteTable(
|
||||||
.$default(() => Date.now()),
|
.$default(() => Date.now()),
|
||||||
},
|
},
|
||||||
(table) => [
|
(table) => [
|
||||||
index("session_input_session_pending_type_delivery_seq_idx").on(
|
index("session_input_session_pending_delivery_seq_idx").on(
|
||||||
table.session_id,
|
table.session_id,
|
||||||
table.promoted_seq,
|
table.promoted_seq,
|
||||||
table.type,
|
|
||||||
table.delivery,
|
table.delivery,
|
||||||
table.admitted_seq,
|
table.admitted_seq,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@ export const Plugin = {
|
||||||
|
|
||||||
const run = Effect.gen(function* () {
|
const run = Effect.gen(function* () {
|
||||||
// The child session owns its agent/model (set at create); prompt only admits input.
|
// The child session owns its agent/model (set at create); prompt only admits input.
|
||||||
yield* runtime.session.prompt({ sessionID: child.id, prompt: { text: input.prompt }, resume: false })
|
yield* runtime.session.prompt({ sessionID: child.id, text: input.prompt, resume: false })
|
||||||
yield* runtime.session.resume(child.id)
|
yield* runtime.session.resume(child.id)
|
||||||
return yield* latestAssistantText(child.id)
|
return yield* latestAssistantText(child.id)
|
||||||
}).pipe(Effect.onInterrupt(() => runtime.session.interrupt(child.id)))
|
}).pipe(Effect.onInterrupt(() => runtime.session.interrupt(child.id)))
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import simplifySessionInputMigration from "@opencode-ai/core/database/migration/
|
||||||
import resetSessionEventsMigration from "@opencode-ai/core/database/migration/20260703200000_reset_v2_session_events"
|
import resetSessionEventsMigration from "@opencode-ai/core/database/migration/20260703200000_reset_v2_session_events"
|
||||||
import durableSessionInboxMigration from "@opencode-ai/core/database/migration/20260707010146_durable_session_inbox"
|
import durableSessionInboxMigration from "@opencode-ai/core/database/migration/20260707010146_durable_session_inbox"
|
||||||
import migratePrelaunchV2StateMigration from "@opencode-ai/core/database/migration/20260707120000_migrate_prelaunch_v2_state"
|
import migratePrelaunchV2StateMigration from "@opencode-ai/core/database/migration/20260707120000_migrate_prelaunch_v2_state"
|
||||||
|
import genericSessionInputMigration from "@opencode-ai/core/database/migration/20260709013000_generic_session_input"
|
||||||
import renameInstructionsMigration from "@opencode-ai/core/database/migration/20260705180000_rename_instructions"
|
import renameInstructionsMigration from "@opencode-ai/core/database/migration/20260705180000_rename_instructions"
|
||||||
import addSessionForkMigration from "@opencode-ai/core/database/migration/20260706223930_add-session-fork"
|
import addSessionForkMigration from "@opencode-ai/core/database/migration/20260706223930_add-session-fork"
|
||||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
|
|
@ -369,7 +370,7 @@ describe("DatabaseMigration", () => {
|
||||||
{ name: "event_aggregate_type_seq_idx" },
|
{ name: "event_aggregate_type_seq_idx" },
|
||||||
{ name: "session_input_session_admitted_seq_idx" },
|
{ name: "session_input_session_admitted_seq_idx" },
|
||||||
{ name: "session_input_session_pending_compaction_idx" },
|
{ name: "session_input_session_pending_compaction_idx" },
|
||||||
{ name: "session_input_session_pending_type_delivery_seq_idx" },
|
{ name: "session_input_session_pending_delivery_seq_idx" },
|
||||||
{ name: "session_input_session_promoted_seq_idx" },
|
{ name: "session_input_session_promoted_seq_idx" },
|
||||||
{ name: "session_message_session_seq_idx" },
|
{ name: "session_message_session_seq_idx" },
|
||||||
{ name: "session_message_session_time_created_id_idx" },
|
{ name: "session_message_session_time_created_id_idx" },
|
||||||
|
|
@ -607,7 +608,7 @@ describe("DatabaseMigration", () => {
|
||||||
sql`INSERT INTO event (id, aggregate_id, seq, type, data, created) VALUES ('event', 'session', 9, 'session.updated.1', '{}', 1)`,
|
sql`INSERT INTO event (id, aggregate_id, seq, type, data, created) VALUES ('event', 'session', 9, 'session.updated.1', '{}', 1)`,
|
||||||
)
|
)
|
||||||
yield* db.run(
|
yield* db.run(
|
||||||
sql`INSERT INTO session_input (id, session_id, type, prompt, delivery, admitted_seq, time_created) VALUES ('input', 'session', 'prompt', '{}', 'steer', 9, 1)`,
|
sql`INSERT INTO session_input (id, session_id, type, data, delivery, admitted_seq, time_created) VALUES ('input', 'session', 'user', '{}', 'steer', 9, 1)`,
|
||||||
)
|
)
|
||||||
yield* db.run(
|
yield* db.run(
|
||||||
sql`INSERT INTO session_message (id, session_id, type, seq, time_created, time_updated, data) VALUES ('projected', 'session', 'user', 9, 1, 1, '{}')`,
|
sql`INSERT INTO session_message (id, session_id, type, seq, time_created, time_updated, data) VALUES ('projected', 'session', 'user', 9, 1, 1, '{}')`,
|
||||||
|
|
@ -701,6 +702,56 @@ describe("DatabaseMigration", () => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("migrates prompt inbox rows and lifecycle events to generic user input", async () => {
|
||||||
|
await run(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const db = yield* makeDb
|
||||||
|
yield* db.run(sql`CREATE TABLE session (id text PRIMARY KEY)`)
|
||||||
|
yield* db.run(sql`INSERT INTO session (id) VALUES ('session')`)
|
||||||
|
yield* db.run(
|
||||||
|
sql`CREATE TABLE session_input (id text PRIMARY KEY, session_id text NOT NULL, type text NOT NULL, prompt text, delivery text, admitted_seq integer NOT NULL, promoted_seq integer, time_created integer NOT NULL)`,
|
||||||
|
)
|
||||||
|
yield* db.run(
|
||||||
|
sql`INSERT INTO session_input (id, session_id, type, prompt, delivery, admitted_seq, promoted_seq, time_created) VALUES ('input', 'session', 'prompt', '{"text":"hello"}', 'queue', 4, NULL, 1)`,
|
||||||
|
)
|
||||||
|
yield* db.run(
|
||||||
|
sql`INSERT INTO session_input (id, session_id, type, prompt, delivery, admitted_seq, promoted_seq, time_created) VALUES ('empty', 'session', 'prompt', NULL, 'steer', 6, NULL, 2)`,
|
||||||
|
)
|
||||||
|
yield* db.run(
|
||||||
|
sql`CREATE TABLE event (id text PRIMARY KEY, aggregate_id text NOT NULL, seq integer NOT NULL, created integer NOT NULL, type text NOT NULL, data text NOT NULL)`,
|
||||||
|
)
|
||||||
|
yield* db.run(
|
||||||
|
sql`INSERT INTO event (id, aggregate_id, seq, created, type, data) VALUES ('admitted', 'session', 4, 1, 'session.prompt.admitted.1', '{"sessionID":"session","inputID":"input","prompt":{"text":"hello"},"delivery":"queue"}')`,
|
||||||
|
)
|
||||||
|
yield* db.run(
|
||||||
|
sql`INSERT INTO event (id, aggregate_id, seq, created, type, data) VALUES ('promoted', 'session', 5, 2, 'session.prompt.promoted.1', '{"sessionID":"session","inputID":"input"}')`,
|
||||||
|
)
|
||||||
|
yield* db.run(
|
||||||
|
sql`INSERT INTO event (id, aggregate_id, seq, created, type, data) VALUES ('empty-admitted', 'session', 6, 2, 'session.prompt.admitted.1', '{"sessionID":"session","inputID":"empty","prompt":null,"delivery":"steer"}')`,
|
||||||
|
)
|
||||||
|
yield* db.run(
|
||||||
|
sql`INSERT INTO event (id, aggregate_id, seq, created, type, data) VALUES ('empty-promoted', 'session', 7, 2, 'session.prompt.promoted.1', '{"sessionID":"session","inputID":"empty"}')`,
|
||||||
|
)
|
||||||
|
|
||||||
|
yield* DatabaseMigration.applyOnly(db, [genericSessionInputMigration])
|
||||||
|
|
||||||
|
expect(yield* db.all(sql`SELECT id, type, data, delivery FROM session_input ORDER BY admitted_seq`)).toEqual([
|
||||||
|
{ id: "input", type: "user", data: '{"text":"hello"}', delivery: "queue" },
|
||||||
|
])
|
||||||
|
expect(yield* db.all(sql`SELECT type, data FROM event ORDER BY seq`)).toEqual([
|
||||||
|
{
|
||||||
|
type: "session.input.admitted.1",
|
||||||
|
data: '{"sessionID":"session","inputID":"input","input":{"type":"user","data":{"text":"hello"},"delivery":"queue"}}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "session.input.promoted.1",
|
||||||
|
data: '{"sessionID":"session","inputID":"input"}',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
test("resets incompatible projected Session messages before adding sequence order", async () => {
|
test("resets incompatible projected Session messages before adding sequence order", async () => {
|
||||||
await run(
|
await run(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ import { SessionCompaction } from "@opencode-ai/core/session/compaction"
|
||||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||||
import { SessionInput } from "@opencode-ai/core/session/input"
|
import { SessionInput } from "@opencode-ai/core/session/input"
|
||||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||||
import { Prompt } from "@opencode-ai/schema/prompt"
|
|
||||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||||
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
||||||
|
|
@ -84,14 +83,16 @@ describe("SessionV2.compact", () => {
|
||||||
const created = yield* session.create({ location })
|
const created = yield* session.create({ location })
|
||||||
|
|
||||||
const messageID = SessionMessage.ID.create()
|
const messageID = SessionMessage.ID.create()
|
||||||
const prompt = Prompt.make({ text: "Please compact this session history." })
|
yield* events.publish(SessionEvent.InputAdmitted, {
|
||||||
yield* events.publish(SessionEvent.PromptAdmitted, {
|
|
||||||
sessionID: created.id,
|
sessionID: created.id,
|
||||||
inputID: messageID,
|
inputID: messageID,
|
||||||
prompt,
|
input: {
|
||||||
delivery: "steer",
|
type: "user",
|
||||||
|
data: { text: "Please compact this session history." },
|
||||||
|
delivery: "steer",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
yield* events.publish(SessionEvent.PromptPromoted, {
|
yield* events.publish(SessionEvent.InputPromoted, {
|
||||||
sessionID: created.id,
|
sessionID: created.id,
|
||||||
inputID: messageID,
|
inputID: messageID,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||||
import { PromptInput } from "@opencode-ai/schema/prompt-input"
|
|
||||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||||
|
|
@ -193,14 +192,12 @@ describe("SessionV2.create", () => {
|
||||||
const parent = yield* session.create({ location, title: "Parent" })
|
const parent = yield* session.create({ location, title: "Parent" })
|
||||||
const admitted = yield* session.prompt({
|
const admitted = yield* session.prompt({
|
||||||
sessionID: parent.id,
|
sessionID: parent.id,
|
||||||
prompt: PromptInput.Prompt.make({ text: "First" }),
|
text: "First",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
yield* SessionInput.promoteSteers(db, events, parent.id)
|
yield* SessionInput.promoteSteers(db, events, parent.id)
|
||||||
yield* events.publish(SessionEvent.Synthetic, {
|
yield* session.synthetic({ sessionID: parent.id, text: "parent note", resume: false })
|
||||||
sessionID: parent.id,
|
yield* SessionInput.promoteSteers(db, events, parent.id)
|
||||||
text: "parent note",
|
|
||||||
})
|
|
||||||
|
|
||||||
const forked = yield* session.fork({ sessionID: parent.id })
|
const forked = yield* session.fork({ sessionID: parent.id })
|
||||||
const parentContext = yield* session.context(parent.id)
|
const parentContext = yield* session.context(parent.id)
|
||||||
|
|
@ -222,19 +219,25 @@ describe("SessionV2.create", () => {
|
||||||
})
|
})
|
||||||
expect(yield* SessionInput.find(db, forkContext[0].id)).toMatchObject({
|
expect(yield* SessionInput.find(db, forkContext[0].id)).toMatchObject({
|
||||||
sessionID: forked.id,
|
sessionID: forked.id,
|
||||||
prompt: { text: "First" },
|
type: "user",
|
||||||
|
data: { text: "First" },
|
||||||
promotedSeq: 2,
|
promotedSeq: 2,
|
||||||
})
|
})
|
||||||
|
expect(yield* SessionInput.find(db, forkContext[1].id)).toMatchObject({
|
||||||
|
sessionID: forked.id,
|
||||||
|
type: "synthetic",
|
||||||
|
data: { text: "parent note" },
|
||||||
|
})
|
||||||
|
|
||||||
yield* session.prompt({
|
yield* session.prompt({
|
||||||
sessionID: parent.id,
|
sessionID: parent.id,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Parent changed" }),
|
text: "Parent changed",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
yield* SessionInput.promoteSteers(db, events, parent.id)
|
yield* SessionInput.promoteSteers(db, events, parent.id)
|
||||||
yield* session.prompt({
|
yield* session.prompt({
|
||||||
sessionID: forked.id,
|
sessionID: forked.id,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Child continues" }),
|
text: "Child continues",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
yield* SessionInput.promoteSteers(db, events, forked.id)
|
yield* SessionInput.promoteSteers(db, events, forked.id)
|
||||||
|
|
@ -246,7 +249,7 @@ describe("SessionV2.create", () => {
|
||||||
Array.from(yield* Stream.runCollect(logEvents(session, forked.id))).map(
|
Array.from(yield* Stream.runCollect(logEvents(session, forked.id))).map(
|
||||||
(event): number | undefined => event.durable?.seq,
|
(event): number | undefined => event.durable?.seq,
|
||||||
),
|
),
|
||||||
).toEqual([0, 4, 5])
|
).toEqual([0, 5, 6])
|
||||||
expect(yield* SessionInput.find(db, admitted.id)).toMatchObject({ sessionID: parent.id })
|
expect(yield* SessionInput.find(db, admitted.id)).toMatchObject({ sessionID: parent.id })
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
@ -259,13 +262,13 @@ describe("SessionV2.create", () => {
|
||||||
const parent = yield* session.create({ location })
|
const parent = yield* session.create({ location })
|
||||||
const first = yield* session.prompt({
|
const first = yield* session.prompt({
|
||||||
sessionID: parent.id,
|
sessionID: parent.id,
|
||||||
prompt: PromptInput.Prompt.make({ text: "First" }),
|
text: "First",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
yield* SessionInput.promoteSteers(db, events, parent.id)
|
yield* SessionInput.promoteSteers(db, events, parent.id)
|
||||||
const second = yield* session.prompt({
|
const second = yield* session.prompt({
|
||||||
sessionID: parent.id,
|
sessionID: parent.id,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Second" }),
|
text: "Second",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
yield* SessionInput.promoteSteers(db, events, parent.id)
|
yield* SessionInput.promoteSteers(db, events, parent.id)
|
||||||
|
|
@ -410,7 +413,7 @@ describe("SessionV2.create", () => {
|
||||||
const created = yield* session.create({ location })
|
const created = yield* session.create({ location })
|
||||||
yield* session.prompt({
|
yield* session.prompt({
|
||||||
sessionID: created.id,
|
sessionID: created.id,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Hello" }),
|
text: "Hello",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
yield* SessionInput.promoteSteers(db, events, created.id)
|
yield* SessionInput.promoteSteers(db, events, created.id)
|
||||||
|
|
@ -418,8 +421,12 @@ describe("SessionV2.create", () => {
|
||||||
expect(
|
expect(
|
||||||
Array.from(yield* logEvents(session, created.id, true).pipe(Stream.take(2), Stream.runCollect)),
|
Array.from(yield* logEvents(session, created.id, true).pipe(Stream.take(2), Stream.runCollect)),
|
||||||
).toMatchObject([
|
).toMatchObject([
|
||||||
{ durable: { seq: 1 }, type: "session.prompt.admitted", data: { prompt: { text: "Hello" } } },
|
{
|
||||||
{ durable: { seq: 2 }, type: "session.prompt.promoted" },
|
durable: { seq: 1 },
|
||||||
|
type: "session.input.admitted",
|
||||||
|
data: { input: { type: "user", data: { text: "Hello" }, delivery: "steer" } },
|
||||||
|
},
|
||||||
|
{ durable: { seq: 2 }, type: "session.input.promoted" },
|
||||||
])
|
])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
@ -432,7 +439,7 @@ describe("SessionV2.create", () => {
|
||||||
const created = yield* session.create({ id: SessionV2.ID.make("ses_fresh_target_replay"), location })
|
const created = yield* session.create({ id: SessionV2.ID.make("ses_fresh_target_replay"), location })
|
||||||
const admitted = yield* session.prompt({
|
const admitted = yield* session.prompt({
|
||||||
sessionID: created.id,
|
sessionID: created.id,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Replay lifecycle" }),
|
text: "Replay lifecycle",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
yield* SessionInput.promoteSteers(sourceDb, sourceEvents, created.id)
|
yield* SessionInput.promoteSteers(sourceDb, sourceEvents, created.id)
|
||||||
|
|
@ -476,7 +483,8 @@ describe("SessionV2.create", () => {
|
||||||
expect(yield* SessionInput.find(db, admitted.id)).toMatchObject({
|
expect(yield* SessionInput.find(db, admitted.id)).toMatchObject({
|
||||||
id: admitted.id,
|
id: admitted.id,
|
||||||
sessionID: created.id,
|
sessionID: created.id,
|
||||||
prompt: { text: "Replay lifecycle" },
|
type: "user",
|
||||||
|
data: { text: "Replay lifecycle" },
|
||||||
delivery: "steer",
|
delivery: "steer",
|
||||||
admittedSeq: 1,
|
admittedSeq: 1,
|
||||||
})
|
})
|
||||||
|
|
@ -486,7 +494,8 @@ describe("SessionV2.create", () => {
|
||||||
expect(yield* SessionInput.find(db, admitted.id)).toMatchObject({
|
expect(yield* SessionInput.find(db, admitted.id)).toMatchObject({
|
||||||
id: admitted.id,
|
id: admitted.id,
|
||||||
sessionID: created.id,
|
sessionID: created.id,
|
||||||
prompt: { text: "Replay lifecycle" },
|
type: "user",
|
||||||
|
data: { text: "Replay lifecycle" },
|
||||||
delivery: "steer",
|
delivery: "steer",
|
||||||
admittedSeq: 1,
|
admittedSeq: 1,
|
||||||
promotedSeq: 2,
|
promotedSeq: 2,
|
||||||
|
|
@ -504,8 +513,8 @@ describe("SessionV2.create", () => {
|
||||||
.pipe(Effect.orDie)).map((event) => [event.seq, event.type]),
|
.pipe(Effect.orDie)).map((event) => [event.seq, event.type]),
|
||||||
).toEqual([
|
).toEqual([
|
||||||
[0, EventV2.versionedType(SessionV1.Event.Created.type, 1)],
|
[0, EventV2.versionedType(SessionV1.Event.Created.type, 1)],
|
||||||
[1, EventV2.versionedType(SessionEvent.PromptAdmitted.type, 1)],
|
[1, EventV2.versionedType(SessionEvent.InputAdmitted.type, 1)],
|
||||||
[2, EventV2.versionedType(SessionEvent.PromptPromoted.type, 1)],
|
[2, EventV2.versionedType(SessionEvent.InputPromoted.type, 1)],
|
||||||
])
|
])
|
||||||
}).pipe(Effect.provide(Layer.fresh(targetLayer)))
|
}).pipe(Effect.provide(Layer.fresh(targetLayer)))
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||||
import { Prompt } from "@opencode-ai/schema/prompt"
|
|
||||||
import { Money } from "@opencode-ai/schema/money"
|
import { Money } from "@opencode-ai/schema/money"
|
||||||
import { SessionMessageUpdater } from "@opencode-ai/core/session/message-updater"
|
import { SessionMessageUpdater } from "@opencode-ai/core/session/message-updater"
|
||||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||||
|
|
@ -266,28 +265,26 @@ describe("SessionProjector", () => {
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
|
|
||||||
yield* events.publish(SessionEvent.PromptAdmitted, {
|
yield* events.publish(SessionEvent.InputAdmitted, {
|
||||||
sessionID,
|
sessionID,
|
||||||
inputID: SessionMessage.ID.make("msg_first"),
|
inputID: SessionMessage.ID.make("msg_first"),
|
||||||
prompt: Prompt.make({ text: "first" }),
|
input: { type: "user", data: { text: "first" }, delivery: "steer" },
|
||||||
delivery: "steer",
|
|
||||||
})
|
})
|
||||||
yield* events.publish(
|
yield* events.publish(
|
||||||
SessionEvent.PromptPromoted,
|
SessionEvent.InputPromoted,
|
||||||
{
|
{
|
||||||
sessionID,
|
sessionID,
|
||||||
inputID: SessionMessage.ID.make("msg_first"),
|
inputID: SessionMessage.ID.make("msg_first"),
|
||||||
},
|
},
|
||||||
{ id: EventV2.ID.make("evt_z") },
|
{ id: EventV2.ID.make("evt_z") },
|
||||||
)
|
)
|
||||||
yield* events.publish(SessionEvent.PromptAdmitted, {
|
yield* events.publish(SessionEvent.InputAdmitted, {
|
||||||
sessionID,
|
sessionID,
|
||||||
inputID: SessionMessage.ID.make("msg_second"),
|
inputID: SessionMessage.ID.make("msg_second"),
|
||||||
prompt: Prompt.make({ text: "second" }),
|
input: { type: "user", data: { text: "second" }, delivery: "steer" },
|
||||||
delivery: "steer",
|
|
||||||
})
|
})
|
||||||
yield* events.publish(
|
yield* events.publish(
|
||||||
SessionEvent.PromptPromoted,
|
SessionEvent.InputPromoted,
|
||||||
{
|
{
|
||||||
sessionID,
|
sessionID,
|
||||||
inputID: SessionMessage.ID.make("msg_second"),
|
inputID: SessionMessage.ID.make("msg_second"),
|
||||||
|
|
@ -344,12 +341,11 @@ describe("SessionProjector", () => {
|
||||||
const admitted = yield* SessionInput.admit(db, events, {
|
const admitted = yield* SessionInput.admit(db, events, {
|
||||||
id,
|
id,
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: Prompt.make({ text: "promote me" }),
|
input: { type: "user", data: { text: "promote me" }, delivery: "steer" },
|
||||||
delivery: "steer",
|
|
||||||
})
|
})
|
||||||
if (!admitted) return yield* Effect.die("Prompt admission failed")
|
if (!admitted) return yield* Effect.die("Prompt admission failed")
|
||||||
|
|
||||||
const event = yield* events.publish(SessionEvent.PromptPromoted, {
|
const event = yield* events.publish(SessionEvent.InputPromoted, {
|
||||||
sessionID,
|
sessionID,
|
||||||
inputID: id,
|
inputID: id,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import { Project } from "@opencode-ai/core/project"
|
||||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { PromptInput } from "@opencode-ai/schema/prompt-input"
|
|
||||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||||
|
|
@ -174,16 +173,17 @@ describe("SessionV2.prompt", () => {
|
||||||
|
|
||||||
const message = yield* session.prompt({
|
const message = yield* session.prompt({
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Fix the failing tests" }),
|
text: "Fix the failing tests",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(message.prompt.text).toBe("Fix the failing tests")
|
expect(message.data.text).toBe("Fix the failing tests")
|
||||||
expect(yield* session.messages({ sessionID })).toEqual([])
|
expect(yield* session.messages({ sessionID })).toEqual([])
|
||||||
expect(yield* admitted(message.id)).toMatchObject({
|
expect(yield* admitted(message.id)).toMatchObject({
|
||||||
id: message.id,
|
id: message.id,
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: { text: "Fix the failing tests" },
|
type: "user",
|
||||||
|
data: { text: "Fix the failing tests" },
|
||||||
delivery: "steer",
|
delivery: "steer",
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
|
@ -198,7 +198,7 @@ describe("SessionV2.prompt", () => {
|
||||||
|
|
||||||
const boundary = yield* session.prompt({
|
const boundary = yield* session.prompt({
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "boundary" }),
|
text: "boundary",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
yield* SessionInput.promoteSteers(db, events, sessionID)
|
yield* SessionInput.promoteSteers(db, events, sessionID)
|
||||||
|
|
@ -210,7 +210,7 @@ describe("SessionV2.prompt", () => {
|
||||||
})
|
})
|
||||||
expect((yield* session.get(sessionID)).revert?.messageID).toBe(boundary.id)
|
expect((yield* session.get(sessionID)).revert?.messageID).toBe(boundary.id)
|
||||||
|
|
||||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "after revert" }), resume: false })
|
yield* session.prompt({ sessionID, text: "after revert", resume: false })
|
||||||
|
|
||||||
expect((yield* session.get(sessionID)).revert).toBeUndefined()
|
expect((yield* session.get(sessionID)).revert).toBeUndefined()
|
||||||
expect(
|
expect(
|
||||||
|
|
@ -222,6 +222,35 @@ describe("SessionV2.prompt", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("holds synthetic input behind a staged revert and discards it when committed", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* setup
|
||||||
|
const session = yield* SessionV2.Service
|
||||||
|
const events = yield* EventV2.Service
|
||||||
|
const { db } = yield* Database.Service
|
||||||
|
const boundary = yield* session.prompt({
|
||||||
|
sessionID,
|
||||||
|
text: "boundary",
|
||||||
|
resume: false,
|
||||||
|
})
|
||||||
|
yield* SessionInput.promoteSteers(db, events, sessionID)
|
||||||
|
yield* events.publish(SessionEvent.RevertEvent.Staged, {
|
||||||
|
sessionID,
|
||||||
|
revert: { messageID: boundary.id, files: [] },
|
||||||
|
})
|
||||||
|
wakeCalls.length = 0
|
||||||
|
|
||||||
|
const completion = yield* session.synthetic({ sessionID, text: "stale completion" })
|
||||||
|
|
||||||
|
expect(wakeCalls).toEqual([])
|
||||||
|
expect(yield* SessionInput.find(db, completion.id)).toMatchObject({ type: "synthetic" })
|
||||||
|
|
||||||
|
yield* session.revert.commit(sessionID)
|
||||||
|
|
||||||
|
expect(yield* SessionInput.find(db, completion.id)).toBeUndefined()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("resolves attachment MIME before admission", () =>
|
it.effect("resolves attachment MIME before admission", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* setup
|
yield* setup
|
||||||
|
|
@ -231,14 +260,12 @@ describe("SessionV2.prompt", () => {
|
||||||
|
|
||||||
const message = yield* session.prompt({
|
const message = yield* session.prompt({
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: {
|
text: "Inspect this image",
|
||||||
text: "Inspect this image",
|
files: [{ uri, name: "image.png", mention: { start: 8, end: 17, text: "[Image 1]" } }],
|
||||||
files: [{ uri, name: "image.png", mention: { start: 8, end: 17, text: "[Image 1]" } }],
|
|
||||||
},
|
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(message.prompt.files).toEqual([
|
expect(message.data.files).toEqual([
|
||||||
{
|
{
|
||||||
data: uri.slice(uri.indexOf(",") + 1),
|
data: uri.slice(uri.indexOf(",") + 1),
|
||||||
mime: "image/png",
|
mime: "image/png",
|
||||||
|
|
@ -248,8 +275,8 @@ describe("SessionV2.prompt", () => {
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
const stored = yield* admitted(message.id)
|
const stored = yield* admitted(message.id)
|
||||||
expect(stored?.type).toBe("prompt")
|
expect(stored?.type).toBe("user")
|
||||||
if (stored?.type === "prompt") expect(stored.prompt.files).toEqual(message.prompt.files)
|
if (stored?.type === "user") expect(stored.data.files).toEqual(message.data.files)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -265,21 +292,19 @@ describe("SessionV2.prompt", () => {
|
||||||
|
|
||||||
const message = yield* session.prompt({
|
const message = yield* session.prompt({
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: {
|
text: "Inspect this",
|
||||||
text: "Inspect this",
|
files: [{ uri: sourceUri.href, name: "main.ts" }],
|
||||||
files: [{ uri: sourceUri.href, name: "main.ts" }],
|
|
||||||
},
|
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(message.prompt.files).toHaveLength(1)
|
expect(message.data.files).toHaveLength(1)
|
||||||
expect(message.prompt.files?.[0]).toMatchObject({
|
expect(message.data.files?.[0]).toMatchObject({
|
||||||
mime: "text/plain",
|
mime: "text/plain",
|
||||||
source: { type: "uri", uri: sourceUri.href },
|
source: { type: "uri", uri: sourceUri.href },
|
||||||
name: "main.ts",
|
name: "main.ts",
|
||||||
})
|
})
|
||||||
expect(
|
expect(
|
||||||
Buffer.from(message.prompt.files?.[0]?.data ?? "", "base64")
|
Buffer.from(message.data.files?.[0]?.data ?? "", "base64")
|
||||||
.toString("utf8")
|
.toString("utf8")
|
||||||
.replace(/\r$/, ""),
|
.replace(/\r$/, ""),
|
||||||
).toBe('import { describe, expect } from "bun:test"')
|
).toBe('import { describe, expect } from "bun:test"')
|
||||||
|
|
@ -294,17 +319,18 @@ describe("SessionV2.prompt", () => {
|
||||||
|
|
||||||
const message = yield* session.prompt({
|
const message = yield* session.prompt({
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: { text: "Inspect this", files: [{ uri, name: "source" }] },
|
text: "Inspect this",
|
||||||
|
files: [{ uri, name: "source" }],
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(message.prompt.files).toHaveLength(1)
|
expect(message.data.files).toHaveLength(1)
|
||||||
expect(message.prompt.files?.[0]).toMatchObject({
|
expect(message.data.files?.[0]).toMatchObject({
|
||||||
mime: "application/x-directory",
|
mime: "application/x-directory",
|
||||||
source: { type: "uri", uri },
|
source: { type: "uri", uri },
|
||||||
name: "source",
|
name: "source",
|
||||||
})
|
})
|
||||||
expect(Buffer.from(message.prompt.files?.[0]?.data ?? "", "base64").toString("utf8")).toContain(
|
expect(Buffer.from(message.data.files?.[0]?.data ?? "", "base64").toString("utf8")).toContain(
|
||||||
"session-prompt.test.ts",
|
"session-prompt.test.ts",
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|
@ -327,11 +353,12 @@ describe("SessionV2.prompt", () => {
|
||||||
|
|
||||||
const message = yield* session.prompt({
|
const message = yield* session.prompt({
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: { text: "Inspect this image", files: [{ uri: pathToFileURL(source).href }] },
|
text: "Inspect this image",
|
||||||
|
files: [{ uri: pathToFileURL(source).href }],
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(message.prompt.files).toEqual([
|
expect(message.data.files).toEqual([
|
||||||
{
|
{
|
||||||
data: bytes.toString("base64"),
|
data: bytes.toString("base64"),
|
||||||
mime: "image/png",
|
mime: "image/png",
|
||||||
|
|
@ -340,7 +367,7 @@ describe("SessionV2.prompt", () => {
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
const stored = yield* admitted(message.id)
|
const stored = yield* admitted(message.id)
|
||||||
expect(stored?.type === "prompt" ? stored.prompt.files : undefined).toEqual(message.prompt.files)
|
expect(stored?.type === "user" ? stored.data.files : undefined).toEqual(message.data.files)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -352,11 +379,12 @@ describe("SessionV2.prompt", () => {
|
||||||
|
|
||||||
const message = yield* session.prompt({
|
const message = yield* session.prompt({
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: { text: "Inspect this", files: [{ uri, name: "main.ts" }] },
|
text: "Inspect this",
|
||||||
|
files: [{ uri, name: "main.ts" }],
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(message.prompt.files).toEqual([
|
expect(message.data.files).toEqual([
|
||||||
{
|
{
|
||||||
data: Buffer.from("export const value = 1\n").toString("base64"),
|
data: Buffer.from("export const value = 1\n").toString("base64"),
|
||||||
mime: "text/plain",
|
mime: "text/plain",
|
||||||
|
|
@ -376,7 +404,8 @@ describe("SessionV2.prompt", () => {
|
||||||
const error = yield* session
|
const error = yield* session
|
||||||
.prompt({
|
.prompt({
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: { text: "Inspect this", files: [{ uri, name: "image.png" }] },
|
text: "Inspect this",
|
||||||
|
files: [{ uri, name: "image.png" }],
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
.pipe(Effect.flip)
|
.pipe(Effect.flip)
|
||||||
|
|
@ -402,22 +431,22 @@ describe("SessionV2.prompt", () => {
|
||||||
const fiber = yield* publicEvents({ sessionID }).pipe(Stream.take(4), Stream.runCollect, Effect.forkScoped)
|
const fiber = yield* publicEvents({ sessionID }).pipe(Stream.take(4), Stream.runCollect, Effect.forkScoped)
|
||||||
yield* Effect.yieldNow
|
yield* Effect.yieldNow
|
||||||
|
|
||||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
|
yield* session.prompt({ sessionID, text: "First", resume: false })
|
||||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Second" }), resume: false })
|
yield* session.prompt({ sessionID, text: "Second", resume: false })
|
||||||
yield* SessionInput.promoteSteers(db, events, sessionID)
|
yield* SessionInput.promoteSteers(db, events, sessionID)
|
||||||
const streamed = Array.from(yield* Fiber.join(fiber))
|
const streamed = Array.from(yield* Fiber.join(fiber))
|
||||||
|
|
||||||
expect(streamed.map((event): [number | undefined, string] => [event.durable?.seq, event.type])).toEqual([
|
expect(streamed.map((event): [number | undefined, string] => [event.durable?.seq, event.type])).toEqual([
|
||||||
[0, "session.prompt.admitted"],
|
[0, "session.input.admitted"],
|
||||||
[1, "session.prompt.admitted"],
|
[1, "session.input.admitted"],
|
||||||
[2, "session.prompt.promoted"],
|
[2, "session.input.promoted"],
|
||||||
[3, "session.prompt.promoted"],
|
[3, "session.input.promoted"],
|
||||||
])
|
])
|
||||||
expect(
|
expect(
|
||||||
Array.from(
|
Array.from(
|
||||||
yield* publicEvents({ sessionID, after: streamed[0].durable?.seq }).pipe(Stream.take(1), Stream.runCollect),
|
yield* publicEvents({ sessionID, after: streamed[0].durable?.seq }).pipe(Stream.take(1), Stream.runCollect),
|
||||||
).map((event): [number | undefined, string] => [event.durable?.seq, event.type]),
|
).map((event): [number | undefined, string] => [event.durable?.seq, event.type]),
|
||||||
).toEqual([[1, "session.prompt.admitted"]])
|
).toEqual([[1, "session.input.admitted"]])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -427,7 +456,7 @@ describe("SessionV2.prompt", () => {
|
||||||
const session = yield* SessionV2.Service
|
const session = yield* SessionV2.Service
|
||||||
const message = yield* session.prompt({
|
const message = yield* session.prompt({
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Fix the failing tests" }),
|
text: "Fix the failing tests",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -446,7 +475,7 @@ describe("SessionV2.prompt", () => {
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* setup
|
yield* setup
|
||||||
const session = yield* SessionV2.Service
|
const session = yield* SessionV2.Service
|
||||||
const input = { sessionID, prompt: PromptInput.Prompt.make({ text: "Fix the failing tests" }), resume: false }
|
const input = { sessionID, text: "Fix the failing tests", resume: false }
|
||||||
|
|
||||||
const first = yield* session.prompt(input)
|
const first = yield* session.prompt(input)
|
||||||
const second = yield* session.prompt(input)
|
const second = yield* session.prompt(input)
|
||||||
|
|
@ -464,7 +493,7 @@ describe("SessionV2.prompt", () => {
|
||||||
const input = {
|
const input = {
|
||||||
sessionID,
|
sessionID,
|
||||||
id: messageID,
|
id: messageID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Fix the failing tests" }),
|
text: "Fix the failing tests",
|
||||||
resume: false,
|
resume: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -484,7 +513,7 @@ describe("SessionV2.prompt", () => {
|
||||||
const input = {
|
const input = {
|
||||||
sessionID,
|
sessionID,
|
||||||
id: messageID,
|
id: messageID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Recover committed prompt" }),
|
text: "Recover committed prompt",
|
||||||
resume: false,
|
resume: false,
|
||||||
}
|
}
|
||||||
const first = yield* session.prompt(input)
|
const first = yield* session.prompt(input)
|
||||||
|
|
@ -505,13 +534,13 @@ describe("SessionV2.prompt", () => {
|
||||||
yield* session.prompt({
|
yield* session.prompt({
|
||||||
sessionID,
|
sessionID,
|
||||||
id: messageID,
|
id: messageID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Fix the failing tests" }),
|
text: "Fix the failing tests",
|
||||||
})
|
})
|
||||||
const failure = yield* session
|
const failure = yield* session
|
||||||
.prompt({
|
.prompt({
|
||||||
sessionID,
|
sessionID,
|
||||||
id: messageID,
|
id: messageID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Delete the failing tests" }),
|
text: "Delete the failing tests",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
.pipe(Effect.flip)
|
.pipe(Effect.flip)
|
||||||
|
|
@ -530,14 +559,14 @@ describe("SessionV2.prompt", () => {
|
||||||
yield* session.prompt({
|
yield* session.prompt({
|
||||||
id: messageID,
|
id: messageID,
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Fix the failing tests" }),
|
text: "Fix the failing tests",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
const failure = yield* session
|
const failure = yield* session
|
||||||
.prompt({
|
.prompt({
|
||||||
id: messageID,
|
id: messageID,
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Fix the failing tests" }),
|
text: "Fix the failing tests",
|
||||||
delivery: "queue",
|
delivery: "queue",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
|
|
@ -554,7 +583,7 @@ describe("SessionV2.prompt", () => {
|
||||||
const input = {
|
const input = {
|
||||||
sessionID,
|
sessionID,
|
||||||
id: messageID,
|
id: messageID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Fix the failing tests" }),
|
text: "Fix the failing tests",
|
||||||
resume: false,
|
resume: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -563,7 +592,7 @@ describe("SessionV2.prompt", () => {
|
||||||
expect(messages[1]).toEqual(messages[0])
|
expect(messages[1]).toEqual(messages[0])
|
||||||
expect(yield* session.messages({ sessionID })).toEqual([])
|
expect(yield* session.messages({ sessionID })).toEqual([])
|
||||||
expect(yield* admittedCount).toBe(1)
|
expect(yield* admittedCount).toBe(1)
|
||||||
expect(yield* eventCount(EventV2.versionedType(SessionEvent.PromptAdmitted.type, 1))).toBe(1)
|
expect(yield* eventCount(EventV2.versionedType(SessionEvent.InputAdmitted.type, 1))).toBe(1)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -576,7 +605,7 @@ describe("SessionV2.prompt", () => {
|
||||||
yield* session.prompt({
|
yield* session.prompt({
|
||||||
id: messageID,
|
id: messageID,
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Promote once" }),
|
text: "Promote once",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -585,7 +614,7 @@ describe("SessionV2.prompt", () => {
|
||||||
{ concurrency: "unbounded" },
|
{ concurrency: "unbounded" },
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(yield* eventCount(EventV2.versionedType(SessionEvent.PromptPromoted.type, 1))).toBe(1)
|
expect(yield* eventCount(EventV2.versionedType(SessionEvent.InputPromoted.type, 1))).toBe(1)
|
||||||
expect(yield* admitted(messageID)).toMatchObject({ promotedSeq: 1 })
|
expect(yield* admitted(messageID)).toMatchObject({ promotedSeq: 1 })
|
||||||
expect(yield* session.messages({ sessionID })).toMatchObject([
|
expect(yield* session.messages({ sessionID })).toMatchObject([
|
||||||
{ id: messageID, type: "user", text: "Promote once" },
|
{ id: messageID, type: "user", text: "Promote once" },
|
||||||
|
|
@ -603,9 +632,11 @@ describe("SessionV2.prompt", () => {
|
||||||
yield* session.prompt({
|
yield* session.prompt({
|
||||||
id: messageID,
|
id: messageID,
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Replay pending" }),
|
text: "Replay pending",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
|
const syntheticID = SessionMessage.ID.create()
|
||||||
|
yield* session.synthetic({ id: syntheticID, sessionID, text: "Replay synthetic", resume: false })
|
||||||
const recorded = yield* db
|
const recorded = yield* db
|
||||||
.select()
|
.select()
|
||||||
.from(EventTable)
|
.from(EventTable)
|
||||||
|
|
@ -631,7 +662,16 @@ describe("SessionV2.prompt", () => {
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(yield* admitted(messageID)).toMatchObject({ id: messageID, prompt: { text: "Replay pending" } })
|
expect(yield* admitted(messageID)).toMatchObject({
|
||||||
|
id: messageID,
|
||||||
|
type: "user",
|
||||||
|
data: { text: "Replay pending" },
|
||||||
|
})
|
||||||
|
expect(yield* admitted(syntheticID)).toMatchObject({
|
||||||
|
id: syntheticID,
|
||||||
|
type: "synthetic",
|
||||||
|
data: { text: "Replay synthetic" },
|
||||||
|
})
|
||||||
expect(yield* session.messages({ sessionID })).toEqual([])
|
expect(yield* session.messages({ sessionID })).toEqual([])
|
||||||
expect(wakeCalls).toEqual([])
|
expect(wakeCalls).toEqual([])
|
||||||
}),
|
}),
|
||||||
|
|
@ -656,11 +696,9 @@ describe("SessionV2.prompt", () => {
|
||||||
.onConflictDoNothing()
|
.onConflictDoNothing()
|
||||||
.run()
|
.run()
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
const prompt = PromptInput.Prompt.make({ text: "Fix the failing tests" })
|
yield* session.prompt({ id: messageID, sessionID, text: "Fix the failing tests", resume: false })
|
||||||
|
|
||||||
yield* session.prompt({ id: messageID, sessionID, prompt, resume: false })
|
|
||||||
const failure = yield* session
|
const failure = yield* session
|
||||||
.prompt({ id: messageID, sessionID: other, prompt, resume: false })
|
.prompt({ id: messageID, sessionID: other, text: "Fix the failing tests", resume: false })
|
||||||
.pipe(Effect.flip)
|
.pipe(Effect.flip)
|
||||||
|
|
||||||
expect(failure).toMatchObject({ _tag: "Session.PromptConflictError", sessionID: other, messageID })
|
expect(failure).toMatchObject({ _tag: "Session.PromptConflictError", sessionID: other, messageID })
|
||||||
|
|
@ -692,7 +730,7 @@ describe("SessionV2.prompt", () => {
|
||||||
.prompt({
|
.prompt({
|
||||||
id: messageID,
|
id: messageID,
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Conflicting prompt" }),
|
text: "Conflicting prompt",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
.pipe(Effect.flip)
|
.pipe(Effect.flip)
|
||||||
|
|
@ -709,7 +747,7 @@ describe("SessionV2.prompt", () => {
|
||||||
executionCalls.length = 0
|
executionCalls.length = 0
|
||||||
wakeCalls.length = 0
|
wakeCalls.length = 0
|
||||||
|
|
||||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Run by default" }) })
|
yield* session.prompt({ sessionID, text: "Run by default" })
|
||||||
|
|
||||||
expect(executionCalls).toEqual([])
|
expect(executionCalls).toEqual([])
|
||||||
expect(wakeCalls).toEqual([sessionID])
|
expect(wakeCalls).toEqual([sessionID])
|
||||||
|
|
@ -725,7 +763,7 @@ describe("SessionV2.prompt", () => {
|
||||||
|
|
||||||
yield* session.prompt({
|
yield* session.prompt({
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Run explicitly" }),
|
text: "Run explicitly",
|
||||||
resume: true,
|
resume: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -741,10 +779,150 @@ describe("SessionV2.prompt", () => {
|
||||||
executionCalls.length = 0
|
executionCalls.length = 0
|
||||||
wakeCalls.length = 0
|
wakeCalls.length = 0
|
||||||
|
|
||||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Do not run" }), resume: false })
|
yield* session.prompt({ sessionID, text: "Do not run", resume: false })
|
||||||
|
|
||||||
expect(executionCalls).toEqual([])
|
expect(executionCalls).toEqual([])
|
||||||
expect(wakeCalls).toEqual([])
|
expect(wakeCalls).toEqual([])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("treats prompt metadata as durable retry identity", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* setup
|
||||||
|
const session = yield* SessionV2.Service
|
||||||
|
const input = {
|
||||||
|
id: messageID,
|
||||||
|
sessionID,
|
||||||
|
text: "Deploy",
|
||||||
|
metadata: { source: "api" },
|
||||||
|
resume: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
const first = yield* session.prompt(input)
|
||||||
|
const retried = yield* session.prompt(input)
|
||||||
|
const failure = yield* session.prompt({ ...input, metadata: { source: "plugin" } }).pipe(Effect.flip)
|
||||||
|
|
||||||
|
expect(retried).toEqual(first)
|
||||||
|
expect(first.data.metadata).toEqual({ source: "api" })
|
||||||
|
expect(failure._tag).toBe("Session.PromptConflictError")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("durably admits synthetic input before transcript promotion", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* setup
|
||||||
|
const session = yield* SessionV2.Service
|
||||||
|
const events = yield* EventV2.Service
|
||||||
|
const { db } = yield* Database.Service
|
||||||
|
|
||||||
|
const input = yield* session.synthetic({
|
||||||
|
id: messageID,
|
||||||
|
sessionID,
|
||||||
|
text: "Background work completed",
|
||||||
|
description: "shell completion",
|
||||||
|
metadata: { job: "shell" },
|
||||||
|
resume: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(yield* session.messages({ sessionID })).toEqual([])
|
||||||
|
expect(yield* admitted(input.id)).toMatchObject({
|
||||||
|
type: "synthetic",
|
||||||
|
sessionID,
|
||||||
|
delivery: "steer",
|
||||||
|
data: {
|
||||||
|
text: "Background work completed",
|
||||||
|
description: "shell completion",
|
||||||
|
metadata: { job: "shell" },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
yield* SessionInput.promoteSteers(db, events, sessionID)
|
||||||
|
|
||||||
|
expect(yield* session.messages({ sessionID })).toMatchObject([
|
||||||
|
{
|
||||||
|
id: messageID,
|
||||||
|
type: "synthetic",
|
||||||
|
text: "Background work completed",
|
||||||
|
description: "shell completion",
|
||||||
|
metadata: { job: "shell" },
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("reconciles exact synthetic retries and rejects conflicting reuse", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* setup
|
||||||
|
const session = yield* SessionV2.Service
|
||||||
|
const events = yield* EventV2.Service
|
||||||
|
const database = yield* Database.Service
|
||||||
|
const input = { id: messageID, sessionID, text: "Completed", resume: false }
|
||||||
|
|
||||||
|
const entries = yield* Effect.all([session.synthetic(input), session.synthetic(input)], {
|
||||||
|
concurrency: "unbounded",
|
||||||
|
})
|
||||||
|
yield* SessionInput.promoteSteers(database.db, events, sessionID)
|
||||||
|
const promotedRetry = yield* session.synthetic(input)
|
||||||
|
const failure = yield* session.synthetic({ ...input, text: "Different completion" }).pipe(Effect.flip)
|
||||||
|
|
||||||
|
expect(entries[1]).toEqual(entries[0])
|
||||||
|
expect(promotedRetry).toMatchObject({ id: messageID, type: "synthetic", promotedSeq: expect.any(Number) })
|
||||||
|
expect(failure).toMatchObject({ _tag: "Session.SyntheticConflictError", sessionID, inputID: messageID })
|
||||||
|
expect(yield* admittedCount).toBe(1)
|
||||||
|
expect(yield* eventCount(EventV2.versionedType(SessionEvent.InputAdmitted.type, 1))).toBe(1)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("keeps synthetic queue input pending until the queue boundary", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* setup
|
||||||
|
const session = yield* SessionV2.Service
|
||||||
|
const events = yield* EventV2.Service
|
||||||
|
const { db } = yield* Database.Service
|
||||||
|
|
||||||
|
const input = yield* session.synthetic({
|
||||||
|
sessionID,
|
||||||
|
text: "Queued completion",
|
||||||
|
delivery: "queue",
|
||||||
|
resume: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(input.delivery).toBe("queue")
|
||||||
|
expect(yield* SessionInput.promoteSteers(db, events, sessionID)).toBe(0)
|
||||||
|
expect(yield* session.messages({ sessionID })).toEqual([])
|
||||||
|
expect(yield* SessionInput.promoteNextQueued(db, events, sessionID)).toBe(true)
|
||||||
|
expect(yield* session.messages({ sessionID })).toMatchObject([
|
||||||
|
{ id: input.id, type: "synthetic", text: "Queued completion" },
|
||||||
|
])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("promotes prompt and synthetic steers in admission order", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* setup
|
||||||
|
const session = yield* SessionV2.Service
|
||||||
|
const events = yield* EventV2.Service
|
||||||
|
const { db } = yield* Database.Service
|
||||||
|
|
||||||
|
yield* session.prompt({
|
||||||
|
sessionID,
|
||||||
|
text: "First prompt",
|
||||||
|
resume: false,
|
||||||
|
})
|
||||||
|
yield* session.synthetic({ sessionID, text: "Background completion", resume: false })
|
||||||
|
yield* session.prompt({
|
||||||
|
sessionID,
|
||||||
|
text: "Second prompt",
|
||||||
|
resume: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
yield* SessionInput.promoteSteers(db, events, sessionID)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(yield* session.messages({ sessionID, order: "asc" })).map((message) =>
|
||||||
|
message.type === "user" || message.type === "synthetic" ? message.text : message.type,
|
||||||
|
),
|
||||||
|
).toEqual(["First prompt", "Background completion", "Second prompt"])
|
||||||
|
}),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { Snapshot } from "@opencode-ai/core/snapshot"
|
import { Snapshot } from "@opencode-ai/core/snapshot"
|
||||||
import { SessionCompaction } from "@opencode-ai/core/session/compaction"
|
import { SessionCompaction } from "@opencode-ai/core/session/compaction"
|
||||||
import { SessionTitle } from "@opencode-ai/core/session/title"
|
import { SessionTitle } from "@opencode-ai/core/session/title"
|
||||||
import { PromptInput } from "@opencode-ai/schema/prompt-input"
|
|
||||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||||
import { SessionRunCoordinator } from "@opencode-ai/core/session/run-coordinator"
|
import { SessionRunCoordinator } from "@opencode-ai/core/session/run-coordinator"
|
||||||
|
|
@ -179,7 +178,7 @@ describe("SessionRunnerLLM recorded", () => {
|
||||||
const session = yield* SessionV2.Service
|
const session = yield* SessionV2.Service
|
||||||
const prompt = yield* session.prompt({
|
const prompt = yield* session.prompt({
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Say hello in one short sentence." }),
|
text: "Say hello in one short sentence.",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -200,8 +199,8 @@ describe("SessionRunnerLLM recorded", () => {
|
||||||
.orderBy(EventTable.seq)
|
.orderBy(EventTable.seq)
|
||||||
.all()).map((event) => event.type),
|
.all()).map((event) => event.type),
|
||||||
).toEqual([
|
).toEqual([
|
||||||
"session.prompt.admitted.1",
|
"session.input.admitted.1",
|
||||||
"session.prompt.promoted.1",
|
"session.input.promoted.1",
|
||||||
"session.step.started.1",
|
"session.step.started.1",
|
||||||
"session.text.started.1",
|
"session.text.started.1",
|
||||||
"session.text.ended.1",
|
"session.text.ended.1",
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ import { Snapshot } from "@opencode-ai/core/snapshot"
|
||||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||||
import { SessionInput } from "@opencode-ai/core/session/input"
|
import { SessionInput } from "@opencode-ai/core/session/input"
|
||||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||||
import { PromptInput } from "@opencode-ai/schema/prompt-input"
|
|
||||||
import { Money } from "@opencode-ai/schema/money"
|
import { Money } from "@opencode-ai/schema/money"
|
||||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||||
|
|
@ -72,6 +71,7 @@ const requests: LLMRequest[] = []
|
||||||
let response: LLMEvent[] = []
|
let response: LLMEvent[] = []
|
||||||
let responses: LLMEvent[][] | undefined
|
let responses: LLMEvent[][] | undefined
|
||||||
let responseStream: Stream.Stream<LLMEvent, LLMError> | undefined
|
let responseStream: Stream.Stream<LLMEvent, LLMError> | undefined
|
||||||
|
let responseStreams: Stream.Stream<LLMEvent, LLMError>[] | undefined
|
||||||
let streamGate: Deferred.Deferred<void> | undefined
|
let streamGate: Deferred.Deferred<void> | undefined
|
||||||
let streamStarted: Deferred.Deferred<void> | undefined
|
let streamStarted: Deferred.Deferred<void> | undefined
|
||||||
let streamFailure: LLMError | undefined
|
let streamFailure: LLMError | undefined
|
||||||
|
|
@ -86,6 +86,7 @@ const client = Layer.succeed(
|
||||||
prepare: () => Effect.die("unused"),
|
prepare: () => Effect.die("unused"),
|
||||||
stream: ((request: LLMRequest) => {
|
stream: ((request: LLMRequest) => {
|
||||||
requests.push(request)
|
requests.push(request)
|
||||||
|
if (responseStreams) return responseStreams.shift() ?? Stream.empty
|
||||||
if (responseStream) {
|
if (responseStream) {
|
||||||
const stream = responseStream
|
const stream = responseStream
|
||||||
responseStream = undefined
|
responseStream = undefined
|
||||||
|
|
@ -389,8 +390,7 @@ const it = testEffect(
|
||||||
)
|
)
|
||||||
const sessionID = SessionV2.ID.make("ses_runner_test")
|
const sessionID = SessionV2.ID.make("ses_runner_test")
|
||||||
const otherSessionID = SessionV2.ID.make("ses_runner_other")
|
const otherSessionID = SessionV2.ID.make("ses_runner_other")
|
||||||
const admit = (session: SessionV2.Interface, text: string) =>
|
const admit = (session: SessionV2.Interface, text: string) => session.prompt({ sessionID, text, resume: false })
|
||||||
session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text }), resume: false })
|
|
||||||
|
|
||||||
const insertSession = (id: SessionV2.ID) =>
|
const insertSession = (id: SessionV2.ID) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|
@ -427,6 +427,7 @@ const setup = Effect.gen(function* () {
|
||||||
responses = undefined
|
responses = undefined
|
||||||
streamFailure = undefined
|
streamFailure = undefined
|
||||||
responseStream = undefined
|
responseStream = undefined
|
||||||
|
responseStreams = undefined
|
||||||
streamGate = undefined
|
streamGate = undefined
|
||||||
streamStarted = undefined
|
streamStarted = undefined
|
||||||
toolExecutionGate = undefined
|
toolExecutionGate = undefined
|
||||||
|
|
@ -774,7 +775,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
|
|
||||||
const message = yield* session.prompt({
|
const message = yield* session.prompt({
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Run automatically" }),
|
text: "Run automatically",
|
||||||
})
|
})
|
||||||
yield* session.wait(sessionID)
|
yield* session.wait(sessionID)
|
||||||
|
|
||||||
|
|
@ -785,6 +786,34 @@ describe("SessionRunnerLLM", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("runs a follow-up when synthetic input arrives during an active continuation", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const session = yield* setup
|
||||||
|
const secondStarted = yield* Deferred.make<void>()
|
||||||
|
const releaseSecond = yield* Deferred.make<void>()
|
||||||
|
responseStreams = [
|
||||||
|
Stream.fromIterable(reply.tool("call-echo", "echo", { text: "background started" })),
|
||||||
|
Stream.unwrap(
|
||||||
|
Deferred.succeed(secondStarted, undefined).pipe(
|
||||||
|
Effect.andThen(Deferred.await(releaseSecond)),
|
||||||
|
Effect.as(Stream.fromIterable(reply.stop())),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Stream.fromIterable(reply.text("Handled completion", "text-completion")),
|
||||||
|
]
|
||||||
|
yield* admit(session, "Start background work")
|
||||||
|
const running = yield* session.resume(sessionID).pipe(Effect.forkChild({ startImmediately: true }))
|
||||||
|
yield* Deferred.await(secondStarted)
|
||||||
|
|
||||||
|
yield* session.synthetic({ sessionID, text: "Background work completed" })
|
||||||
|
yield* Deferred.succeed(releaseSecond, undefined)
|
||||||
|
yield* Fiber.join(running)
|
||||||
|
|
||||||
|
expect(requests).toHaveLength(3)
|
||||||
|
expect(userTexts(requests[2]!)).toContain("Background work completed")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("streams one request with registry definitions from chronological V2 user history", () =>
|
it.effect("streams one request with registry definitions from chronological V2 user history", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const session = yield* setup
|
const session = yield* setup
|
||||||
|
|
@ -813,7 +842,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
yield* session.prompt({
|
yield* session.prompt({
|
||||||
id: messageID,
|
id: messageID,
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "First" }),
|
text: "First",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -832,7 +861,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
).toBeUndefined()
|
).toBeUndefined()
|
||||||
|
|
||||||
systemUnavailable = false
|
systemUnavailable = false
|
||||||
yield* session.prompt({ id: messageID, sessionID, prompt: PromptInput.Prompt.make({ text: "First" }) })
|
yield* session.prompt({ id: messageID, sessionID, text: "First" })
|
||||||
yield* session.wait(sessionID)
|
yield* session.wait(sessionID)
|
||||||
|
|
||||||
expect(requests).toHaveLength(1)
|
expect(requests).toHaveLength(1)
|
||||||
|
|
@ -1099,7 +1128,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
.run()
|
.run()
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
const session = yield* SessionV2.Service
|
const session = yield* SessionV2.Service
|
||||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Inspect files" }), resume: false })
|
yield* session.prompt({ sessionID, text: "Inspect files", resume: false })
|
||||||
|
|
||||||
requests.length = 0
|
requests.length = 0
|
||||||
response = []
|
response = []
|
||||||
|
|
@ -1120,7 +1149,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
const release = yield* Deferred.make<void>()
|
const release = yield* Deferred.make<void>()
|
||||||
pluginFlushHook = Deferred.await(release)
|
pluginFlushHook = Deferred.await(release)
|
||||||
const session = yield* SessionV2.Service
|
const session = yield* SessionV2.Service
|
||||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Wait for plugins" }), resume: false })
|
yield* session.prompt({ sessionID, text: "Wait for plugins", resume: false })
|
||||||
|
|
||||||
requests.length = 0
|
requests.length = 0
|
||||||
response = []
|
response = []
|
||||||
|
|
@ -1409,9 +1438,10 @@ describe("SessionRunnerLLM", () => {
|
||||||
expect((yield* session.messages({ sessionID })).find((message) => message.id === first.id)).toBeUndefined()
|
expect((yield* session.messages({ sessionID })).find((message) => message.id === first.id)).toBeUndefined()
|
||||||
|
|
||||||
yield* admit(session, "Steer after compaction")
|
yield* admit(session, "Steer after compaction")
|
||||||
|
yield* session.synthetic({ sessionID, text: "Completion after compaction", resume: false })
|
||||||
yield* session.prompt({
|
yield* session.prompt({
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Queue after compaction" }),
|
text: "Queue after compaction",
|
||||||
delivery: "queue",
|
delivery: "queue",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
|
|
@ -1423,6 +1453,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
expect(requests).toHaveLength(4)
|
expect(requests).toHaveLength(4)
|
||||||
expect(userTexts(requests[1])[0]).toContain("Create a new anchored summary")
|
expect(userTexts(requests[1])[0]).toContain("Create a new anchored summary")
|
||||||
expect(userTexts(requests[2])).toContain("Steer after compaction")
|
expect(userTexts(requests[2])).toContain("Steer after compaction")
|
||||||
|
expect(userTexts(requests[2])).toContain("Completion after compaction")
|
||||||
expect(userTexts(requests[3])).toContain("Queue after compaction")
|
expect(userTexts(requests[3])).toContain("Queue after compaction")
|
||||||
expect(yield* SessionInput.pendingCompaction((yield* Database.Service).db, sessionID)).toBeUndefined()
|
expect(yield* SessionInput.pendingCompaction((yield* Database.Service).db, sessionID)).toBeUndefined()
|
||||||
expect((yield* session.messages({ sessionID })).find((message) => message.id === first.id)).toMatchObject({
|
expect((yield* session.messages({ sessionID })).find((message) => message.id === first.id)).toMatchObject({
|
||||||
|
|
@ -1451,7 +1482,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
const compaction = yield* session.compact({ sessionID })
|
const compaction = yield* session.compact({ sessionID })
|
||||||
yield* session.prompt({
|
yield* session.prompt({
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Continue after failure" }),
|
text: "Continue after failure",
|
||||||
delivery: "queue",
|
delivery: "queue",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
|
|
@ -2189,7 +2220,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
|
|
||||||
const first = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
const first = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
||||||
yield* Deferred.await(streamStarted)
|
yield* Deferred.await(streamStarted)
|
||||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Change direction" }) })
|
yield* session.prompt({ sessionID, text: "Change direction" })
|
||||||
yield* Deferred.succeed(streamGate, undefined)
|
yield* Deferred.succeed(streamGate, undefined)
|
||||||
yield* Fiber.join(first)
|
yield* Fiber.join(first)
|
||||||
streamGate = undefined
|
streamGate = undefined
|
||||||
|
|
@ -2221,7 +2252,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
yield* Deferred.await(streamStarted)
|
yield* Deferred.await(streamStarted)
|
||||||
yield* session.prompt({
|
yield* session.prompt({
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Wait until continuation ends" }),
|
text: "Wait until continuation ends",
|
||||||
delivery: "queue",
|
delivery: "queue",
|
||||||
})
|
})
|
||||||
yield* Deferred.succeed(streamGate, undefined)
|
yield* Deferred.succeed(streamGate, undefined)
|
||||||
|
|
@ -2250,7 +2281,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
yield* Deferred.await(streamStarted)
|
yield* Deferred.await(streamStarted)
|
||||||
yield* session.prompt({
|
yield* session.prompt({
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Run after interrupt" }),
|
text: "Run after interrupt",
|
||||||
delivery: "queue",
|
delivery: "queue",
|
||||||
})
|
})
|
||||||
yield* session.interrupt(sessionID)
|
yield* session.interrupt(sessionID)
|
||||||
|
|
@ -2284,7 +2315,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
yield* Deferred.await(streamStarted)
|
yield* Deferred.await(streamStarted)
|
||||||
yield* session.prompt({
|
yield* session.prompt({
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Steer after interrupt" }),
|
text: "Steer after interrupt",
|
||||||
})
|
})
|
||||||
yield* session.interrupt(sessionID)
|
yield* session.interrupt(sessionID)
|
||||||
expect(yield* Fiber.await(run)).toMatchObject({ _tag: "Failure" })
|
expect(yield* Fiber.await(run)).toMatchObject({ _tag: "Failure" })
|
||||||
|
|
@ -2315,8 +2346,8 @@ describe("SessionRunnerLLM", () => {
|
||||||
|
|
||||||
const first = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
const first = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
||||||
yield* Deferred.await(streamStarted)
|
yield* Deferred.await(streamStarted)
|
||||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Queue first" }), delivery: "queue" })
|
yield* session.prompt({ sessionID, text: "Queue first", delivery: "queue" })
|
||||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Queue second" }), delivery: "queue" })
|
yield* session.prompt({ sessionID, text: "Queue second", delivery: "queue" })
|
||||||
yield* Deferred.succeed(streamGate, undefined)
|
yield* Deferred.succeed(streamGate, undefined)
|
||||||
yield* Fiber.join(first)
|
yield* Fiber.join(first)
|
||||||
streamGate = undefined
|
streamGate = undefined
|
||||||
|
|
@ -2335,7 +2366,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
yield* admit(session, "Start steering")
|
yield* admit(session, "Start steering")
|
||||||
yield* session.prompt({
|
yield* session.prompt({
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Queue for later" }),
|
text: "Queue for later",
|
||||||
delivery: "queue",
|
delivery: "queue",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
|
|
@ -2362,16 +2393,17 @@ describe("SessionRunnerLLM", () => {
|
||||||
|
|
||||||
const first = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
const first = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
||||||
while (requests.length < 1) yield* Effect.yieldNow
|
while (requests.length < 1) yield* Effect.yieldNow
|
||||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Queue first" }), delivery: "queue" })
|
yield* session.prompt({ sessionID, text: "Queue first", delivery: "queue" })
|
||||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Queue second" }), delivery: "queue" })
|
yield* session.prompt({ sessionID, text: "Queue second", delivery: "queue" })
|
||||||
streamGate = secondGate
|
streamGate = secondGate
|
||||||
yield* Deferred.succeed(firstGate, undefined)
|
yield* Deferred.succeed(firstGate, undefined)
|
||||||
while (requests.length < 2) yield* Effect.yieldNow
|
while (requests.length < 2) yield* Effect.yieldNow
|
||||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Steer before next queued input" }) })
|
yield* session.prompt({ sessionID, text: "Steer before next queued input" })
|
||||||
yield* session.prompt({
|
yield* session.prompt({
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Also steer before next queued input" }),
|
text: "Also steer before next queued input",
|
||||||
})
|
})
|
||||||
|
yield* session.synthetic({ sessionID, text: "Background completion before next queued input" })
|
||||||
yield* Deferred.succeed(secondGate, undefined)
|
yield* Deferred.succeed(secondGate, undefined)
|
||||||
yield* Fiber.join(first)
|
yield* Fiber.join(first)
|
||||||
streamGate = undefined
|
streamGate = undefined
|
||||||
|
|
@ -2384,12 +2416,14 @@ describe("SessionRunnerLLM", () => {
|
||||||
"Queue first",
|
"Queue first",
|
||||||
"Steer before next queued input",
|
"Steer before next queued input",
|
||||||
"Also steer before next queued input",
|
"Also steer before next queued input",
|
||||||
|
"Background completion before next queued input",
|
||||||
])
|
])
|
||||||
expect(userTexts(requests[3]!)).toEqual([
|
expect(userTexts(requests[3]!)).toEqual([
|
||||||
"Start working",
|
"Start working",
|
||||||
"Queue first",
|
"Queue first",
|
||||||
"Steer before next queued input",
|
"Steer before next queued input",
|
||||||
"Also steer before next queued input",
|
"Also steer before next queued input",
|
||||||
|
"Background completion before next queued input",
|
||||||
"Queue second",
|
"Queue second",
|
||||||
])
|
])
|
||||||
}),
|
}),
|
||||||
|
|
@ -2406,8 +2440,8 @@ describe("SessionRunnerLLM", () => {
|
||||||
|
|
||||||
const first = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
const first = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
||||||
yield* Deferred.await(streamStarted)
|
yield* Deferred.await(streamStarted)
|
||||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First steer" }) })
|
yield* session.prompt({ sessionID, text: "First steer" })
|
||||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Second steer" }) })
|
yield* session.prompt({ sessionID, text: "Second steer" })
|
||||||
yield* Deferred.succeed(streamGate, undefined)
|
yield* Deferred.succeed(streamGate, undefined)
|
||||||
yield* Fiber.join(first)
|
yield* Fiber.join(first)
|
||||||
streamGate = undefined
|
streamGate = undefined
|
||||||
|
|
@ -2433,7 +2467,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
|
|
||||||
const first = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
const first = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
||||||
yield* Deferred.await(streamStarted)
|
yield* Deferred.await(streamStarted)
|
||||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Recover with this" }) })
|
yield* session.prompt({ sessionID, text: "Recover with this" })
|
||||||
yield* Deferred.succeed(streamGate, undefined)
|
yield* Deferred.succeed(streamGate, undefined)
|
||||||
expect(yield* Fiber.join(first).pipe(Effect.flip)).toBe(streamFailure)
|
expect(yield* Fiber.join(first).pipe(Effect.flip)).toBe(streamFailure)
|
||||||
|
|
||||||
|
|
@ -2592,7 +2626,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
const session = yield* setup
|
const session = yield* setup
|
||||||
yield* session.prompt({
|
yield* session.prompt({
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Wait in queue" }),
|
text: "Wait in queue",
|
||||||
delivery: "queue",
|
delivery: "queue",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
|
|
@ -2611,7 +2645,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const defect = new Error("fail after prompt promotion")
|
const defect = new Error("fail after prompt promotion")
|
||||||
let fail = true
|
let fail = true
|
||||||
yield* events.project(SessionEvent.PromptPromoted, () => (fail ? Effect.die(defect) : Effect.void))
|
yield* events.project(SessionEvent.InputPromoted, () => (fail ? Effect.die(defect) : Effect.void))
|
||||||
yield* admit(session, "Recover promoted input")
|
yield* admit(session, "Recover promoted input")
|
||||||
|
|
||||||
expect(yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))).toBe(defect)
|
expect(yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))).toBe(defect)
|
||||||
|
|
@ -2631,7 +2665,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
const session = yield* setup
|
const session = yield* setup
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
yield* events.listen((event) =>
|
yield* events.listen((event) =>
|
||||||
event.type === SessionEvent.PromptPromoted.type
|
event.type === SessionEvent.InputPromoted.type
|
||||||
? Effect.die("fail after prompt promotion commits")
|
? Effect.die("fail after prompt promotion commits")
|
||||||
: Effect.void,
|
: Effect.void,
|
||||||
)
|
)
|
||||||
|
|
@ -2651,7 +2685,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
yield* admit(session, "Run first")
|
yield* admit(session, "Run first")
|
||||||
yield* session.prompt({
|
yield* session.prompt({
|
||||||
sessionID: otherSessionID,
|
sessionID: otherSessionID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Run second" }),
|
text: "Run second",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -2686,12 +2720,12 @@ describe("SessionRunnerLLM", () => {
|
||||||
yield* insertSession(otherLongSessionID)
|
yield* insertSession(otherLongSessionID)
|
||||||
yield* session.prompt({
|
yield* session.prompt({
|
||||||
sessionID: longSessionID,
|
sessionID: longSessionID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Run long session" }),
|
text: "Run long session",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
yield* session.prompt({
|
yield* session.prompt({
|
||||||
sessionID: otherLongSessionID,
|
sessionID: otherLongSessionID,
|
||||||
prompt: PromptInput.Prompt.make({ text: "Run other long session" }),
|
text: "Run other long session",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -3253,7 +3287,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
|
|
||||||
const run = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
const run = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
||||||
yield* Deferred.await(streamStarted)
|
yield* Deferred.await(streamStarted)
|
||||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Change direction" }) })
|
yield* session.prompt({ sessionID, text: "Change direction" })
|
||||||
yield* Deferred.succeed(streamGate, undefined)
|
yield* Deferred.succeed(streamGate, undefined)
|
||||||
yield* Fiber.join(run)
|
yield* Fiber.join(run)
|
||||||
streamGate = undefined
|
streamGate = undefined
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||||
import { Prompt } from "@opencode-ai/schema/prompt"
|
|
||||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||||
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
||||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||||
|
|
@ -84,13 +83,12 @@ const prompt = (sessionID: SessionV2.ID, text: string) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const messageID = SessionMessage.ID.create()
|
const messageID = SessionMessage.ID.create()
|
||||||
yield* events.publish(SessionEvent.PromptAdmitted, {
|
yield* events.publish(SessionEvent.InputAdmitted, {
|
||||||
sessionID,
|
sessionID,
|
||||||
inputID: messageID,
|
inputID: messageID,
|
||||||
prompt: Prompt.make({ text }),
|
input: { type: "user", data: { text }, delivery: "steer" },
|
||||||
delivery: "steer",
|
|
||||||
})
|
})
|
||||||
yield* events.publish(SessionEvent.PromptPromoted, {
|
yield* events.publish(SessionEvent.InputPromoted, {
|
||||||
sessionID,
|
sessionID,
|
||||||
inputID: messageID,
|
inputID: messageID,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,9 @@ test("Core reuses the canonical shared schemas", async () => {
|
||||||
[SessionV2.Info, Session.Info],
|
[SessionV2.Info, Session.Info],
|
||||||
[SessionV2.ListAnchor, Session.ListAnchor],
|
[SessionV2.ListAnchor, Session.ListAnchor],
|
||||||
[coreSessionInput.Delivery, SessionInput.Delivery],
|
[coreSessionInput.Delivery, SessionInput.Delivery],
|
||||||
[coreSessionInput.Admitted, SessionInput.Admitted],
|
[coreSessionInput.Message, SessionInput.Message],
|
||||||
|
[coreSessionInput.User, SessionInput.User],
|
||||||
|
[coreSessionInput.Synthetic, SessionInput.Synthetic],
|
||||||
[coreSessionMessage.ID, SessionMessage.ID],
|
[coreSessionMessage.ID, SessionMessage.ID],
|
||||||
[coreSessionMessage.AssistantRetry, SessionMessage.AssistantRetry],
|
[coreSessionMessage.AssistantRetry, SessionMessage.AssistantRetry],
|
||||||
[coreSessionMessage.AgentSelected, SessionMessage.AgentSelected],
|
[coreSessionMessage.AgentSelected, SessionMessage.AgentSelected],
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { DateTime, Effect, Layer, Schema } from "effect"
|
import { DateTime, Effect, Fiber, Layer, Schema, Stream } from "effect"
|
||||||
import { Money } from "@opencode-ai/schema/money"
|
import { Money } from "@opencode-ai/schema/money"
|
||||||
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"
|
||||||
|
|
@ -16,6 +16,7 @@ import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||||
|
import { SessionInput } from "@opencode-ai/core/session/input"
|
||||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||||
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
||||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||||
|
|
@ -263,6 +264,13 @@ describe("SubagentTool", () => {
|
||||||
const locations = yield* LocationServiceMap.Service
|
const locations = yield* LocationServiceMap.Service
|
||||||
const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locations.get(parent.location)))
|
const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locations.get(parent.location)))
|
||||||
yield* waitForTool(registry, SubagentTool.name)
|
yield* waitForTool(registry, SubagentTool.name)
|
||||||
|
const events = yield* EventV2.Service
|
||||||
|
const admitted = yield* events.subscribe(SessionEvent.InputAdmitted).pipe(
|
||||||
|
Stream.filter((event) => event.data.sessionID === parent.id && event.data.input.type === "synthetic"),
|
||||||
|
Stream.take(1),
|
||||||
|
Stream.runCollect,
|
||||||
|
Effect.forkScoped({ startImmediately: true }),
|
||||||
|
)
|
||||||
|
|
||||||
const settled = yield* settleTool(registry, {
|
const settled = yield* settleTool(registry, {
|
||||||
sessionID: parent.id,
|
sessionID: parent.id,
|
||||||
|
|
@ -277,7 +285,10 @@ describe("SubagentTool", () => {
|
||||||
const childID = outputSessionID(settled.output?.structured)
|
const childID = outputSessionID(settled.output?.structured)
|
||||||
expect(settled.output?.structured).toMatchObject({ status: "running" })
|
expect(settled.output?.structured).toMatchObject({ status: "running" })
|
||||||
|
|
||||||
yield* Effect.yieldNow
|
const admission = Array.from(yield* Fiber.join(admitted))[0]
|
||||||
|
expect(admission?.data.input.data.text).toContain(`<subagent id="${childID}" state="completed"`)
|
||||||
|
const database = yield* Database.Service
|
||||||
|
yield* SessionInput.promoteSteers(database.db, events, parent.id)
|
||||||
const synthetic = (yield* sessions.context(parent.id)).filter((message) => message.type === "synthetic")
|
const synthetic = (yield* sessions.context(parent.id)).filter((message) => message.type === "synthetic")
|
||||||
expect(synthetic).toHaveLength(1)
|
expect(synthetic).toHaveLength(1)
|
||||||
expect(synthetic[0]?.text).toContain(`<subagent id="${childID}" state="completed"`)
|
expect(synthetic[0]?.text).toContain(`<subagent id="${childID}" state="completed"`)
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ function prompted(inputID: string): V2Event {
|
||||||
return {
|
return {
|
||||||
id: "evt_prompted",
|
id: "evt_prompted",
|
||||||
created: 0,
|
created: 0,
|
||||||
type: "session.prompt.promoted",
|
type: "session.input.promoted",
|
||||||
durable: { aggregateID: "ses_1", seq: 0, version: 1 },
|
durable: { aggregateID: "ses_1", seq: 0, version: 1 },
|
||||||
data: { sessionID: "ses_1", inputID },
|
data: { sessionID: "ses_1", inputID },
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,23 @@ function durable(sessionID: string, seq = 0, version: 1 | 2 = 1) {
|
||||||
return { aggregateID: sessionID, seq, version }
|
return { aggregateID: sessionID, seq, version }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function promptAdmission(input: Parameters<OpenCodeClient["session"]["prompt"]>[0], sessionID = "ses_1") {
|
||||||
|
return {
|
||||||
|
admittedSeq: 1,
|
||||||
|
id: input.id ?? "msg_prompt",
|
||||||
|
sessionID,
|
||||||
|
type: "user" as const,
|
||||||
|
data: {
|
||||||
|
text: input.text,
|
||||||
|
files: input.files,
|
||||||
|
agents: input.agents,
|
||||||
|
metadata: input.metadata,
|
||||||
|
},
|
||||||
|
delivery: input.delivery ?? ("steer" as const),
|
||||||
|
timeCreated: 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function footer() {
|
function footer() {
|
||||||
const commits: StreamCommit[] = []
|
const commits: StreamCommit[] = []
|
||||||
const events: FooterEvent[] = []
|
const events: FooterEvent[] = []
|
||||||
|
|
@ -170,19 +187,8 @@ describe("V2 mini transport", () => {
|
||||||
|
|
||||||
let admitted = false
|
let admitted = false
|
||||||
spyOn(client.session, "prompt").mockImplementation((request) => {
|
spyOn(client.session, "prompt").mockImplementation((request) => {
|
||||||
const messageID = request.id ?? "msg_prompt"
|
|
||||||
const prompt = request.prompt ?? { text: "" }
|
|
||||||
admitted = true
|
admitted = true
|
||||||
return ok({
|
return ok({ data: promptAdmission(request) }) as never
|
||||||
data: {
|
|
||||||
admittedSeq: 1,
|
|
||||||
id: messageID,
|
|
||||||
sessionID: "ses_1",
|
|
||||||
prompt,
|
|
||||||
delivery: "steer" as const,
|
|
||||||
timeCreated: 2,
|
|
||||||
},
|
|
||||||
}) as never
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const turn = transport.runPromptTurn({
|
const turn = transport.runPromptTurn({
|
||||||
|
|
@ -197,7 +203,7 @@ describe("V2 mini transport", () => {
|
||||||
events.push({
|
events.push({
|
||||||
id: "evt_prompted",
|
id: "evt_prompted",
|
||||||
created: 0,
|
created: 0,
|
||||||
type: "session.prompt.promoted",
|
type: "session.input.promoted",
|
||||||
durable: durable("ses_1"),
|
durable: durable("ses_1"),
|
||||||
data: {
|
data: {
|
||||||
sessionID: "ses_1",
|
sessionID: "ses_1",
|
||||||
|
|
@ -255,7 +261,7 @@ describe("V2 mini transport", () => {
|
||||||
events.push({
|
events.push({
|
||||||
id: "evt_prompted",
|
id: "evt_prompted",
|
||||||
created: 0,
|
created: 0,
|
||||||
type: "session.prompt.promoted",
|
type: "session.input.promoted",
|
||||||
durable: durable("ses_1"),
|
durable: durable("ses_1"),
|
||||||
data: {
|
data: {
|
||||||
sessionID: "ses_1",
|
sessionID: "ses_1",
|
||||||
|
|
@ -270,16 +276,7 @@ describe("V2 mini transport", () => {
|
||||||
data: { sessionID: "ses_1" },
|
data: { sessionID: "ses_1" },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return ok({
|
return ok({ data: promptAdmission(input) }) as never
|
||||||
data: {
|
|
||||||
admittedSeq: 1,
|
|
||||||
id: input.id ?? "msg_prompt",
|
|
||||||
sessionID: "ses_1",
|
|
||||||
prompt: input.prompt ?? { text: "" },
|
|
||||||
delivery: "steer" as const,
|
|
||||||
timeCreated: 2,
|
|
||||||
},
|
|
||||||
}) as never
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await transport.runPromptTurn({
|
await transport.runPromptTurn({
|
||||||
|
|
@ -310,8 +307,8 @@ describe("V2 mini transport", () => {
|
||||||
includeFiles: true,
|
includeFiles: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(request?.prompt?.text).toBe("Review @note.ts and @docs")
|
expect(request?.text).toBe("Review @note.ts and @docs")
|
||||||
expect(request?.prompt?.files).toEqual([
|
expect(request?.files).toEqual([
|
||||||
{
|
{
|
||||||
uri: pathToFileURL(filePath).href,
|
uri: pathToFileURL(filePath).href,
|
||||||
name: "note.ts",
|
name: "note.ts",
|
||||||
|
|
@ -350,7 +347,7 @@ describe("V2 mini transport", () => {
|
||||||
events.push({
|
events.push({
|
||||||
id: "evt_prompted",
|
id: "evt_prompted",
|
||||||
created: 0,
|
created: 0,
|
||||||
type: "session.prompt.promoted",
|
type: "session.input.promoted",
|
||||||
durable: durable("ses_1"),
|
durable: durable("ses_1"),
|
||||||
data: {
|
data: {
|
||||||
sessionID: "ses_1",
|
sessionID: "ses_1",
|
||||||
|
|
@ -365,16 +362,7 @@ describe("V2 mini transport", () => {
|
||||||
data: { sessionID: "ses_1" },
|
data: { sessionID: "ses_1" },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return ok({
|
return ok({ data: promptAdmission(input) })
|
||||||
data: {
|
|
||||||
admittedSeq: 1,
|
|
||||||
id: input.id ?? "msg_prompt",
|
|
||||||
sessionID: "ses_1",
|
|
||||||
prompt: input.prompt ?? { text: "" },
|
|
||||||
delivery: "steer" as const,
|
|
||||||
timeCreated: 2,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await transport.runPromptTurn({
|
await transport.runPromptTurn({
|
||||||
|
|
@ -407,8 +395,8 @@ describe("V2 mini transport", () => {
|
||||||
|
|
||||||
expect(remoteRead).not.toHaveBeenCalled()
|
expect(remoteRead).not.toHaveBeenCalled()
|
||||||
expect(remoteList).not.toHaveBeenCalled()
|
expect(remoteList).not.toHaveBeenCalled()
|
||||||
expect(request?.prompt?.text).toBe("Review @note.ts and @docs")
|
expect(request?.text).toBe("Review @note.ts and @docs")
|
||||||
expect(request?.prompt?.files).toEqual([
|
expect(request?.files).toEqual([
|
||||||
{
|
{
|
||||||
uri: "file:///remote/project/note.ts",
|
uri: "file:///remote/project/note.ts",
|
||||||
name: "note.ts",
|
name: "note.ts",
|
||||||
|
|
@ -448,7 +436,7 @@ describe("V2 mini transport", () => {
|
||||||
events.push({
|
events.push({
|
||||||
id: "evt_prompted",
|
id: "evt_prompted",
|
||||||
created: 0,
|
created: 0,
|
||||||
type: "session.prompt.promoted",
|
type: "session.input.promoted",
|
||||||
durable: durable("ses_1"),
|
durable: durable("ses_1"),
|
||||||
data: {
|
data: {
|
||||||
sessionID: "ses_1",
|
sessionID: "ses_1",
|
||||||
|
|
@ -463,16 +451,7 @@ describe("V2 mini transport", () => {
|
||||||
data: { sessionID: "ses_1" },
|
data: { sessionID: "ses_1" },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return ok({
|
return ok({ data: promptAdmission(input) })
|
||||||
data: {
|
|
||||||
admittedSeq: 1,
|
|
||||||
id: input.id ?? "msg_prompt",
|
|
||||||
sessionID: "ses_1",
|
|
||||||
prompt: input.prompt ?? { text: "" },
|
|
||||||
delivery: "steer" as const,
|
|
||||||
timeCreated: 2,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await transport.runPromptTurn({
|
await transport.runPromptTurn({
|
||||||
|
|
@ -496,8 +475,8 @@ describe("V2 mini transport", () => {
|
||||||
includeFiles: true,
|
includeFiles: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(request?.prompt?.text).toBe("Review @diagram.png")
|
expect(request?.text).toBe("Review @diagram.png")
|
||||||
expect(request?.prompt?.files).toEqual([
|
expect(request?.files).toEqual([
|
||||||
{
|
{
|
||||||
name: "diagram.png",
|
name: "diagram.png",
|
||||||
uri: pathToFileURL(filePath).href,
|
uri: pathToFileURL(filePath).href,
|
||||||
|
|
@ -589,12 +568,8 @@ describe("V2 mini transport", () => {
|
||||||
// The generated method has conditional return types for throwOnError; this mock represents the successful branch.
|
// The generated method has conditional return types for throwOnError; this mock represents the successful branch.
|
||||||
// @ts-expect-error successful SDK response is valid for both modes at runtime
|
// @ts-expect-error successful SDK response is valid for both modes at runtime
|
||||||
spyOn(client.session, "prompt").mockImplementation((request) => {
|
spyOn(client.session, "prompt").mockImplementation((request) => {
|
||||||
const messageID = request.id ?? "msg_prompt"
|
|
||||||
const prompt = request.prompt ?? { text: "" }
|
|
||||||
admitted = true
|
admitted = true
|
||||||
return ok({
|
return ok({ data: promptAdmission(request) })
|
||||||
data: { admittedSeq: 1, id: messageID, sessionID: "ses_1", prompt, delivery: "steer" as const, timeCreated: 2 },
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const turn = transport.runPromptTurn({
|
const turn = transport.runPromptTurn({
|
||||||
|
|
@ -661,12 +636,8 @@ describe("V2 mini transport", () => {
|
||||||
// The generated method has conditional return types for throwOnError; this mock represents the successful branch.
|
// The generated method has conditional return types for throwOnError; this mock represents the successful branch.
|
||||||
// @ts-expect-error successful SDK response is valid for both modes at runtime
|
// @ts-expect-error successful SDK response is valid for both modes at runtime
|
||||||
spyOn(client.session, "prompt").mockImplementation((request) => {
|
spyOn(client.session, "prompt").mockImplementation((request) => {
|
||||||
const messageID = request.id ?? "msg_prompt"
|
|
||||||
const prompt = request.prompt ?? { text: "" }
|
|
||||||
admitted = true
|
admitted = true
|
||||||
return ok({
|
return ok({ data: promptAdmission(request) })
|
||||||
data: { admittedSeq: 1, id: messageID, sessionID: "ses_1", prompt, delivery: "steer" as const, timeCreated: 2 },
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const turn = transport.runPromptTurn({
|
const turn = transport.runPromptTurn({
|
||||||
|
|
@ -899,12 +870,8 @@ describe("V2 mini transport", () => {
|
||||||
// The generated method has conditional return types for throwOnError; this mock represents the successful branch.
|
// The generated method has conditional return types for throwOnError; this mock represents the successful branch.
|
||||||
// @ts-expect-error successful SDK response is valid for both modes at runtime
|
// @ts-expect-error successful SDK response is valid for both modes at runtime
|
||||||
spyOn(client.session, "prompt").mockImplementation((request) => {
|
spyOn(client.session, "prompt").mockImplementation((request) => {
|
||||||
const messageID = request.id ?? "msg_prompt"
|
|
||||||
const prompt = request.prompt ?? { text: "" }
|
|
||||||
admitted = true
|
admitted = true
|
||||||
return ok({
|
return ok({ data: promptAdmission(request) })
|
||||||
data: { admittedSeq: 1, id: messageID, sessionID: "ses_1", prompt, delivery: "steer" as const, timeCreated: 2 },
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
const interrupted = spyOn(client.session, "interrupt").mockImplementation(() => ok(undefined))
|
const interrupted = spyOn(client.session, "interrupt").mockImplementation(() => ok(undefined))
|
||||||
|
|
||||||
|
|
@ -956,12 +923,8 @@ describe("V2 mini transport", () => {
|
||||||
// The generated method has conditional return types for throwOnError; this mock represents the successful branch.
|
// The generated method has conditional return types for throwOnError; this mock represents the successful branch.
|
||||||
// @ts-expect-error successful SDK response is valid for both modes at runtime
|
// @ts-expect-error successful SDK response is valid for both modes at runtime
|
||||||
spyOn(client.session, "prompt").mockImplementation((request) => {
|
spyOn(client.session, "prompt").mockImplementation((request) => {
|
||||||
const messageID = request.id ?? "msg_prompt"
|
|
||||||
const prompt = request.prompt ?? { text: "" }
|
|
||||||
admitted = true
|
admitted = true
|
||||||
return ok({
|
return ok({ data: promptAdmission(request) })
|
||||||
data: { admittedSeq: 1, id: messageID, sessionID: "ses_1", prompt, delivery: "steer" as const, timeCreated: 2 },
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const turn = transport.runPromptTurn({
|
const turn = transport.runPromptTurn({
|
||||||
|
|
@ -976,7 +939,7 @@ describe("V2 mini transport", () => {
|
||||||
events.push({
|
events.push({
|
||||||
id: "evt_prompted",
|
id: "evt_prompted",
|
||||||
created: 0,
|
created: 0,
|
||||||
type: "session.prompt.promoted",
|
type: "session.input.promoted",
|
||||||
durable: durable("ses_1"),
|
durable: durable("ses_1"),
|
||||||
data: {
|
data: {
|
||||||
sessionID: "ses_1",
|
sessionID: "ses_1",
|
||||||
|
|
@ -1015,12 +978,8 @@ describe("V2 mini transport", () => {
|
||||||
// The generated method has conditional return types for throwOnError; this mock represents the successful branch.
|
// The generated method has conditional return types for throwOnError; this mock represents the successful branch.
|
||||||
// @ts-expect-error successful SDK response is valid for both modes at runtime
|
// @ts-expect-error successful SDK response is valid for both modes at runtime
|
||||||
spyOn(client.session, "prompt").mockImplementation((request) => {
|
spyOn(client.session, "prompt").mockImplementation((request) => {
|
||||||
const messageID = request.id ?? "msg_prompt"
|
|
||||||
const prompt = request.prompt ?? { text: "" }
|
|
||||||
admitted = true
|
admitted = true
|
||||||
return ok({
|
return ok({ data: promptAdmission(request) })
|
||||||
data: { admittedSeq: 1, id: messageID, sessionID: "ses_1", prompt, delivery: "steer" as const, timeCreated: 2 },
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
const interrupted = spyOn(client.session, "interrupt").mockImplementation(() => ok(undefined))
|
const interrupted = spyOn(client.session, "interrupt").mockImplementation(() => ok(undefined))
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
|
|
@ -1037,7 +996,7 @@ describe("V2 mini transport", () => {
|
||||||
events.push({
|
events.push({
|
||||||
id: "evt_prompted",
|
id: "evt_prompted",
|
||||||
created: 0,
|
created: 0,
|
||||||
type: "session.prompt.promoted",
|
type: "session.input.promoted",
|
||||||
durable: durable("ses_1"),
|
durable: durable("ses_1"),
|
||||||
data: {
|
data: {
|
||||||
sessionID: "ses_1",
|
sessionID: "ses_1",
|
||||||
|
|
@ -1464,7 +1423,7 @@ describe("V2 mini transport", () => {
|
||||||
events.push({
|
events.push({
|
||||||
id: "evt_prompted",
|
id: "evt_prompted",
|
||||||
created: 0,
|
created: 0,
|
||||||
type: "session.prompt.promoted",
|
type: "session.input.promoted",
|
||||||
durable: durable("ses_1"),
|
durable: durable("ses_1"),
|
||||||
data: {
|
data: {
|
||||||
sessionID: "ses_1",
|
sessionID: "ses_1",
|
||||||
|
|
@ -1483,7 +1442,8 @@ describe("V2 mini transport", () => {
|
||||||
admittedSeq: 1,
|
admittedSeq: 1,
|
||||||
id: input.id ?? "msg_cmd",
|
id: input.id ?? "msg_cmd",
|
||||||
sessionID: "ses_1",
|
sessionID: "ses_1",
|
||||||
prompt: { text: "evaluated template" },
|
type: "user" as const,
|
||||||
|
data: { text: "evaluated template" },
|
||||||
delivery: "steer" as const,
|
delivery: "steer" as const,
|
||||||
timeCreated: 2,
|
timeCreated: 2,
|
||||||
})
|
})
|
||||||
|
|
@ -1862,13 +1822,12 @@ describe("V2 mini transport", () => {
|
||||||
events.push({
|
events.push({
|
||||||
id: "evt_child_admitted",
|
id: "evt_child_admitted",
|
||||||
created: 1,
|
created: 1,
|
||||||
type: "session.prompt.admitted",
|
type: "session.input.admitted",
|
||||||
durable: durable("ses_child"),
|
durable: durable("ses_child"),
|
||||||
data: {
|
data: {
|
||||||
sessionID: "ses_child",
|
sessionID: "ses_child",
|
||||||
inputID: "msg_child_prompt",
|
inputID: "msg_child_prompt",
|
||||||
prompt: { text: "actual child prompt" },
|
input: { type: "user", data: { text: "actual child prompt" }, delivery: "steer" },
|
||||||
delivery: "steer",
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await Bun.sleep(0)
|
await Bun.sleep(0)
|
||||||
|
|
@ -1881,7 +1840,7 @@ describe("V2 mini transport", () => {
|
||||||
events.push({
|
events.push({
|
||||||
id: "evt_child_promoted",
|
id: "evt_child_promoted",
|
||||||
created: 2,
|
created: 2,
|
||||||
type: "session.prompt.promoted",
|
type: "session.input.promoted",
|
||||||
durable: durable("ses_child", 1),
|
durable: durable("ses_child", 1),
|
||||||
data: { sessionID: "ses_child", inputID: "msg_child_prompt" },
|
data: { sessionID: "ses_child", inputID: "msg_child_prompt" },
|
||||||
})
|
})
|
||||||
|
|
@ -1928,13 +1887,12 @@ describe("V2 mini transport", () => {
|
||||||
events.push({
|
events.push({
|
||||||
id: "evt_child_admitted_race",
|
id: "evt_child_admitted_race",
|
||||||
created: 1,
|
created: 1,
|
||||||
type: "session.prompt.admitted",
|
type: "session.input.admitted",
|
||||||
durable: durable("ses_child"),
|
durable: durable("ses_child"),
|
||||||
data: {
|
data: {
|
||||||
sessionID: "ses_child",
|
sessionID: "ses_child",
|
||||||
inputID: "msg_child_race",
|
inputID: "msg_child_race",
|
||||||
prompt: { text: "prompt admitted before hydration" },
|
input: { type: "user", data: { text: "prompt admitted before hydration" }, delivery: "steer" },
|
||||||
delivery: "steer",
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await Bun.sleep(0)
|
await Bun.sleep(0)
|
||||||
|
|
@ -1943,7 +1901,7 @@ describe("V2 mini transport", () => {
|
||||||
events.push({
|
events.push({
|
||||||
id: "evt_child_promoted_race",
|
id: "evt_child_promoted_race",
|
||||||
created: 2,
|
created: 2,
|
||||||
type: "session.prompt.promoted",
|
type: "session.input.promoted",
|
||||||
durable: durable("ses_child", 1),
|
durable: durable("ses_child", 1),
|
||||||
data: { sessionID: "ses_child", inputID: "msg_child_race" },
|
data: { sessionID: "ses_child", inputID: "msg_child_race" },
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -293,11 +293,12 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||||
params: { sessionID: Session.ID },
|
params: { sessionID: Session.ID },
|
||||||
payload: Schema.Struct({
|
payload: Schema.Struct({
|
||||||
id: SessionMessage.ID.pipe(Schema.optional),
|
id: SessionMessage.ID.pipe(Schema.optional),
|
||||||
prompt: PromptInput.Prompt,
|
...PromptInput.Prompt.fields,
|
||||||
|
metadata: SessionInput.UserData.fields.metadata,
|
||||||
delivery: SessionInput.Delivery.pipe(Schema.optional),
|
delivery: SessionInput.Delivery.pipe(Schema.optional),
|
||||||
resume: Schema.Boolean.pipe(Schema.optional),
|
resume: Schema.Boolean.pipe(Schema.optional),
|
||||||
}),
|
}),
|
||||||
success: Schema.Struct({ data: SessionInput.Admitted }),
|
success: Schema.Struct({ data: SessionInput.User }),
|
||||||
error: [ConflictError, InvalidRequestError, SessionNotFoundError],
|
error: [ConflictError, InvalidRequestError, SessionNotFoundError],
|
||||||
})
|
})
|
||||||
.middleware(sessionLocationMiddleware)
|
.middleware(sessionLocationMiddleware)
|
||||||
|
|
@ -323,7 +324,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||||
delivery: SessionInput.Delivery.pipe(Schema.optional),
|
delivery: SessionInput.Delivery.pipe(Schema.optional),
|
||||||
resume: Schema.Boolean.pipe(Schema.optional),
|
resume: Schema.Boolean.pipe(Schema.optional),
|
||||||
}),
|
}),
|
||||||
success: Schema.Struct({ data: SessionInput.Admitted }),
|
success: Schema.Struct({ data: SessionInput.User }),
|
||||||
error: [ConflictError, InvalidRequestError, SessionNotFoundError, CommandNotFoundError, CommandEvaluationError],
|
error: [ConflictError, InvalidRequestError, SessionNotFoundError, CommandNotFoundError, CommandEvaluationError],
|
||||||
})
|
})
|
||||||
.middleware(sessionLocationMiddleware)
|
.middleware(sessionLocationMiddleware)
|
||||||
|
|
@ -360,20 +361,22 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||||
HttpApiEndpoint.post("session.synthetic", "/api/session/:sessionID/synthetic", {
|
HttpApiEndpoint.post("session.synthetic", "/api/session/:sessionID/synthetic", {
|
||||||
params: { sessionID: Session.ID },
|
params: { sessionID: Session.ID },
|
||||||
payload: Schema.Struct({
|
payload: Schema.Struct({
|
||||||
|
id: SessionMessage.ID.pipe(Schema.optional),
|
||||||
text: Schema.String,
|
text: Schema.String,
|
||||||
description: Schema.String.pipe(Schema.optional),
|
description: Schema.String.pipe(Schema.optional),
|
||||||
metadata: SessionMessage.Synthetic.fields.metadata,
|
metadata: SessionMessage.Synthetic.fields.metadata,
|
||||||
|
delivery: SessionInput.Delivery.pipe(Schema.optional),
|
||||||
resume: Schema.Boolean.pipe(Schema.optional),
|
resume: Schema.Boolean.pipe(Schema.optional),
|
||||||
}),
|
}),
|
||||||
success: HttpApiSchema.NoContent,
|
success: Schema.Struct({ data: SessionInput.Synthetic }),
|
||||||
error: SessionNotFoundError,
|
error: [ConflictError, SessionNotFoundError],
|
||||||
})
|
})
|
||||||
.middleware(sessionLocationMiddleware)
|
.middleware(sessionLocationMiddleware)
|
||||||
.annotateMerge(
|
.annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "v2.session.synthetic",
|
identifier: "v2.session.synthetic",
|
||||||
summary: "Add synthetic message",
|
summary: "Add synthetic message",
|
||||||
description: "Append a synthetic message to a session and resume execution.",
|
description: "Durably admit synthetic session input and schedule execution unless resume is false.",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,9 @@ import { optional } from "./schema.js"
|
||||||
import { Event } from "./event.js"
|
import { Event } from "./event.js"
|
||||||
import { ToolContent } from "./llm.js"
|
import { ToolContent } from "./llm.js"
|
||||||
import { FinishReason } from "./llm.js"
|
import { FinishReason } from "./llm.js"
|
||||||
import { Delivery } from "./session-delivery.js"
|
|
||||||
import { Model } from "./model.js"
|
import { Model } from "./model.js"
|
||||||
import { NonNegativeInt, PositiveInt, RelativePath } from "./schema.js"
|
import { NonNegativeInt, PositiveInt, RelativePath } from "./schema.js"
|
||||||
import { FileAttachment, Prompt } from "./prompt.js"
|
import { FileAttachment } from "./prompt.js"
|
||||||
import { SessionID } from "./session-id.js"
|
import { SessionID } from "./session-id.js"
|
||||||
import { Location } from "./location.js"
|
import { Location } from "./location.js"
|
||||||
import { SessionMessage } from "./session-message.js"
|
import { SessionMessage } from "./session-message.js"
|
||||||
|
|
@ -20,6 +19,7 @@ import { Skill as SkillSchema } from "./skill.js"
|
||||||
import { Money } from "./money.js"
|
import { Money } from "./money.js"
|
||||||
import { Snapshot } from "./snapshot.js"
|
import { Snapshot } from "./snapshot.js"
|
||||||
import { TokenUsage } from "./token-usage.js"
|
import { TokenUsage } from "./token-usage.js"
|
||||||
|
import { SessionInput } from "./session-input.js"
|
||||||
|
|
||||||
export { FileAttachment }
|
export { FileAttachment }
|
||||||
|
|
||||||
|
|
@ -35,12 +35,6 @@ export interface Source extends Schema.Schema.Type<typeof Source> {}
|
||||||
const Base = {
|
const Base = {
|
||||||
sessionID: SessionID,
|
sessionID: SessionID,
|
||||||
}
|
}
|
||||||
const PromptFields = {
|
|
||||||
...Base,
|
|
||||||
inputID: SessionMessage.ID,
|
|
||||||
prompt: Prompt,
|
|
||||||
delivery: Delivery,
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
durable: {
|
durable: {
|
||||||
|
|
@ -120,22 +114,26 @@ export const Forked = Event.durable({
|
||||||
})
|
})
|
||||||
export type Forked = typeof Forked.Type
|
export type Forked = typeof Forked.Type
|
||||||
|
|
||||||
export const PromptPromoted = Event.durable({
|
export const InputPromoted = Event.durable({
|
||||||
type: "session.prompt.promoted",
|
type: "session.input.promoted",
|
||||||
...options,
|
...options,
|
||||||
schema: {
|
schema: {
|
||||||
sessionID: SessionID,
|
sessionID: SessionID,
|
||||||
inputID: SessionMessage.ID,
|
inputID: SessionMessage.ID,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export type PromptPromoted = typeof PromptPromoted.Type
|
export type InputPromoted = typeof InputPromoted.Type
|
||||||
|
|
||||||
export const PromptAdmitted = Event.durable({
|
export const InputAdmitted = Event.durable({
|
||||||
type: "session.prompt.admitted",
|
type: "session.input.admitted",
|
||||||
...options,
|
...options,
|
||||||
schema: PromptFields,
|
schema: {
|
||||||
|
...Base,
|
||||||
|
inputID: SessionMessage.ID,
|
||||||
|
input: SessionInput.Message,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export type PromptAdmitted = typeof PromptAdmitted.Type
|
export type InputAdmitted = typeof InputAdmitted.Type
|
||||||
|
|
||||||
export namespace Execution {
|
export namespace Execution {
|
||||||
export const Started = Event.durable({ type: "session.execution.started", ...options, schema: Base })
|
export const Started = Event.durable({ type: "session.execution.started", ...options, schema: Base })
|
||||||
|
|
@ -523,8 +521,8 @@ export const Definitions = Event.inventory(
|
||||||
UsageUpdated,
|
UsageUpdated,
|
||||||
Deleted,
|
Deleted,
|
||||||
Forked,
|
Forked,
|
||||||
PromptPromoted,
|
InputPromoted,
|
||||||
PromptAdmitted,
|
InputAdmitted,
|
||||||
Execution.Started,
|
Execution.Started,
|
||||||
Execution.Succeeded,
|
Execution.Succeeded,
|
||||||
Execution.Failed,
|
Execution.Failed,
|
||||||
|
|
|
||||||
|
|
@ -11,34 +11,70 @@ import { SessionMessage } from "./session-message.js"
|
||||||
export const Delivery = SessionDelivery.Delivery
|
export const Delivery = SessionDelivery.Delivery
|
||||||
export type Delivery = SessionDelivery.Delivery
|
export type Delivery = SessionDelivery.Delivery
|
||||||
|
|
||||||
export interface Admitted extends Schema.Schema.Type<typeof Admitted> {}
|
export interface UserData extends Schema.Schema.Type<typeof UserData> {}
|
||||||
export const Admitted = Schema.Struct({
|
export const UserData = Schema.Struct({
|
||||||
|
...Prompt.fields,
|
||||||
|
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(optional),
|
||||||
|
}).annotate({ identifier: "SessionInput.UserData" })
|
||||||
|
|
||||||
|
export interface SyntheticData extends Schema.Schema.Type<typeof SyntheticData> {}
|
||||||
|
export const SyntheticData = Schema.Struct({
|
||||||
|
text: Schema.String,
|
||||||
|
description: Schema.String.pipe(optional),
|
||||||
|
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(optional),
|
||||||
|
}).annotate({ identifier: "SessionInput.SyntheticData" })
|
||||||
|
|
||||||
|
export interface UserMessage extends Schema.Schema.Type<typeof UserMessage> {}
|
||||||
|
export const UserMessage = Schema.Struct({
|
||||||
|
type: Schema.tag("user"),
|
||||||
|
data: UserData,
|
||||||
|
delivery: Delivery,
|
||||||
|
}).annotate({ identifier: "SessionInput.UserMessage" })
|
||||||
|
|
||||||
|
export interface SyntheticMessage extends Schema.Schema.Type<typeof SyntheticMessage> {}
|
||||||
|
export const SyntheticMessage = Schema.Struct({
|
||||||
|
type: Schema.tag("synthetic"),
|
||||||
|
data: SyntheticData,
|
||||||
|
delivery: Delivery,
|
||||||
|
}).annotate({ identifier: "SessionInput.SyntheticMessage" })
|
||||||
|
|
||||||
|
export const Message = Schema.Union([UserMessage, SyntheticMessage]).pipe(
|
||||||
|
Schema.toTaggedUnion("type"),
|
||||||
|
Schema.annotate({ identifier: "SessionInput.Message" }),
|
||||||
|
)
|
||||||
|
export type Message = typeof Message.Type
|
||||||
|
|
||||||
|
const Admitted = {
|
||||||
admittedSeq: NonNegativeInt,
|
admittedSeq: NonNegativeInt,
|
||||||
id: SessionMessage.ID,
|
id: SessionMessage.ID,
|
||||||
sessionID: SessionID,
|
sessionID: SessionID,
|
||||||
prompt: Prompt,
|
|
||||||
delivery: Delivery,
|
|
||||||
timeCreated: DateTimeUtcFromMillis,
|
timeCreated: DateTimeUtcFromMillis,
|
||||||
|
}
|
||||||
|
const MessageLifecycle = {
|
||||||
|
...Admitted,
|
||||||
promotedSeq: NonNegativeInt.pipe(optional),
|
promotedSeq: NonNegativeInt.pipe(optional),
|
||||||
}).annotate({ identifier: "SessionInput.Admitted" })
|
}
|
||||||
|
|
||||||
export interface PromptEntry extends Schema.Schema.Type<typeof PromptEntry> {}
|
export interface User extends Schema.Schema.Type<typeof User> {}
|
||||||
export const PromptEntry = Schema.Struct({
|
export const User = Schema.Struct({
|
||||||
type: Schema.tag("prompt"),
|
...MessageLifecycle,
|
||||||
...Admitted.fields,
|
...UserMessage.fields,
|
||||||
}).annotate({ identifier: "SessionInput.PromptEntry" })
|
}).annotate({ identifier: "SessionInput.User" })
|
||||||
|
|
||||||
|
export interface Synthetic extends Schema.Schema.Type<typeof Synthetic> {}
|
||||||
|
export const Synthetic = Schema.Struct({
|
||||||
|
...MessageLifecycle,
|
||||||
|
...SyntheticMessage.fields,
|
||||||
|
}).annotate({ identifier: "SessionInput.Synthetic" })
|
||||||
|
|
||||||
export interface Compaction extends Schema.Schema.Type<typeof Compaction> {}
|
export interface Compaction extends Schema.Schema.Type<typeof Compaction> {}
|
||||||
export const Compaction = Schema.Struct({
|
export const Compaction = Schema.Struct({
|
||||||
|
...Admitted,
|
||||||
type: Schema.tag("compaction"),
|
type: Schema.tag("compaction"),
|
||||||
admittedSeq: NonNegativeInt,
|
|
||||||
id: SessionMessage.ID,
|
|
||||||
sessionID: SessionID,
|
|
||||||
timeCreated: DateTimeUtcFromMillis,
|
|
||||||
handledSeq: NonNegativeInt.pipe(optional),
|
handledSeq: NonNegativeInt.pipe(optional),
|
||||||
}).annotate({ identifier: "SessionInput.Compaction" })
|
}).annotate({ identifier: "SessionInput.Compaction" })
|
||||||
|
|
||||||
export const Info = Schema.Union([PromptEntry, Compaction]).pipe(
|
export const Info = Schema.Union([User, Synthetic, Compaction]).pipe(
|
||||||
Schema.toTaggedUnion("type"),
|
Schema.toTaggedUnion("type"),
|
||||||
Schema.annotate({ identifier: "SessionInput.Info" }),
|
Schema.annotate({ identifier: "SessionInput.Info" }),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,13 @@ describe("contract hygiene", () => {
|
||||||
expect(Schema.decodeUnknownSync(Value)({ value: "1" })).toEqual({ value: 1 })
|
expect(Schema.decodeUnknownSync(Value)({ value: "1" })).toEqual({ value: 1 })
|
||||||
expect(Schema.encodeSync(Value)({ value: 1 })).toEqual({ value: "1" })
|
expect(Schema.encodeSync(Value)({ value: 1 })).toEqual({ value: "1" })
|
||||||
expect(Schema.encodeSync(Value)({ value: undefined })).toEqual({})
|
expect(Schema.encodeSync(Value)({ value: undefined })).toEqual({})
|
||||||
|
expect(
|
||||||
|
Schema.encodeSync(SessionInput.SyntheticData)({
|
||||||
|
text: "completed",
|
||||||
|
description: undefined,
|
||||||
|
metadata: undefined,
|
||||||
|
}),
|
||||||
|
).toEqual({ text: "completed" })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("model defaults and provider overlays preserve public invariants", () => {
|
test("model defaults and provider overlays preserve public invariants", () => {
|
||||||
|
|
@ -92,6 +99,10 @@ describe("contract hygiene", () => {
|
||||||
Pty.Info,
|
Pty.Info,
|
||||||
Session.ListAnchor,
|
Session.ListAnchor,
|
||||||
Session.Revert,
|
Session.Revert,
|
||||||
|
SessionInput.UserData,
|
||||||
|
SessionInput.SyntheticData,
|
||||||
|
SessionInput.User,
|
||||||
|
SessionInput.Synthetic,
|
||||||
].map((schema) => schema.ast.annotations?.identifier)
|
].map((schema) => schema.ast.annotations?.identifier)
|
||||||
|
|
||||||
expect(identifiers.every((identifier) => typeof identifier === "string")).toBe(true)
|
expect(identifiers.every((identifier) => typeof identifier === "string")).toBe(true)
|
||||||
|
|
|
||||||
|
|
@ -107,8 +107,8 @@ describe("public event manifest", () => {
|
||||||
"session.moved.1",
|
"session.moved.1",
|
||||||
"session.renamed.1",
|
"session.renamed.1",
|
||||||
"session.forked.1",
|
"session.forked.1",
|
||||||
"session.prompt.promoted.1",
|
"session.input.promoted.1",
|
||||||
"session.prompt.admitted.1",
|
"session.input.admitted.1",
|
||||||
"session.execution.started.1",
|
"session.execution.started.1",
|
||||||
"session.execution.succeeded.1",
|
"session.execution.succeeded.1",
|
||||||
"session.execution.failed.1",
|
"session.execution.failed.1",
|
||||||
|
|
|
||||||
|
|
@ -225,7 +225,7 @@ it.live(
|
||||||
const active = yield* opencode.sessions.active()
|
const active = yield* opencode.sessions.active()
|
||||||
const admitted = yield* opencode.sessions.prompt({
|
const admitted = yield* opencode.sessions.prompt({
|
||||||
sessionID: id,
|
sessionID: id,
|
||||||
prompt: fixture.sdk.PromptInput.Prompt.make({ text: "Do not run" }),
|
text: "Do not run",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
const context = yield* opencode.sessions.context({ sessionID: id })
|
const context = yield* opencode.sessions.context({ sessionID: id })
|
||||||
|
|
@ -236,10 +236,10 @@ it.live(
|
||||||
const remainingContextEntries = yield* opencode.sessions.instructions.entry.list({ sessionID: id })
|
const remainingContextEntries = yield* opencode.sessions.instructions.entry.list({ sessionID: id })
|
||||||
const wake = yield* opencode.sessions.prompt({
|
const wake = yield* opencode.sessions.prompt({
|
||||||
sessionID: id,
|
sessionID: id,
|
||||||
prompt: fixture.sdk.PromptInput.Prompt.make({ text: "Promote this input" }),
|
text: "Promote this input",
|
||||||
})
|
})
|
||||||
const prompted = yield* opencode.sessions.log({ sessionID: id, follow: true }).pipe(
|
const prompted = yield* opencode.sessions.log({ sessionID: id, follow: true }).pipe(
|
||||||
Stream.filter((event) => event.type === "session.prompt.promoted" && event.data.inputID === wake.id),
|
Stream.filter((event) => event.type === "session.input.promoted" && event.data.inputID === wake.id),
|
||||||
Stream.runHead,
|
Stream.runHead,
|
||||||
Effect.timeout("10 seconds"),
|
Effect.timeout("10 seconds"),
|
||||||
Effect.map(Option.getOrThrow),
|
Effect.map(Option.getOrThrow),
|
||||||
|
|
@ -280,7 +280,7 @@ it.live(
|
||||||
expect(page.data.some((session) => session.id === id)).toBe(true)
|
expect(page.data.some((session) => session.id === id)).toBe(true)
|
||||||
expect(active).toEqual({})
|
expect(active).toEqual({})
|
||||||
expect(admitted.sessionID).toBe(id)
|
expect(admitted.sessionID).toBe(id)
|
||||||
expect(prompted.type).toBe("session.prompt.promoted")
|
expect(prompted.type).toBe("session.input.promoted")
|
||||||
expect(wakeContext).toContainEqual(expect.objectContaining({ id: wake.id, type: "user" }))
|
expect(wakeContext).toContainEqual(expect.objectContaining({ id: wake.id, type: "user" }))
|
||||||
expect(contextEntries).toEqual([
|
expect(contextEntries).toEqual([
|
||||||
{ key: "deploy-target", value: "production" },
|
{ key: "deploy-target", value: "production" },
|
||||||
|
|
@ -310,13 +310,13 @@ it.live(
|
||||||
const opencode = yield* fixture.sdk.OpenCode.create()
|
const opencode = yield* fixture.sdk.OpenCode.create()
|
||||||
const id = sessionID(fixture)
|
const id = sessionID(fixture)
|
||||||
const connected = yield* Latch.make(false)
|
const connected = yield* Latch.make(false)
|
||||||
const prompted = yield* Deferred.make<Extract<OpenCodeEvent, { type: "session.prompt.promoted" }>>()
|
const prompted = yield* Deferred.make<Extract<OpenCodeEvent, { type: "session.input.promoted" }>>()
|
||||||
|
|
||||||
yield* opencode.events.subscribe().pipe(
|
yield* opencode.events.subscribe().pipe(
|
||||||
Stream.runForEach((event) =>
|
Stream.runForEach((event) =>
|
||||||
event.type === "server.connected"
|
event.type === "server.connected"
|
||||||
? connected.open
|
? connected.open
|
||||||
: event.type === "session.prompt.promoted" && event.data.sessionID === id
|
: event.type === "session.input.promoted" && event.data.sessionID === id
|
||||||
? Deferred.succeed(prompted, event).pipe(Effect.asVoid)
|
? Deferred.succeed(prompted, event).pipe(Effect.asVoid)
|
||||||
: Effect.void,
|
: Effect.void,
|
||||||
),
|
),
|
||||||
|
|
@ -326,7 +326,7 @@ it.live(
|
||||||
yield* opencode.sessions.create({ id, location: location(fixture) })
|
yield* opencode.sessions.create({ id, location: location(fixture) })
|
||||||
yield* opencode.sessions.prompt({
|
yield* opencode.sessions.prompt({
|
||||||
sessionID: id,
|
sessionID: id,
|
||||||
prompt: fixture.sdk.PromptInput.Prompt.make({ text: "Observe this input" }),
|
text: "Observe this input",
|
||||||
})
|
})
|
||||||
|
|
||||||
const event = yield* Deferred.await(prompted).pipe(Effect.timeout("4 seconds"))
|
const event = yield* Deferred.await(prompted).pipe(Effect.timeout("4 seconds"))
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ if (schemas) {
|
||||||
visit({ ...document, components: { ...document.components, schemas: undefined } })
|
visit({ ...document, components: { ...document.components, schemas: undefined } })
|
||||||
for (const name of Object.keys(schemas)) {
|
for (const name of Object.keys(schemas)) {
|
||||||
if (
|
if (
|
||||||
/^(SessionAgentSelected|SessionModelSelected|SessionMoved|SessionRenamed|SessionForked|SessionPromptPromoted|SessionPromptAdmitted|SessionExecutionStarted|SessionExecutionSucceeded|SessionExecutionFailed|SessionExecutionInterrupted|SessionInstructionsUpdated|SessionSynthetic|SessionSkillActivated|SessionShellStarted|SessionShellEnded|SessionStepStarted|SessionStepEnded|SessionStepFailed|SessionTextStarted|SessionTextDelta|SessionTextEnded|SessionToolInputStarted|SessionToolInputDelta|SessionToolInputEnded|SessionToolCalled|SessionToolProgress|SessionToolSuccess|SessionToolFailed|SessionRetryScheduled|SessionCompactionStarted|SessionCompactionDelta|SessionCompactionEnded|SessionRevertStaged|SessionRevertCleared|SessionRevertCommitted)\d+$/.test(
|
/^(SessionAgentSelected|SessionModelSelected|SessionMoved|SessionRenamed|SessionForked|SessionInputPromoted|SessionInputAdmitted|SessionExecutionStarted|SessionExecutionSucceeded|SessionExecutionFailed|SessionExecutionInterrupted|SessionInstructionsUpdated|SessionSynthetic|SessionSkillActivated|SessionShellStarted|SessionShellEnded|SessionStepStarted|SessionStepEnded|SessionStepFailed|SessionTextStarted|SessionTextDelta|SessionTextEnded|SessionToolInputStarted|SessionToolInputDelta|SessionToolInputEnded|SessionToolCalled|SessionToolProgress|SessionToolSuccess|SessionToolFailed|SessionRetryScheduled|SessionCompactionStarted|SessionCompactionDelta|SessionCompactionEnded|SessionRevertStaged|SessionRevertCleared|SessionRevertCommitted)\d+$/.test(
|
||||||
name,
|
name,
|
||||||
) &&
|
) &&
|
||||||
!reachable.has(name)
|
!reachable.has(name)
|
||||||
|
|
@ -102,7 +102,7 @@ await createClient({
|
||||||
const generatedTypesPath = "./src/v2/gen/types.gen.ts"
|
const generatedTypesPath = "./src/v2/gen/types.gen.ts"
|
||||||
const generatedTypes = await Bun.file(generatedTypesPath).text()
|
const generatedTypes = await Bun.file(generatedTypesPath).text()
|
||||||
if (
|
if (
|
||||||
/export type (SessionAgentSelected|SessionModelSelected|SessionMoved|SessionRenamed|SessionForked|SessionPromptPromoted|SessionPromptAdmitted|SessionExecutionStarted|SessionExecutionSucceeded|SessionExecutionFailed|SessionExecutionInterrupted|SessionInstructionsUpdated|SessionSynthetic|SessionSkillActivated|SessionShellStarted|SessionShellEnded|SessionStepStarted|SessionStepEnded|SessionStepFailed|SessionTextStarted|SessionTextDelta|SessionTextEnded|SessionToolInputStarted|SessionToolInputDelta|SessionToolInputEnded|SessionToolCalled|SessionToolProgress|SessionToolSuccess|SessionToolFailed|SessionRetryScheduled|SessionCompactionStarted|SessionCompactionDelta|SessionCompactionEnded|SessionRevertStaged|SessionRevertCleared|SessionRevertCommitted)\d+ =/.test(
|
/export type (SessionAgentSelected|SessionModelSelected|SessionMoved|SessionRenamed|SessionForked|SessionInputPromoted|SessionInputAdmitted|SessionExecutionStarted|SessionExecutionSucceeded|SessionExecutionFailed|SessionExecutionInterrupted|SessionInstructionsUpdated|SessionSynthetic|SessionSkillActivated|SessionShellStarted|SessionShellEnded|SessionStepStarted|SessionStepEnded|SessionStepFailed|SessionTextStarted|SessionTextDelta|SessionTextEnded|SessionToolInputStarted|SessionToolInputDelta|SessionToolInputEnded|SessionToolCalled|SessionToolProgress|SessionToolSuccess|SessionToolFailed|SessionRetryScheduled|SessionCompactionStarted|SessionCompactionDelta|SessionCompactionEnded|SessionRevertStaged|SessionRevertCleared|SessionRevertCommitted)\d+ =/.test(
|
||||||
generatedTypes,
|
generatedTypes,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,6 @@ import type {
|
||||||
ProjectUpdateErrors,
|
ProjectUpdateErrors,
|
||||||
ProjectUpdateResponses,
|
ProjectUpdateResponses,
|
||||||
PromptAgentAttachment,
|
PromptAgentAttachment,
|
||||||
PromptInput,
|
|
||||||
PromptInputFileAttachment,
|
PromptInputFileAttachment,
|
||||||
ProviderAuthErrors,
|
ProviderAuthErrors,
|
||||||
ProviderAuthResponses,
|
ProviderAuthResponses,
|
||||||
|
|
@ -6165,7 +6164,12 @@ export class Session3 extends HeyApiClient {
|
||||||
parameters: {
|
parameters: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
id?: string | null
|
id?: string | null
|
||||||
prompt?: PromptInput
|
text?: string
|
||||||
|
files?: Array<PromptInputFileAttachment>
|
||||||
|
agents?: Array<PromptAgentAttachment>
|
||||||
|
metadata?: {
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
delivery?: "steer" | "queue" | null
|
delivery?: "steer" | "queue" | null
|
||||||
resume?: boolean | null
|
resume?: boolean | null
|
||||||
},
|
},
|
||||||
|
|
@ -6178,7 +6182,10 @@ export class Session3 extends HeyApiClient {
|
||||||
args: [
|
args: [
|
||||||
{ in: "path", key: "sessionID" },
|
{ in: "path", key: "sessionID" },
|
||||||
{ in: "body", key: "id" },
|
{ in: "body", key: "id" },
|
||||||
{ in: "body", key: "prompt" },
|
{ in: "body", key: "text" },
|
||||||
|
{ in: "body", key: "files" },
|
||||||
|
{ in: "body", key: "agents" },
|
||||||
|
{ in: "body", key: "metadata" },
|
||||||
{ in: "body", key: "delivery" },
|
{ in: "body", key: "delivery" },
|
||||||
{ in: "body", key: "resume" },
|
{ in: "body", key: "resume" },
|
||||||
],
|
],
|
||||||
|
|
@ -6290,16 +6297,18 @@ export class Session3 extends HeyApiClient {
|
||||||
/**
|
/**
|
||||||
* Add synthetic message
|
* Add synthetic message
|
||||||
*
|
*
|
||||||
* Append a synthetic message to a session and resume execution.
|
* Durably admit synthetic session input and schedule execution unless resume is false.
|
||||||
*/
|
*/
|
||||||
public synthetic<ThrowOnError extends boolean = false>(
|
public synthetic<ThrowOnError extends boolean = false>(
|
||||||
parameters: {
|
parameters: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
|
id?: string | null
|
||||||
text?: string
|
text?: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
metadata?: {
|
metadata?: {
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
delivery?: "steer" | "queue" | null
|
||||||
resume?: boolean | null
|
resume?: boolean | null
|
||||||
},
|
},
|
||||||
options?: Options<never, ThrowOnError>,
|
options?: Options<never, ThrowOnError>,
|
||||||
|
|
@ -6310,9 +6319,11 @@ export class Session3 extends HeyApiClient {
|
||||||
{
|
{
|
||||||
args: [
|
args: [
|
||||||
{ in: "path", key: "sessionID" },
|
{ in: "path", key: "sessionID" },
|
||||||
|
{ in: "body", key: "id" },
|
||||||
{ in: "body", key: "text" },
|
{ in: "body", key: "text" },
|
||||||
{ in: "body", key: "description" },
|
{ in: "body", key: "description" },
|
||||||
{ in: "body", key: "metadata" },
|
{ in: "body", key: "metadata" },
|
||||||
|
{ in: "body", key: "delivery" },
|
||||||
{ in: "body", key: "resume" },
|
{ in: "body", key: "resume" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ export type Event =
|
||||||
| EventSessionRenamed
|
| EventSessionRenamed
|
||||||
| EventSessionUsageUpdated
|
| EventSessionUsageUpdated
|
||||||
| EventSessionForked
|
| EventSessionForked
|
||||||
| EventSessionPromptPromoted
|
| EventSessionInputPromoted
|
||||||
| EventSessionPromptAdmitted
|
| EventSessionInputAdmitted
|
||||||
| EventSessionExecutionStarted
|
| EventSessionExecutionStarted
|
||||||
| EventSessionExecutionSucceeded
|
| EventSessionExecutionSucceeded
|
||||||
| EventSessionExecutionFailed
|
| EventSessionExecutionFailed
|
||||||
|
|
@ -599,12 +599,6 @@ export type Part =
|
||||||
| RetryPart
|
| RetryPart
|
||||||
| CompactionPart
|
| CompactionPart
|
||||||
|
|
||||||
export type Prompt = {
|
|
||||||
text: string
|
|
||||||
files?: Array<PromptFileAttachment>
|
|
||||||
agents?: Array<PromptAgentAttachment>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Shell = {
|
export type Shell = {
|
||||||
id: string
|
id: string
|
||||||
status: "running" | "exited" | "timeout" | "killed"
|
status: "running" | "exited" | "timeout" | "killed"
|
||||||
|
|
@ -856,7 +850,7 @@ export type GlobalEvent = {
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
id: string
|
id: string
|
||||||
type: "session.prompt.promoted"
|
type: "session.input.promoted"
|
||||||
properties: {
|
properties: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
inputID: string
|
inputID: string
|
||||||
|
|
@ -864,12 +858,11 @@ export type GlobalEvent = {
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
id: string
|
id: string
|
||||||
type: "session.prompt.admitted"
|
type: "session.input.admitted"
|
||||||
properties: {
|
properties: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
inputID: string
|
inputID: string
|
||||||
prompt: Prompt
|
input: SessionInputMessage
|
||||||
delivery: "steer" | "queue"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
|
|
@ -1720,8 +1713,8 @@ export type GlobalEvent = {
|
||||||
| SyncEventSessionMoved
|
| SyncEventSessionMoved
|
||||||
| SyncEventSessionRenamed
|
| SyncEventSessionRenamed
|
||||||
| SyncEventSessionForked
|
| SyncEventSessionForked
|
||||||
| SyncEventSessionPromptPromoted
|
| SyncEventSessionInputPromoted
|
||||||
| SyncEventSessionPromptAdmitted
|
| SyncEventSessionInputAdmitted
|
||||||
| SyncEventSessionExecutionStarted
|
| SyncEventSessionExecutionStarted
|
||||||
| SyncEventSessionExecutionSucceeded
|
| SyncEventSessionExecutionSucceeded
|
||||||
| SyncEventSessionExecutionFailed
|
| SyncEventSessionExecutionFailed
|
||||||
|
|
@ -2876,12 +2869,6 @@ export type MessageNotFoundError = {
|
||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PromptInput = {
|
|
||||||
text: string
|
|
||||||
files?: Array<PromptInputFileAttachment>
|
|
||||||
agents?: Array<PromptAgentAttachment>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ConflictError = {
|
export type ConflictError = {
|
||||||
_tag: "ConflictError"
|
_tag: "ConflictError"
|
||||||
message: string
|
message: string
|
||||||
|
|
@ -3058,8 +3045,8 @@ export type V2Event =
|
||||||
| SessionRenamed
|
| SessionRenamed
|
||||||
| SessionUsageUpdated
|
| SessionUsageUpdated
|
||||||
| SessionForked
|
| SessionForked
|
||||||
| SessionPromptPromoted
|
| SessionInputPromoted
|
||||||
| SessionPromptAdmitted
|
| SessionInputAdmitted
|
||||||
| SessionExecutionStarted
|
| SessionExecutionStarted
|
||||||
| SessionExecutionSucceeded
|
| SessionExecutionSucceeded
|
||||||
| SessionExecutionFailed
|
| SessionExecutionFailed
|
||||||
|
|
@ -3391,6 +3378,37 @@ export type PromptAgentAttachment = {
|
||||||
mention?: PromptMention
|
mention?: PromptMention
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SessionInputUserData = {
|
||||||
|
text: string
|
||||||
|
files?: Array<PromptFileAttachment>
|
||||||
|
agents?: Array<PromptAgentAttachment>
|
||||||
|
metadata?: {
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SessionInputUserMessage = {
|
||||||
|
type: "user"
|
||||||
|
data: SessionInputUserData
|
||||||
|
delivery: "steer" | "queue"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SessionInputSyntheticData = {
|
||||||
|
text: string
|
||||||
|
description?: string
|
||||||
|
metadata?: {
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SessionInputSyntheticMessage = {
|
||||||
|
type: "synthetic"
|
||||||
|
data: SessionInputSyntheticData
|
||||||
|
delivery: "steer" | "queue"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SessionInputMessage = SessionInputUserMessage | SessionInputSyntheticMessage
|
||||||
|
|
||||||
export type SessionStructuredError = {
|
export type SessionStructuredError = {
|
||||||
type: string
|
type: string
|
||||||
message: string
|
message: string
|
||||||
|
|
@ -3799,11 +3817,11 @@ export type SyncEventSessionForked = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SyncEventSessionPromptPromoted = {
|
export type SyncEventSessionInputPromoted = {
|
||||||
type: "sync"
|
type: "sync"
|
||||||
id: string
|
id: string
|
||||||
syncEvent: {
|
syncEvent: {
|
||||||
type: "session.prompt.promoted.1"
|
type: "session.input.promoted.1"
|
||||||
id: string
|
id: string
|
||||||
seq: number
|
seq: number
|
||||||
aggregateID: string
|
aggregateID: string
|
||||||
|
|
@ -3814,19 +3832,18 @@ export type SyncEventSessionPromptPromoted = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SyncEventSessionPromptAdmitted = {
|
export type SyncEventSessionInputAdmitted = {
|
||||||
type: "sync"
|
type: "sync"
|
||||||
id: string
|
id: string
|
||||||
syncEvent: {
|
syncEvent: {
|
||||||
type: "session.prompt.admitted.1"
|
type: "session.input.admitted.1"
|
||||||
id: string
|
id: string
|
||||||
seq: number
|
seq: number
|
||||||
aggregateID: string
|
aggregateID: string
|
||||||
data: {
|
data: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
inputID: string
|
inputID: string
|
||||||
prompt: Prompt
|
input: SessionInputMessage
|
||||||
delivery: "steer" | "queue"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4459,22 +4476,34 @@ export type PromptInputFileAttachment = {
|
||||||
mention?: PromptMention
|
mention?: PromptMention
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionInputAdmitted = {
|
export type SessionInputUser = {
|
||||||
admittedSeq: number
|
admittedSeq: number
|
||||||
id: string
|
id: string
|
||||||
sessionID: string
|
sessionID: string
|
||||||
prompt: Prompt
|
|
||||||
delivery: "steer" | "queue"
|
|
||||||
timeCreated: number
|
timeCreated: number
|
||||||
promotedSeq?: number
|
promotedSeq?: number
|
||||||
|
type: "user"
|
||||||
|
data: SessionInputUserData
|
||||||
|
delivery: "steer" | "queue"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SessionInputSynthetic = {
|
||||||
|
admittedSeq: number
|
||||||
|
id: string
|
||||||
|
sessionID: string
|
||||||
|
timeCreated: number
|
||||||
|
promotedSeq?: number
|
||||||
|
type: "synthetic"
|
||||||
|
data: SessionInputSyntheticData
|
||||||
|
delivery: "steer" | "queue"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionInputCompaction = {
|
export type SessionInputCompaction = {
|
||||||
type: "compaction"
|
|
||||||
admittedSeq: number
|
admittedSeq: number
|
||||||
id: string
|
id: string
|
||||||
sessionID: string
|
sessionID: string
|
||||||
timeCreated: number
|
timeCreated: number
|
||||||
|
type: "compaction"
|
||||||
handledSeq?: number
|
handledSeq?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4866,13 +4895,13 @@ export type SessionForked = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionPromptPromoted = {
|
export type SessionInputPromoted = {
|
||||||
id: string
|
id: string
|
||||||
created: number
|
created: number
|
||||||
metadata?: {
|
metadata?: {
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
type: "session.prompt.promoted"
|
type: "session.input.promoted"
|
||||||
durable: {
|
durable: {
|
||||||
aggregateID: string
|
aggregateID: string
|
||||||
seq: number
|
seq: number
|
||||||
|
|
@ -4885,13 +4914,13 @@ export type SessionPromptPromoted = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionPromptAdmitted = {
|
export type SessionInputAdmitted = {
|
||||||
id: string
|
id: string
|
||||||
created: number
|
created: number
|
||||||
metadata?: {
|
metadata?: {
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
type: "session.prompt.admitted"
|
type: "session.input.admitted"
|
||||||
durable: {
|
durable: {
|
||||||
aggregateID: string
|
aggregateID: string
|
||||||
seq: number
|
seq: number
|
||||||
|
|
@ -4901,8 +4930,7 @@ export type SessionPromptAdmitted = {
|
||||||
data: {
|
data: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
inputID: string
|
inputID: string
|
||||||
prompt: Prompt
|
input: SessionInputMessage
|
||||||
delivery: "steer" | "queue"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5548,8 +5576,8 @@ export type SessionEventDurable =
|
||||||
| SessionRenamed
|
| SessionRenamed
|
||||||
| SessionDeleted
|
| SessionDeleted
|
||||||
| SessionForked
|
| SessionForked
|
||||||
| SessionPromptPromoted
|
| SessionInputPromoted
|
||||||
| SessionPromptAdmitted
|
| SessionInputAdmitted
|
||||||
| SessionExecutionStarted
|
| SessionExecutionStarted
|
||||||
| SessionExecutionSucceeded
|
| SessionExecutionSucceeded
|
||||||
| SessionExecutionFailed
|
| SessionExecutionFailed
|
||||||
|
|
@ -7182,23 +7210,22 @@ export type EventSessionForked = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EventSessionPromptPromoted = {
|
export type EventSessionInputPromoted = {
|
||||||
id: string
|
id: string
|
||||||
type: "session.prompt.promoted"
|
type: "session.input.promoted"
|
||||||
properties: {
|
properties: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
inputID: string
|
inputID: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EventSessionPromptAdmitted = {
|
export type EventSessionInputAdmitted = {
|
||||||
id: string
|
id: string
|
||||||
type: "session.prompt.admitted"
|
type: "session.input.admitted"
|
||||||
properties: {
|
properties: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
inputID: string
|
inputID: string
|
||||||
prompt: Prompt
|
input: SessionInputMessage
|
||||||
delivery: "steer" | "queue"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -8644,8 +8671,8 @@ export type V2EventV2 =
|
||||||
| SessionUsageUpdatedV2
|
| SessionUsageUpdatedV2
|
||||||
| SessionDeletedV2
|
| SessionDeletedV2
|
||||||
| SessionForkedV2
|
| SessionForkedV2
|
||||||
| SessionPromptPromotedV2
|
| SessionInputPromotedV2
|
||||||
| SessionPromptAdmittedV2
|
| SessionInputAdmittedV2
|
||||||
| SessionExecutionStartedV2
|
| SessionExecutionStartedV2
|
||||||
| SessionExecutionSucceededV2
|
| SessionExecutionSucceededV2
|
||||||
| SessionExecutionFailedV2
|
| SessionExecutionFailedV2
|
||||||
|
|
@ -8801,22 +8828,34 @@ export type SessionInfoV2 = {
|
||||||
|
|
||||||
export type PromptBase64V2 = string
|
export type PromptBase64V2 = string
|
||||||
|
|
||||||
export type SessionInputAdmittedV2 = {
|
export type SessionInputUserV2 = {
|
||||||
admittedSeq: number
|
admittedSeq: number
|
||||||
id: string
|
id: string
|
||||||
sessionID: string
|
sessionID: string
|
||||||
prompt: Prompt
|
|
||||||
delivery: "steer" | "queue"
|
|
||||||
timeCreated: number
|
timeCreated: number
|
||||||
promotedSeq?: number
|
promotedSeq?: number
|
||||||
|
type: "user"
|
||||||
|
data: SessionInputUserData
|
||||||
|
delivery: "steer" | "queue"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SessionInputSyntheticV2 = {
|
||||||
|
admittedSeq: number
|
||||||
|
id: string
|
||||||
|
sessionID: string
|
||||||
|
timeCreated: number
|
||||||
|
promotedSeq?: number
|
||||||
|
type: "synthetic"
|
||||||
|
data: SessionInputSyntheticData
|
||||||
|
delivery: "steer" | "queue"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionInputCompactionV2 = {
|
export type SessionInputCompactionV2 = {
|
||||||
type: "compaction"
|
|
||||||
admittedSeq: number
|
admittedSeq: number
|
||||||
id: string
|
id: string
|
||||||
sessionID: string
|
sessionID: string
|
||||||
timeCreated: number
|
timeCreated: number
|
||||||
|
type: "compaction"
|
||||||
handledSeq?: number
|
handledSeq?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -9115,13 +9154,13 @@ export type SessionForkedV2 = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionPromptPromotedV2 = {
|
export type SessionInputPromotedV2 = {
|
||||||
id: string
|
id: string
|
||||||
created: number
|
created: number
|
||||||
metadata?: {
|
metadata?: {
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
type: "session.prompt.promoted"
|
type: "session.input.promoted"
|
||||||
durable: {
|
durable: {
|
||||||
aggregateID: string
|
aggregateID: string
|
||||||
seq: number
|
seq: number
|
||||||
|
|
@ -9134,13 +9173,42 @@ export type SessionPromptPromotedV2 = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionPromptAdmittedV2 = {
|
export type SessionInputUserData1 = {
|
||||||
|
text: string
|
||||||
|
files?: Array<PromptFileAttachment>
|
||||||
|
agents?: Array<PromptAgentAttachment>
|
||||||
|
metadata?: {
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SessionInputUserMessageV2 = {
|
||||||
|
type: "user"
|
||||||
|
data: SessionInputUserData1
|
||||||
|
delivery: "steer" | "queue"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SessionInputSyntheticData1 = {
|
||||||
|
text: string
|
||||||
|
description?: string
|
||||||
|
metadata?: {
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SessionInputSyntheticMessageV2 = {
|
||||||
|
type: "synthetic"
|
||||||
|
data: SessionInputSyntheticData1
|
||||||
|
delivery: "steer" | "queue"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SessionInputAdmittedV2 = {
|
||||||
id: string
|
id: string
|
||||||
created: number
|
created: number
|
||||||
metadata?: {
|
metadata?: {
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
type: "session.prompt.admitted"
|
type: "session.input.admitted"
|
||||||
durable: {
|
durable: {
|
||||||
aggregateID: string
|
aggregateID: string
|
||||||
seq: number
|
seq: number
|
||||||
|
|
@ -9150,8 +9218,7 @@ export type SessionPromptAdmittedV2 = {
|
||||||
data: {
|
data: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
inputID: string
|
inputID: string
|
||||||
prompt: Prompt
|
input: SessionInputMessage
|
||||||
delivery: "steer" | "queue"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -15672,7 +15739,12 @@ export type V2SessionMoveResponse = V2SessionMoveResponses[keyof V2SessionMoveRe
|
||||||
export type V2SessionPromptData = {
|
export type V2SessionPromptData = {
|
||||||
body: {
|
body: {
|
||||||
id?: string | null
|
id?: string | null
|
||||||
prompt: PromptInput
|
text: string
|
||||||
|
files?: Array<PromptInputFileAttachment>
|
||||||
|
agents?: Array<PromptAgentAttachment>
|
||||||
|
metadata?: {
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
delivery?: "steer" | "queue" | null
|
delivery?: "steer" | "queue" | null
|
||||||
resume?: boolean | null
|
resume?: boolean | null
|
||||||
}
|
}
|
||||||
|
|
@ -15709,7 +15781,7 @@ export type V2SessionPromptResponses = {
|
||||||
* Success
|
* Success
|
||||||
*/
|
*/
|
||||||
200: {
|
200: {
|
||||||
data: SessionInputAdmittedV2
|
data: SessionInputUserV2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -15764,7 +15836,7 @@ export type V2SessionCommandResponses = {
|
||||||
* Success
|
* Success
|
||||||
*/
|
*/
|
||||||
200: {
|
200: {
|
||||||
data: SessionInputAdmittedV2
|
data: SessionInputUserV2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -15811,11 +15883,13 @@ export type V2SessionSkillResponse = V2SessionSkillResponses[keyof V2SessionSkil
|
||||||
|
|
||||||
export type V2SessionSyntheticData = {
|
export type V2SessionSyntheticData = {
|
||||||
body: {
|
body: {
|
||||||
|
id?: string | null
|
||||||
text: string
|
text: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
metadata?: {
|
metadata?: {
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
delivery?: "steer" | "queue" | null
|
||||||
resume?: boolean | null
|
resume?: boolean | null
|
||||||
}
|
}
|
||||||
path: {
|
path: {
|
||||||
|
|
@ -15838,15 +15912,21 @@ export type V2SessionSyntheticErrors = {
|
||||||
* SessionNotFoundError
|
* SessionNotFoundError
|
||||||
*/
|
*/
|
||||||
404: SessionNotFoundError
|
404: SessionNotFoundError
|
||||||
|
/**
|
||||||
|
* ConflictError
|
||||||
|
*/
|
||||||
|
409: ConflictErrorV2
|
||||||
}
|
}
|
||||||
|
|
||||||
export type V2SessionSyntheticError = V2SessionSyntheticErrors[keyof V2SessionSyntheticErrors]
|
export type V2SessionSyntheticError = V2SessionSyntheticErrors[keyof V2SessionSyntheticErrors]
|
||||||
|
|
||||||
export type V2SessionSyntheticResponses = {
|
export type V2SessionSyntheticResponses = {
|
||||||
/**
|
/**
|
||||||
* <No Content>
|
* Success
|
||||||
*/
|
*/
|
||||||
204: void
|
200: {
|
||||||
|
data: SessionInputSyntheticV2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type V2SessionSyntheticResponse = V2SessionSyntheticResponses[keyof V2SessionSyntheticResponses]
|
export type V2SessionSyntheticResponse = V2SessionSyntheticResponses[keyof V2SessionSyntheticResponses]
|
||||||
|
|
|
||||||
|
|
@ -206,37 +206,39 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||||
.handle(
|
.handle(
|
||||||
"session.move",
|
"session.move",
|
||||||
Effect.fn(function* (ctx) {
|
Effect.fn(function* (ctx) {
|
||||||
yield* moveSession.moveSession({
|
yield* moveSession
|
||||||
sessionID: ctx.params.sessionID,
|
.moveSession({
|
||||||
destination: ctx.payload.destination,
|
sessionID: ctx.params.sessionID,
|
||||||
moveChanges: ctx.payload.moveChanges,
|
destination: ctx.payload.destination,
|
||||||
}).pipe(
|
moveChanges: ctx.payload.moveChanges,
|
||||||
Effect.catchTag("Session.NotFoundError", (error) =>
|
})
|
||||||
Effect.fail(
|
.pipe(
|
||||||
new SessionNotFoundError({
|
Effect.catchTag("Session.NotFoundError", (error) =>
|
||||||
sessionID: error.sessionID,
|
Effect.fail(
|
||||||
message: `Session not found: ${error.sessionID}`,
|
new SessionNotFoundError({
|
||||||
}),
|
sessionID: error.sessionID,
|
||||||
|
message: `Session not found: ${error.sessionID}`,
|
||||||
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
Effect.catchTag("MoveSession.DestinationProjectMismatchError", () =>
|
||||||
Effect.catchTag("MoveSession.DestinationProjectMismatchError", () =>
|
Effect.fail(new InvalidRequestError({ message: "Destination directory belongs to another project" })),
|
||||||
Effect.fail(new InvalidRequestError({ message: "Destination directory belongs to another project" })),
|
|
||||||
),
|
|
||||||
Effect.catchTag("MoveSession.ApplyChangesError", () =>
|
|
||||||
Effect.fail(
|
|
||||||
new InvalidRequestError({
|
|
||||||
message:
|
|
||||||
"Unable to apply your changes in the destination directory. The files may conflict with existing changes.",
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
),
|
Effect.catchTag("MoveSession.ApplyChangesError", () =>
|
||||||
Effect.catchTag("MoveSession.CaptureChangesError", (error) =>
|
Effect.fail(
|
||||||
Effect.fail(new InvalidRequestError({ message: error.message })),
|
new InvalidRequestError({
|
||||||
),
|
message:
|
||||||
Effect.catchTag("MoveSession.ResetSourceChangesError", (error) =>
|
"Unable to apply your changes in the destination directory. The files may conflict with existing changes.",
|
||||||
Effect.fail(new InvalidRequestError({ message: error.message })),
|
}),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
|
Effect.catchTag("MoveSession.CaptureChangesError", (error) =>
|
||||||
|
Effect.fail(new InvalidRequestError({ message: error.message })),
|
||||||
|
),
|
||||||
|
Effect.catchTag("MoveSession.ResetSourceChangesError", (error) =>
|
||||||
|
Effect.fail(new InvalidRequestError({ message: error.message })),
|
||||||
|
),
|
||||||
|
)
|
||||||
return HttpApiSchema.NoContent.make()
|
return HttpApiSchema.NoContent.make()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
@ -248,7 +250,10 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||||
.prompt({
|
.prompt({
|
||||||
sessionID: ctx.params.sessionID,
|
sessionID: ctx.params.sessionID,
|
||||||
id: ctx.payload.id,
|
id: ctx.payload.id,
|
||||||
prompt: ctx.payload.prompt,
|
text: ctx.payload.text,
|
||||||
|
files: ctx.payload.files,
|
||||||
|
agents: ctx.payload.agents,
|
||||||
|
metadata: ctx.payload.metadata,
|
||||||
delivery: ctx.payload.delivery,
|
delivery: ctx.payload.delivery,
|
||||||
resume: ctx.payload.resume,
|
resume: ctx.payload.resume,
|
||||||
})
|
})
|
||||||
|
|
@ -270,7 +275,7 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Effect.catchTag("Session.AttachmentError", (error) =>
|
Effect.catchTag("Session.AttachmentError", (error) =>
|
||||||
Effect.fail(new InvalidRequestError({ message: error.message, field: "prompt.files" })),
|
Effect.fail(new InvalidRequestError({ message: error.message, field: "files" })),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
@ -362,12 +367,14 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||||
.handle(
|
.handle(
|
||||||
"session.synthetic",
|
"session.synthetic",
|
||||||
Effect.fn(function* (ctx) {
|
Effect.fn(function* (ctx) {
|
||||||
yield* session
|
const data = yield* session
|
||||||
.synthetic({
|
.synthetic({
|
||||||
|
id: ctx.payload.id,
|
||||||
sessionID: ctx.params.sessionID,
|
sessionID: ctx.params.sessionID,
|
||||||
text: ctx.payload.text,
|
text: ctx.payload.text,
|
||||||
description: ctx.payload.description,
|
description: ctx.payload.description,
|
||||||
metadata: ctx.payload.metadata,
|
metadata: ctx.payload.metadata,
|
||||||
|
delivery: ctx.payload.delivery,
|
||||||
resume: ctx.payload.resume,
|
resume: ctx.payload.resume,
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
|
|
@ -379,8 +386,16 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Effect.catchTag("Session.SyntheticConflictError", (error) =>
|
||||||
|
Effect.fail(
|
||||||
|
new ConflictError({
|
||||||
|
message: `Synthetic input ID conflicts with an existing durable record: ${error.inputID}`,
|
||||||
|
resource: error.inputID,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return HttpApiSchema.NoContent.make()
|
return { data }
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.handle(
|
.handle(
|
||||||
|
|
|
||||||
|
|
@ -286,8 +286,8 @@ export function Prompt(props: PromptProps) {
|
||||||
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
|
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
|
||||||
if (tokens <= 0) return
|
if (tokens <= 0) return
|
||||||
|
|
||||||
const model = data.location
|
const model = data.location.model
|
||||||
.model.list(session.location)
|
.list(session.location)
|
||||||
?.find((model) => model.providerID === last.model.providerID && model.id === last.model.id)
|
?.find((model) => model.providerID === last.model.providerID && model.id === last.model.id)
|
||||||
const pct = model?.limit.context ? `${Math.round((tokens / model.limit.context) * 100)}%` : undefined
|
const pct = model?.limit.context ? `${Math.round((tokens / model.limit.context) * 100)}%` : undefined
|
||||||
const cost = session.cost
|
const cost = session.cost
|
||||||
|
|
@ -1139,11 +1139,9 @@ export function Prompt(props: PromptProps) {
|
||||||
const error = await sdk.api.session
|
const error = await sdk.api.session
|
||||||
.prompt({
|
.prompt({
|
||||||
sessionID,
|
sessionID,
|
||||||
prompt: {
|
text: [...editorParts.map((part) => part.text), inputText].filter(Boolean).join("\n\n"),
|
||||||
text: [...editorParts.map((part) => part.text), inputText].filter(Boolean).join("\n\n"),
|
files: store.prompt.files,
|
||||||
files: store.prompt.files,
|
agents: store.prompt.agents,
|
||||||
agents: store.prompt.agents,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
.then(
|
.then(
|
||||||
() => undefined,
|
() => undefined,
|
||||||
|
|
|
||||||
|
|
@ -315,19 +315,17 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
setStore("session", "info", event.data.sessionID, "subpath", event.data.subpath)
|
setStore("session", "info", event.data.sessionID, "subpath", event.data.subpath)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case "session.prompt.promoted": {
|
case "session.input.promoted": {
|
||||||
message.update(event.data.sessionID, (draft, index) => {
|
message.update(event.data.sessionID, (draft, index) => {
|
||||||
const position = index.get(event.data.inputID)
|
const position = index.get(event.data.inputID)
|
||||||
if (position === undefined) return
|
if (position === undefined) return
|
||||||
const existing = draft[position]
|
const existing = draft[position]
|
||||||
if (existing?.type === "user" && store.session.input[event.data.sessionID]?.includes(event.data.inputID)) {
|
if (!existing || !store.session.input[event.data.sessionID]?.includes(event.data.inputID)) return
|
||||||
existing.time.created = event.created
|
existing.time.created = event.created
|
||||||
draft.splice(position, 1)
|
draft.splice(position, 1)
|
||||||
draft.push(existing)
|
draft.push(existing)
|
||||||
index.clear()
|
index.clear()
|
||||||
draft.forEach((message, indexValue) => index.set(message.id, indexValue))
|
draft.forEach((message, indexValue) => index.set(message.id, indexValue))
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
setStore(
|
setStore(
|
||||||
"session",
|
"session",
|
||||||
|
|
@ -337,21 +335,30 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "session.prompt.admitted":
|
case "session.input.admitted":
|
||||||
if (!store.session.input[event.data.sessionID]?.includes(event.data.inputID))
|
if (!store.session.input[event.data.sessionID]?.includes(event.data.inputID))
|
||||||
setStore("session", "input", event.data.sessionID, [
|
setStore("session", "input", event.data.sessionID, [
|
||||||
...(store.session.input[event.data.sessionID] ?? []),
|
...(store.session.input[event.data.sessionID] ?? []),
|
||||||
event.data.inputID,
|
event.data.inputID,
|
||||||
])
|
])
|
||||||
message.update(event.data.sessionID, (draft, index) => {
|
message.update(event.data.sessionID, (draft, index) => {
|
||||||
message.append(draft, index, {
|
message.append(
|
||||||
id: event.data.inputID,
|
draft,
|
||||||
type: "user",
|
index,
|
||||||
text: event.data.prompt.text,
|
event.data.input.type === "user"
|
||||||
files: event.data.prompt.files,
|
? {
|
||||||
agents: event.data.prompt.agents,
|
id: event.data.inputID,
|
||||||
time: { created: event.created },
|
type: "user",
|
||||||
})
|
...event.data.input.data,
|
||||||
|
time: { created: event.created },
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
id: event.data.inputID,
|
||||||
|
type: "synthetic",
|
||||||
|
...event.data.input.data,
|
||||||
|
time: { created: event.created },
|
||||||
|
},
|
||||||
|
)
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case "session.instructions.updated":
|
case "session.instructions.updated":
|
||||||
|
|
@ -1069,7 +1076,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
console.error("Failed to refresh default location data", failure.reason)
|
console.error("Failed to refresh default location data", failure.reason)
|
||||||
const key = locationKey(defaultLocation())
|
const key = locationKey(defaultLocation())
|
||||||
const locations = new Map(
|
const locations = new Map(
|
||||||
Object.values(store.session.info).map((session) => [locationKey(session.location), session.location] as const),
|
Object.values(store.session.info).map(
|
||||||
|
(session) => [locationKey(session.location), session.location] as const,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
const refreshed = await Promise.allSettled(
|
const refreshed = await Promise.allSettled(
|
||||||
Array.from(locations)
|
Array.from(locations)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export type PastedText = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PromptInfo = Types.DeepMutable<SessionPromptInput["prompt"]> & {
|
export type PromptInfo = Types.DeepMutable<Pick<SessionPromptInput, "text" | "files" | "agents">> & {
|
||||||
pasted: PastedText[]
|
pasted: PastedText[]
|
||||||
mode?: "normal" | "shell"
|
mode?: "normal" | "shell"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ export function createSessionRows(sessionID: Accessor<string>) {
|
||||||
on(
|
on(
|
||||||
() =>
|
() =>
|
||||||
data.session.message.list(sessionID()).flatMap((message) =>
|
data.session.message.list(sessionID()).flatMap((message) =>
|
||||||
message.type === "user"
|
message.type === "user" || message.type === "synthetic"
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
id: message.id,
|
id: message.id,
|
||||||
|
|
@ -156,7 +156,7 @@ export function createSessionRows(sessionID: Accessor<string>) {
|
||||||
|
|
||||||
const isPending = (messageID: string) => {
|
const isPending = (messageID: string) => {
|
||||||
const message = data.session.message.get(sessionID(), messageID)
|
const message = data.session.message.get(sessionID(), messageID)
|
||||||
if (message?.type === "user") return data.session.input.has(sessionID(), messageID)
|
if (message?.type === "user" || message?.type === "synthetic") return data.session.input.has(sessionID(), messageID)
|
||||||
return message?.type === "compaction" && message.status === "running"
|
return message?.type === "compaction" && message.status === "running"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -168,11 +168,21 @@ export function createSessionRows(sessionID: Accessor<string>) {
|
||||||
const message = (event: { id: string; data: { sessionID: string } }) => {
|
const message = (event: { id: string; data: { sessionID: string } }) => {
|
||||||
if (event.data.sessionID === sessionID()) appendMessage(event.id.replace(/^evt_/, "msg_"))
|
if (event.data.sessionID === sessionID()) appendMessage(event.id.replace(/^evt_/, "msg_"))
|
||||||
}
|
}
|
||||||
const input = (event: { data: { sessionID: string; inputID: string } }) => {
|
const input = (event: {
|
||||||
if (event.data.sessionID === sessionID()) appendMessage(event.data.inputID)
|
data: {
|
||||||
|
sessionID: string
|
||||||
|
inputID: string
|
||||||
|
input: { type: "user" } | { type: "synthetic"; data: { description?: string } }
|
||||||
|
}
|
||||||
|
}) => {
|
||||||
|
if (
|
||||||
|
event.data.sessionID === sessionID() &&
|
||||||
|
(event.data.input.type === "user" || event.data.input.data.description?.trim())
|
||||||
|
)
|
||||||
|
appendMessage(event.data.inputID)
|
||||||
}
|
}
|
||||||
const subscriptions = [
|
const subscriptions = [
|
||||||
data.on("session.prompt.admitted", input),
|
data.on("session.input.admitted", input),
|
||||||
data.on("session.compaction.started", message),
|
data.on("session.compaction.started", message),
|
||||||
data.on("session.instructions.updated", message),
|
data.on("session.instructions.updated", message),
|
||||||
data.on("session.synthetic", (event) => {
|
data.on("session.synthetic", (event) => {
|
||||||
|
|
|
||||||
|
|
@ -595,13 +595,12 @@ test("completes exploration when a queued prompt is promoted", async () => {
|
||||||
emitEvent(events, {
|
emitEvent(events, {
|
||||||
id: "evt_prompt_admitted",
|
id: "evt_prompt_admitted",
|
||||||
created: 3,
|
created: 3,
|
||||||
type: "session.prompt.admitted",
|
type: "session.input.admitted",
|
||||||
durable: durable(sessionID, 2),
|
durable: durable(sessionID, 2),
|
||||||
data: {
|
data: {
|
||||||
sessionID,
|
sessionID,
|
||||||
inputID: "message-user",
|
inputID: "message-user",
|
||||||
prompt: { text: "Continue" },
|
input: { type: "user", data: { text: "Continue" }, delivery: "steer" },
|
||||||
delivery: "steer",
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await wait(() => rows.at(-1)?.type === "message")
|
await wait(() => rows.at(-1)?.type === "message")
|
||||||
|
|
@ -610,7 +609,7 @@ test("completes exploration when a queued prompt is promoted", async () => {
|
||||||
emitEvent(events, {
|
emitEvent(events, {
|
||||||
id: "evt_prompt_promoted",
|
id: "evt_prompt_promoted",
|
||||||
created: 4,
|
created: 4,
|
||||||
type: "session.prompt.promoted",
|
type: "session.input.promoted",
|
||||||
durable: durable(sessionID, 3),
|
durable: durable(sessionID, 3),
|
||||||
data: { sessionID, inputID: "message-user" },
|
data: { sessionID, inputID: "message-user" },
|
||||||
})
|
})
|
||||||
|
|
@ -651,9 +650,9 @@ test("removes committed revert messages from local state", async () => {
|
||||||
emitEvent(events, {
|
emitEvent(events, {
|
||||||
id: EventV2.ID.create(),
|
id: EventV2.ID.create(),
|
||||||
created: seq,
|
created: seq,
|
||||||
type: "session.prompt.admitted",
|
type: "session.input.admitted",
|
||||||
durable: durable(sessionID, seq),
|
durable: durable(sessionID, seq),
|
||||||
data: { sessionID, inputID, prompt: { text: inputID }, delivery: "steer" },
|
data: { sessionID, inputID, input: { type: "user", data: { text: inputID }, delivery: "steer" } },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
await wait(() => data.session.message.ids(sessionID).length === 3)
|
await wait(() => data.session.message.ids(sessionID).length === 3)
|
||||||
|
|
@ -1817,9 +1816,7 @@ test("reconciles all pending form requests when the event stream reconnects", as
|
||||||
await wait(() => data.session.form.list("ses_old")?.[0]?.id === "frm_old")
|
await wait(() => data.session.form.list("ses_old")?.[0]?.id === "frm_old")
|
||||||
expect(data.session.form.list("ses_keep")?.[0]?.id).toBe("frm_keep")
|
expect(data.session.form.list("ses_keep")?.[0]?.id).toBe("frm_keep")
|
||||||
|
|
||||||
requests = [
|
requests = [{ id: "frm_new", sessionID: "ses_new", title: "Input requested", mode: "form" as const, fields: [] }]
|
||||||
{ id: "frm_new", sessionID: "ses_new", title: "Input requested", mode: "form" as const, fields: [] },
|
|
||||||
]
|
|
||||||
events.disconnect()
|
events.disconnect()
|
||||||
|
|
||||||
await wait(() => calls === 2 && data.session.form.list("ses_new")?.[0]?.id === "frm_new")
|
await wait(() => calls === 2 && data.session.form.list("ses_new")?.[0]?.id === "frm_new")
|
||||||
|
|
@ -2022,13 +2019,12 @@ test("renders admitted prompts immediately and tracks them until promoted", asyn
|
||||||
emitEvent(events, {
|
emitEvent(events, {
|
||||||
id: "evt_admitted_1",
|
id: "evt_admitted_1",
|
||||||
created: 0,
|
created: 0,
|
||||||
type: "session.prompt.admitted",
|
type: "session.input.admitted",
|
||||||
durable: durable(sessionID),
|
durable: durable(sessionID),
|
||||||
data: {
|
data: {
|
||||||
sessionID,
|
sessionID,
|
||||||
inputID: messageID,
|
inputID: messageID,
|
||||||
prompt: { text: "hello" },
|
input: { type: "user", data: { text: "hello" }, delivery: "steer" },
|
||||||
delivery: "steer",
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await wait(() => sync.session.message.list(sessionID)?.length === 1)
|
await wait(() => sync.session.message.list(sessionID)?.length === 1)
|
||||||
|
|
@ -2043,7 +2039,7 @@ test("renders admitted prompts immediately and tracks them until promoted", asyn
|
||||||
emitEvent(events, {
|
emitEvent(events, {
|
||||||
id: "evt_prompted_1",
|
id: "evt_prompted_1",
|
||||||
created: 0,
|
created: 0,
|
||||||
type: "session.prompt.promoted",
|
type: "session.input.promoted",
|
||||||
durable: durable(sessionID, 1),
|
durable: durable(sessionID, 1),
|
||||||
data: {
|
data: {
|
||||||
sessionID,
|
sessionID,
|
||||||
|
|
@ -2051,8 +2047,8 @@ test("renders admitted prompts immediately and tracks them until promoted", asyn
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await wait(() => received.at(-1) === "session.prompt.promoted")
|
await wait(() => received.at(-1) === "session.input.promoted")
|
||||||
expect(received.slice(-2)).toEqual(["session.prompt.admitted", "session.prompt.promoted"])
|
expect(received.slice(-2)).toEqual(["session.input.admitted", "session.input.promoted"])
|
||||||
unsubscribe()
|
unsubscribe()
|
||||||
const message = sync.session.message.list(sessionID)?.[0]
|
const message = sync.session.message.list(sessionID)?.[0]
|
||||||
expect(message?.type).toBe("user")
|
expect(message?.type).toBe("user")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue