feat(core): resolve prompt attachments
This commit is contained in:
parent
c13f06c30c
commit
4d100de194
29 changed files with 610 additions and 426 deletions
|
|
@ -16,7 +16,7 @@ import { SessionV2 } from "@opencode-ai/core/session"
|
|||
import { SessionCompaction } from "@opencode-ai/core/session/compaction"
|
||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { Prompt } from "@opencode-ai/core/session/prompt"
|
||||
import { Prompt } from "@opencode-ai/schema/prompt"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { ProviderV2 } from "@opencode-ai/core/provider"
|
|||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||
import { Prompt } from "@opencode-ai/core/session/prompt"
|
||||
import { PromptInput } from "@opencode-ai/schema/prompt-input"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
|
|
@ -192,7 +192,7 @@ describe("SessionV2.create", () => {
|
|||
const parent = yield* session.create({ location, title: "Parent" })
|
||||
const admitted = yield* session.prompt({
|
||||
sessionID: parent.id,
|
||||
prompt: Prompt.make({ text: "First" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "First" }),
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInput.promoteSteers(db, events, parent.id)
|
||||
|
|
@ -224,9 +224,9 @@ describe("SessionV2.create", () => {
|
|||
promotedSeq: 2,
|
||||
})
|
||||
|
||||
yield* session.prompt({ sessionID: parent.id, prompt: Prompt.make({ text: "Parent changed" }), resume: false })
|
||||
yield* session.prompt({ sessionID: parent.id, prompt: PromptInput.Prompt.make({ text: "Parent changed" }), resume: false })
|
||||
yield* SessionInput.promoteSteers(db, events, parent.id)
|
||||
yield* session.prompt({ sessionID: forked.id, prompt: Prompt.make({ text: "Child continues" }), resume: false })
|
||||
yield* session.prompt({ sessionID: forked.id, prompt: PromptInput.Prompt.make({ text: "Child continues" }), resume: false })
|
||||
yield* SessionInput.promoteSteers(db, events, forked.id)
|
||||
|
||||
expect((yield* session.context(parent.id)).map((message) => message.type)).toEqual(["user", "synthetic", "user"])
|
||||
|
|
@ -249,13 +249,13 @@ describe("SessionV2.create", () => {
|
|||
const parent = yield* session.create({ location })
|
||||
const first = yield* session.prompt({
|
||||
sessionID: parent.id,
|
||||
prompt: Prompt.make({ text: "First" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "First" }),
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInput.promoteSteers(db, events, parent.id)
|
||||
const second = yield* session.prompt({
|
||||
sessionID: parent.id,
|
||||
prompt: Prompt.make({ text: "Second" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Second" }),
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInput.promoteSteers(db, events, parent.id)
|
||||
|
|
@ -373,7 +373,7 @@ describe("SessionV2.create", () => {
|
|||
const events = yield* EventV2.Service
|
||||
const { db } = yield* Database.Service
|
||||
const created = yield* session.create({ location })
|
||||
yield* session.prompt({ sessionID: created.id, prompt: Prompt.make({ text: "Hello" }), resume: false })
|
||||
yield* session.prompt({ sessionID: created.id, prompt: PromptInput.Prompt.make({ text: "Hello" }), resume: false })
|
||||
yield* SessionInput.promoteSteers(db, events, created.id)
|
||||
|
||||
expect(
|
||||
|
|
@ -393,7 +393,7 @@ describe("SessionV2.create", () => {
|
|||
const created = yield* session.create({ id: SessionV2.ID.make("ses_fresh_target_replay"), location })
|
||||
const admitted = yield* session.prompt({
|
||||
sessionID: created.id,
|
||||
prompt: Prompt.make({ text: "Replay lifecycle" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Replay lifecycle" }),
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInput.promoteSteers(sourceDb, sourceEvents, created.id)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { AbsolutePath } from "@opencode-ai/core/schema"
|
|||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { Prompt } from "@opencode-ai/core/session/prompt"
|
||||
import { Prompt } from "@opencode-ai/schema/prompt"
|
||||
import { SessionMessageUpdater } from "@opencode-ai/core/session/message-updater"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { DateTime, Effect, Fiber, Layer, Schema, Stream } from "effect"
|
||||
import { mkdtemp, rm } from "fs/promises"
|
||||
import { tmpdir } from "os"
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
import { eq } from "drizzle-orm"
|
||||
|
|
@ -15,7 +17,7 @@ import { Project } from "@opencode-ai/core/project"
|
|||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { Prompt } from "@opencode-ai/core/session/prompt"
|
||||
import { PromptInput } from "@opencode-ai/schema/prompt-input"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
|
|
@ -171,7 +173,7 @@ describe("SessionV2.prompt", () => {
|
|||
|
||||
const message = yield* session.prompt({
|
||||
sessionID,
|
||||
prompt: Prompt.make({ text: "Fix the failing tests" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Fix the failing tests" }),
|
||||
resume: false,
|
||||
})
|
||||
|
||||
|
|
@ -195,7 +197,7 @@ describe("SessionV2.prompt", () => {
|
|||
|
||||
const boundary = yield* session.prompt({
|
||||
sessionID,
|
||||
prompt: Prompt.make({ text: "boundary" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "boundary" }),
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInput.promoteSteers(db, events, sessionID)
|
||||
|
|
@ -207,7 +209,7 @@ describe("SessionV2.prompt", () => {
|
|||
})
|
||||
expect((yield* session.get(sessionID)).revert?.messageID).toBe(boundary.id)
|
||||
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "after revert" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "after revert" }), resume: false })
|
||||
|
||||
expect((yield* session.get(sessionID)).revert).toBeUndefined()
|
||||
expect(
|
||||
|
|
@ -230,17 +232,25 @@ describe("SessionV2.prompt", () => {
|
|||
sessionID,
|
||||
prompt: {
|
||||
text: "Inspect this image",
|
||||
files: [{ uri, name: "image.png" }],
|
||||
files: [{ uri, name: "image.png", mention: { start: 8, end: 17, text: "[Image 1]" } }],
|
||||
},
|
||||
resume: false,
|
||||
})
|
||||
|
||||
expect(message.prompt.files).toEqual([{ uri, name: "image.png", mime: "image/png" }])
|
||||
expect(message.prompt.files).toEqual([
|
||||
{
|
||||
data: uri.slice(uri.indexOf(",") + 1),
|
||||
mime: "image/png",
|
||||
source: { type: "inline" },
|
||||
name: "image.png",
|
||||
mention: { start: 8, end: 17, text: "[Image 1]" },
|
||||
},
|
||||
])
|
||||
expect((yield* admitted(message.id))?.prompt.files).toEqual(message.prompt.files)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("classifies source files and directories from local content", () =>
|
||||
it.effect("materializes selected source file content", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
|
|
@ -253,27 +263,78 @@ describe("SessionV2.prompt", () => {
|
|||
const message = yield* session.prompt({
|
||||
sessionID,
|
||||
prompt: {
|
||||
text: "Inspect these",
|
||||
files: [
|
||||
{ uri: sourceUri.href, name: "main.ts" },
|
||||
{ uri: pathToFileURL(directory).href, name: "source" },
|
||||
],
|
||||
text: "Inspect this",
|
||||
files: [{ uri: sourceUri.href, name: "main.ts" }],
|
||||
},
|
||||
resume: false,
|
||||
})
|
||||
|
||||
expect(message.prompt.files).toEqual([
|
||||
{
|
||||
uri: sourceUri.href,
|
||||
name: "main.ts",
|
||||
data: Buffer.from('import { describe, expect } from "bun:test"').toString("base64"),
|
||||
mime: "text/plain",
|
||||
content: 'import { describe, expect } from "bun:test"',
|
||||
source: { type: "uri", uri: sourceUri.href },
|
||||
name: "main.ts",
|
||||
},
|
||||
{ uri: pathToFileURL(directory).href, name: "source", mime: "application/x-directory" },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects directories as file attachments", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
const uri = pathToFileURL(import.meta.dir).href
|
||||
|
||||
const error = yield* session
|
||||
.prompt({
|
||||
sessionID,
|
||||
prompt: { text: "Inspect this", files: [{ uri, name: "source" }] },
|
||||
resume: false,
|
||||
})
|
||||
.pipe(Effect.flip)
|
||||
|
||||
expect(error).toMatchObject({
|
||||
_tag: "Session.AttachmentError",
|
||||
uri,
|
||||
message: `Attachment is not a file: ${uri}`,
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("materializes local image content before admission", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
const directory = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => mkdtemp(path.join(tmpdir(), "opencode-session-prompt-"))),
|
||||
(directory) => Effect.promise(() => rm(directory, { recursive: true, force: true })),
|
||||
)
|
||||
const source = path.join(directory, "image.png")
|
||||
const bytes = Buffer.from(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
|
||||
"base64",
|
||||
)
|
||||
yield* Effect.promise(() => Bun.write(source, bytes))
|
||||
|
||||
const message = yield* session.prompt({
|
||||
sessionID,
|
||||
prompt: { text: "Inspect this image", files: [{ uri: pathToFileURL(source).href }] },
|
||||
resume: false,
|
||||
})
|
||||
|
||||
expect(message.prompt.files).toEqual([
|
||||
{
|
||||
data: bytes.toString("base64"),
|
||||
mime: "image/png",
|
||||
source: { type: "uri", uri: pathToFileURL(source).href },
|
||||
name: "image.png",
|
||||
},
|
||||
])
|
||||
expect((yield* admitted(message.id))?.prompt.files).toEqual(message.prompt.files)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("sniffs data URL content instead of trusting its declared MIME", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
|
|
@ -286,7 +347,36 @@ describe("SessionV2.prompt", () => {
|
|||
resume: false,
|
||||
})
|
||||
|
||||
expect(message.prompt.files).toEqual([{ uri, name: "main.ts", mime: "text/plain" }])
|
||||
expect(message.prompt.files).toEqual([
|
||||
{
|
||||
data: Buffer.from("export const value = 1\n").toString("base64"),
|
||||
mime: "text/plain",
|
||||
source: { type: "inline" },
|
||||
name: "main.ts",
|
||||
},
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects malformed base64 data URLs", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
const uri = "data:image/png;base64,not-base64"
|
||||
|
||||
const error = yield* session
|
||||
.prompt({
|
||||
sessionID,
|
||||
prompt: { text: "Inspect this", files: [{ uri, name: "image.png" }] },
|
||||
resume: false,
|
||||
})
|
||||
.pipe(Effect.flip)
|
||||
|
||||
expect(error).toMatchObject({
|
||||
_tag: "Session.AttachmentError",
|
||||
uri,
|
||||
message: "Invalid attachment data URL",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -303,8 +393,8 @@ describe("SessionV2.prompt", () => {
|
|||
const fiber = yield* publicEvents({ sessionID }).pipe(Stream.take(4), Stream.runCollect, Effect.forkScoped)
|
||||
yield* Effect.yieldNow
|
||||
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Second" }), resume: false })
|
||||
yield* SessionInput.promoteSteers(db, events, sessionID)
|
||||
const streamed = Array.from(yield* Fiber.join(fiber))
|
||||
|
||||
|
|
@ -328,7 +418,7 @@ describe("SessionV2.prompt", () => {
|
|||
const session = yield* SessionV2.Service
|
||||
const message = yield* session.prompt({
|
||||
sessionID,
|
||||
prompt: Prompt.make({ text: "Fix the failing tests" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Fix the failing tests" }),
|
||||
resume: false,
|
||||
})
|
||||
|
||||
|
|
@ -347,7 +437,7 @@ describe("SessionV2.prompt", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
const input = { sessionID, prompt: Prompt.make({ text: "Fix the failing tests" }), resume: false }
|
||||
const input = { sessionID, prompt: PromptInput.Prompt.make({ text: "Fix the failing tests" }), resume: false }
|
||||
|
||||
const first = yield* session.prompt(input)
|
||||
const second = yield* session.prompt(input)
|
||||
|
|
@ -365,7 +455,7 @@ describe("SessionV2.prompt", () => {
|
|||
const input = {
|
||||
sessionID,
|
||||
id: messageID,
|
||||
prompt: Prompt.make({ text: "Fix the failing tests" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Fix the failing tests" }),
|
||||
resume: false,
|
||||
}
|
||||
|
||||
|
|
@ -385,7 +475,7 @@ describe("SessionV2.prompt", () => {
|
|||
const input = {
|
||||
sessionID,
|
||||
id: messageID,
|
||||
prompt: Prompt.make({ text: "Recover committed prompt" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Recover committed prompt" }),
|
||||
resume: false,
|
||||
}
|
||||
const first = yield* session.prompt(input)
|
||||
|
|
@ -406,13 +496,13 @@ describe("SessionV2.prompt", () => {
|
|||
yield* session.prompt({
|
||||
sessionID,
|
||||
id: messageID,
|
||||
prompt: Prompt.make({ text: "Fix the failing tests" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Fix the failing tests" }),
|
||||
})
|
||||
const failure = yield* session
|
||||
.prompt({
|
||||
sessionID,
|
||||
id: messageID,
|
||||
prompt: Prompt.make({ text: "Delete the failing tests" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Delete the failing tests" }),
|
||||
resume: false,
|
||||
})
|
||||
.pipe(Effect.flip)
|
||||
|
|
@ -431,14 +521,14 @@ describe("SessionV2.prompt", () => {
|
|||
yield* session.prompt({
|
||||
id: messageID,
|
||||
sessionID,
|
||||
prompt: Prompt.make({ text: "Fix the failing tests" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Fix the failing tests" }),
|
||||
resume: false,
|
||||
})
|
||||
const failure = yield* session
|
||||
.prompt({
|
||||
id: messageID,
|
||||
sessionID,
|
||||
prompt: Prompt.make({ text: "Fix the failing tests" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Fix the failing tests" }),
|
||||
delivery: "queue",
|
||||
resume: false,
|
||||
})
|
||||
|
|
@ -455,7 +545,7 @@ describe("SessionV2.prompt", () => {
|
|||
const input = {
|
||||
sessionID,
|
||||
id: messageID,
|
||||
prompt: Prompt.make({ text: "Fix the failing tests" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Fix the failing tests" }),
|
||||
resume: false,
|
||||
}
|
||||
|
||||
|
|
@ -474,7 +564,7 @@ describe("SessionV2.prompt", () => {
|
|||
const { db } = yield* Database.Service
|
||||
const session = yield* SessionV2.Service
|
||||
const events = yield* EventV2.Service
|
||||
yield* session.prompt({ id: messageID, sessionID, prompt: Prompt.make({ text: "Promote once" }), resume: false })
|
||||
yield* session.prompt({ id: messageID, sessionID, prompt: PromptInput.Prompt.make({ text: "Promote once" }), resume: false })
|
||||
|
||||
yield* Effect.all(
|
||||
[SessionInput.promoteSteers(db, events, sessionID), SessionInput.promoteSteers(db, events, sessionID)],
|
||||
|
|
@ -499,7 +589,7 @@ describe("SessionV2.prompt", () => {
|
|||
yield* session.prompt({
|
||||
id: messageID,
|
||||
sessionID,
|
||||
prompt: Prompt.make({ text: "Replay pending" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Replay pending" }),
|
||||
resume: false,
|
||||
})
|
||||
const recorded = yield* db
|
||||
|
|
@ -552,7 +642,7 @@ describe("SessionV2.prompt", () => {
|
|||
.onConflictDoNothing()
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
const prompt = Prompt.make({ text: "Fix the failing tests" })
|
||||
const prompt = PromptInput.Prompt.make({ text: "Fix the failing tests" })
|
||||
|
||||
yield* session.prompt({ id: messageID, sessionID, prompt, resume: false })
|
||||
const failure = yield* session
|
||||
|
|
@ -586,7 +676,7 @@ describe("SessionV2.prompt", () => {
|
|||
.pipe(Effect.orDie)
|
||||
|
||||
const failure = yield* session
|
||||
.prompt({ id: messageID, sessionID, prompt: Prompt.make({ text: "Conflicting prompt" }), resume: false })
|
||||
.prompt({ id: messageID, sessionID, prompt: PromptInput.Prompt.make({ text: "Conflicting prompt" }), resume: false })
|
||||
.pipe(Effect.flip)
|
||||
|
||||
expect(failure).toMatchObject({ _tag: "Session.PromptConflictError", sessionID, messageID })
|
||||
|
|
@ -601,7 +691,7 @@ describe("SessionV2.prompt", () => {
|
|||
executionCalls.length = 0
|
||||
wakeCalls.length = 0
|
||||
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Run by default" }) })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Run by default" }) })
|
||||
|
||||
expect(executionCalls).toEqual([])
|
||||
expect(wakeCalls).toEqual([sessionID])
|
||||
|
|
@ -617,7 +707,7 @@ describe("SessionV2.prompt", () => {
|
|||
|
||||
yield* session.prompt({
|
||||
sessionID,
|
||||
prompt: Prompt.make({ text: "Run explicitly" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Run explicitly" }),
|
||||
resume: true,
|
||||
})
|
||||
|
||||
|
|
@ -633,7 +723,7 @@ describe("SessionV2.prompt", () => {
|
|||
executionCalls.length = 0
|
||||
wakeCalls.length = 0
|
||||
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Do not run" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Do not run" }), resume: false })
|
||||
|
||||
expect(executionCalls).toEqual([])
|
||||
expect(wakeCalls).toEqual([])
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import * as OpenAIChat from "@opencode-ai/llm/protocols/openai-chat"
|
|||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { AgentAttachment, FileAttachment } from "@opencode-ai/core/session/prompt"
|
||||
import { AgentAttachment, Base64, FileAttachment } from "@opencode-ai/schema/prompt"
|
||||
import { toLLMMessages } from "@opencode-ai/core/session/runner/to-llm-message"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { Shell } from "@opencode-ai/schema/shell"
|
||||
|
|
@ -49,7 +49,12 @@ describe("toLLMMessages", () => {
|
|||
})
|
||||
|
||||
test("maps every top-level V2 Session message type", () => {
|
||||
const file = FileAttachment.make({ uri: "data:image/png;base64,aGVsbG8=", mime: "image/png", name: "hello.png" })
|
||||
const file = FileAttachment.make({
|
||||
data: Base64.make("aGVsbG8="),
|
||||
mime: "image/png",
|
||||
source: { type: "inline" },
|
||||
name: "hello.png",
|
||||
})
|
||||
const messages = toLLMMessages(
|
||||
[
|
||||
SessionMessage.AgentSelected.make({
|
||||
|
|
@ -122,7 +127,7 @@ describe("toLLMMessages", () => {
|
|||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Inspect this image" },
|
||||
{ type: "media", mediaType: "image/png", data: "data:image/png;base64,aGVsbG8=", filename: "hello.png" },
|
||||
{ type: "media", mediaType: "image/png", data: "aGVsbG8=", filename: "hello.png" },
|
||||
],
|
||||
metadata: { agents: [{ name: "build" }] },
|
||||
}),
|
||||
|
|
@ -151,9 +156,9 @@ Recent work
|
|||
|
||||
test("lowers text attachments as separate user messages", () => {
|
||||
const file = FileAttachment.make({
|
||||
uri: "file:///project/main.ts",
|
||||
data: Base64.make(Buffer.from("export const value = 1").toString("base64")),
|
||||
mime: "text/plain",
|
||||
content: "export const value = 1",
|
||||
source: { type: "uri", uri: "file:///project/main.ts" },
|
||||
name: "main.ts",
|
||||
})
|
||||
const messages = toLLMMessages(
|
||||
|
|
@ -175,10 +180,10 @@ Recent work
|
|||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Attached file: main.ts\nSource: file:///project/main.ts\n\nexport const value = 1",
|
||||
text: "Attached file: main.ts\n\nexport const value = 1",
|
||||
},
|
||||
],
|
||||
metadata: { attachment: { uri: "file:///project/main.ts", name: "main.ts" } },
|
||||
metadata: { attachment: { source: file.source, name: "main.ts" } },
|
||||
})
|
||||
expect(messages[1]).toMatchObject({
|
||||
id: id("user-text-file"),
|
||||
|
|
@ -187,15 +192,21 @@ Recent work
|
|||
})
|
||||
})
|
||||
|
||||
test("derives text attachment content from durable data URLs", () => {
|
||||
const uri = `data:text/plain;base64,${Buffer.from("inline content").toString("base64")}`
|
||||
test("decodes inline text attachment content", () => {
|
||||
const messages = toLLMMessages(
|
||||
[
|
||||
SessionMessage.User.make({
|
||||
id: id("user-data-file"),
|
||||
type: "user",
|
||||
text: "Review this file",
|
||||
files: [FileAttachment.make({ uri, mime: "text/plain", name: "inline.txt" })],
|
||||
files: [
|
||||
FileAttachment.make({
|
||||
data: Base64.make(Buffer.from("inline content").toString("base64")),
|
||||
mime: "text/plain",
|
||||
source: { type: "inline" },
|
||||
name: "inline.txt",
|
||||
}),
|
||||
],
|
||||
time: { created },
|
||||
}),
|
||||
],
|
||||
|
|
@ -205,11 +216,40 @@ Recent work
|
|||
expect(messages[0]?.content).toEqual([
|
||||
{
|
||||
type: "text",
|
||||
text: `Attached file: inline.txt\nSource: ${uri}\n\ninline content`,
|
||||
text: "Attached file: inline.txt\n\ninline content",
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("uses materialized image data as provider media and drops unsupported attachments", () => {
|
||||
const data = Base64.make("AAECAw==")
|
||||
const messages = toLLMMessages(
|
||||
[
|
||||
SessionMessage.User.make({
|
||||
id: id("user-local-image"),
|
||||
type: "user",
|
||||
text: "Inspect this image",
|
||||
files: [
|
||||
FileAttachment.make({ data, mime: "image/png", source: { type: "inline" }, name: "image.png" }),
|
||||
FileAttachment.make({
|
||||
data: Base64.make("JVBERg=="),
|
||||
mime: "application/pdf",
|
||||
source: { type: "inline" },
|
||||
name: "document.pdf",
|
||||
}),
|
||||
],
|
||||
time: { created },
|
||||
}),
|
||||
],
|
||||
model,
|
||||
)
|
||||
|
||||
expect(messages[0]?.content).toEqual([
|
||||
{ type: "text", text: "Inspect this image" },
|
||||
{ type: "media", mediaType: "image/png", data, filename: "image.png" },
|
||||
])
|
||||
})
|
||||
|
||||
test("replays durable tool media into canonical tool messages without structured base64", () => {
|
||||
const messages = toLLMMessages(
|
||||
[
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import { SessionV2 } from "@opencode-ai/core/session"
|
|||
import { Snapshot } from "@opencode-ai/core/snapshot"
|
||||
import { SessionCompaction } from "@opencode-ai/core/session/compaction"
|
||||
import { SessionTitle } from "@opencode-ai/core/session/title"
|
||||
import { Prompt } from "@opencode-ai/core/session/prompt"
|
||||
import { PromptInput } from "@opencode-ai/schema/prompt-input"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
import { SessionRunCoordinator } from "@opencode-ai/core/session/run-coordinator"
|
||||
|
|
@ -172,7 +172,7 @@ describe("SessionRunnerLLM recorded", () => {
|
|||
const session = yield* SessionV2.Service
|
||||
const prompt = yield* session.prompt({
|
||||
sessionID,
|
||||
prompt: Prompt.make({ text: "Say hello in one short sentence." }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Say hello in one short sentence." }),
|
||||
resume: false,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import { SessionCompaction } from "@opencode-ai/core/session/compaction"
|
|||
import { SessionTitle } from "@opencode-ai/core/session/title"
|
||||
import { SessionInput } from "@opencode-ai/core/session/input"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { Prompt } from "@opencode-ai/core/session/prompt"
|
||||
import { PromptInput } from "@opencode-ai/schema/prompt-input"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
import { SessionRunCoordinator } from "@opencode-ai/core/session/run-coordinator"
|
||||
|
|
@ -375,7 +375,7 @@ const setupOverflowRecovery = Effect.gen(function* () {
|
|||
response = fragmentFixture("text", "text-earlier", ["Earlier answer"]).completeEvents
|
||||
yield* session.prompt({
|
||||
sessionID,
|
||||
prompt: Prompt.make({ text: "Earlier question ".repeat(700) }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Earlier question ".repeat(700) }),
|
||||
resume: false,
|
||||
})
|
||||
yield* session.resume(sessionID)
|
||||
|
|
@ -516,7 +516,7 @@ const verifyEphemeralDeltas = (kind: FragmentKind) =>
|
|||
const chunks = Array.from({ length: 32 }, (_, index) => `${index},`)
|
||||
const fixture = fragmentFixture(kind, fragmentID(kind, "many"), chunks)
|
||||
const expectedContext = [{ type: "user", text: prompt }, fixture.expectedAssistant]
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: prompt }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: prompt }), resume: false })
|
||||
const events = yield* EventV2.Service
|
||||
const live = yield* events.subscribe(fixture.delta).pipe(Stream.take(32), Stream.runCollect, Effect.forkScoped)
|
||||
yield* Effect.yieldNow
|
||||
|
|
@ -547,7 +547,7 @@ const verifyPartialFlushOnFailure = (kind: FragmentKind) =>
|
|||
const prompt = `Fail after ${kind}`
|
||||
const fixture = fragmentFixture(kind, fragmentID(kind, "partial"), ["Partial"])
|
||||
const failure = providerUnavailable()
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: prompt }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: prompt }), resume: false })
|
||||
responseStream = Stream.concat(Stream.fromIterable(fixture.partialEvents), Stream.fail(failure))
|
||||
|
||||
expect(yield* session.resume(sessionID).pipe(Effect.flip)).toBe(failure)
|
||||
|
|
@ -569,7 +569,7 @@ const verifyPartialFlushOnInterruption = (kind: FragmentKind) =>
|
|||
const prompt = `Interrupt after ${kind}`
|
||||
const fixture = fragmentFixture(kind, fragmentID(kind, "interrupted"), ["Partial"])
|
||||
const streamed = yield* Deferred.make<void>()
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: prompt }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: prompt }), resume: false })
|
||||
responseStream = Stream.concat(
|
||||
Stream.fromIterable(fixture.partialEvents),
|
||||
Stream.fromEffect(Deferred.succeed(streamed, undefined)).pipe(Stream.flatMap(() => Stream.never)),
|
||||
|
|
@ -613,7 +613,7 @@ describe("SessionRunnerLLM", () => {
|
|||
}),
|
||||
}),
|
||||
})
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Use application context" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Use application context" }), resume: false })
|
||||
responses = [
|
||||
[
|
||||
LLMEvent.stepStart({ index: 0 }),
|
||||
|
|
@ -661,7 +661,7 @@ describe("SessionRunnerLLM", () => {
|
|||
streamStarted = undefined
|
||||
response = []
|
||||
|
||||
const message = yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Run automatically" }) })
|
||||
const message = yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Run automatically" }) })
|
||||
yield* session.wait(sessionID)
|
||||
|
||||
expect(requests).toHaveLength(1)
|
||||
|
|
@ -675,8 +675,8 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Second" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
responses = undefined
|
||||
|
|
@ -703,7 +703,7 @@ describe("SessionRunnerLLM", () => {
|
|||
const { db } = yield* Database.Service
|
||||
const messageID = SessionMessage.ID.create()
|
||||
systemUnavailable = true
|
||||
yield* session.prompt({ id: messageID, sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
|
||||
yield* session.prompt({ id: messageID, sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
|
||||
requests.length = 0
|
||||
|
||||
const exit = yield* session.resume(sessionID).pipe(Effect.exit)
|
||||
|
|
@ -721,7 +721,7 @@ describe("SessionRunnerLLM", () => {
|
|||
).toBeUndefined()
|
||||
|
||||
systemUnavailable = false
|
||||
yield* session.prompt({ id: messageID, sessionID, prompt: Prompt.make({ text: "First" }) })
|
||||
yield* session.prompt({ id: messageID, sessionID, prompt: PromptInput.Prompt.make({ text: "First" }) })
|
||||
yield* session.wait(sessionID)
|
||||
|
||||
expect(requests).toHaveLength(1)
|
||||
|
|
@ -735,7 +735,7 @@ describe("SessionRunnerLLM", () => {
|
|||
const session = yield* SessionV2.Service
|
||||
const events = yield* EventV2.Service
|
||||
const { db } = yield* Database.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
|
||||
requests.length = 0
|
||||
response = []
|
||||
yield* session.resume(sessionID)
|
||||
|
|
@ -752,7 +752,7 @@ describe("SessionRunnerLLM", () => {
|
|||
.get(),
|
||||
).toBeUndefined()
|
||||
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Second" }), resume: false })
|
||||
const exit = yield* session.resume(sessionID).pipe(Effect.exit)
|
||||
|
||||
expect(Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)).toBe(true)
|
||||
|
|
@ -766,7 +766,7 @@ describe("SessionRunnerLLM", () => {
|
|||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
const { db } = yield* Database.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
|
||||
response = []
|
||||
yield* session.resume(sessionID)
|
||||
|
||||
|
|
@ -795,7 +795,7 @@ describe("SessionRunnerLLM", () => {
|
|||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
const { db } = yield* Database.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
|
||||
response = []
|
||||
yield* session.resume(sessionID)
|
||||
yield* db
|
||||
|
|
@ -804,7 +804,7 @@ describe("SessionRunnerLLM", () => {
|
|||
.where(eq(InstructionCheckpointTable.session_id, sessionID))
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Second" }), resume: false })
|
||||
requests.length = 0
|
||||
|
||||
yield* session.resume(sessionID)
|
||||
|
|
@ -828,13 +828,13 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
response = []
|
||||
yield* session.resume(sessionID)
|
||||
systemBaseline = "Changed context"
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Second" }), resume: false })
|
||||
yield* session.resume(sessionID)
|
||||
|
||||
expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([
|
||||
|
|
@ -863,7 +863,7 @@ describe("SessionRunnerLLM", () => {
|
|||
yield* setup
|
||||
currentModel = Model.make({ id: "gpt-5", provider: "openai", route: OpenAIChat.route })
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
response = fragmentFixture("text", "text-provider-prompt", ["Done"]).completeEvents
|
||||
|
|
@ -888,7 +888,7 @@ describe("SessionRunnerLLM", () => {
|
|||
}),
|
||||
)
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
response = fragmentFixture("text", "text-empty-agent-system", ["Done"]).completeEvents
|
||||
|
|
@ -912,7 +912,7 @@ describe("SessionRunnerLLM", () => {
|
|||
}),
|
||||
)
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
response = fragmentFixture("text", "text-build", ["Done"]).completeEvents
|
||||
|
|
@ -938,7 +938,7 @@ describe("SessionRunnerLLM", () => {
|
|||
editor.default(AgentV2.ID.make("reviewer"))
|
||||
})
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
response = fragmentFixture("text", "text-reviewer", ["Done"]).completeEvents
|
||||
|
|
@ -960,7 +960,7 @@ describe("SessionRunnerLLM", () => {
|
|||
}),
|
||||
)
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
response = fragmentFixture("text", "text-no-system", ["Done"]).completeEvents
|
||||
|
|
@ -988,7 +988,7 @@ describe("SessionRunnerLLM", () => {
|
|||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
response = fragmentFixture("text", "text-selected", ["Done"]).completeEvents
|
||||
|
|
@ -1005,7 +1005,7 @@ describe("SessionRunnerLLM", () => {
|
|||
const session = yield* SessionV2.Service
|
||||
const events = yield* EventV2.Service
|
||||
skillBaselines.set(AgentV2.ID.make("build"), "Build skills")
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
response = []
|
||||
|
|
@ -1015,7 +1015,7 @@ describe("SessionRunnerLLM", () => {
|
|||
sessionID,
|
||||
agent: "reviewer",
|
||||
})
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Second" }), resume: false })
|
||||
yield* session.resume(sessionID)
|
||||
|
||||
expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([
|
||||
|
|
@ -1044,7 +1044,7 @@ describe("SessionRunnerLLM", () => {
|
|||
})
|
||||
.pipe(Effect.asVoid)
|
||||
})
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
response = []
|
||||
|
|
@ -1072,7 +1072,7 @@ describe("SessionRunnerLLM", () => {
|
|||
})
|
||||
.pipe(Effect.asVoid)
|
||||
})
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
response = []
|
||||
|
|
@ -1088,13 +1088,13 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
response = []
|
||||
yield* session.resume(sessionID)
|
||||
systemRemoved = true
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Second" }), resume: false })
|
||||
yield* session.resume(sessionID)
|
||||
|
||||
expect(requests[1]?.messages.map((message) => message.role)).toEqual(["user", "system", "user"])
|
||||
|
|
@ -1111,7 +1111,7 @@ describe("SessionRunnerLLM", () => {
|
|||
const session = yield* SessionV2.Service
|
||||
const contextEntries = yield* InstructionEntry.Service
|
||||
yield* contextEntries.put({ sessionID, key: "deploy-target", value: "production" })
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
response = []
|
||||
|
|
@ -1125,7 +1125,7 @@ describe("SessionRunnerLLM", () => {
|
|||
|
||||
// Non-string JSON pretty-prints; the change narrates as a System update.
|
||||
yield* contextEntries.put({ sessionID, key: "deploy-target", value: { region: "us-east-1" } })
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Second" }), resume: false })
|
||||
yield* session.resume(sessionID)
|
||||
|
||||
expect(requests[1]?.messages.map((message) => message.role)).toEqual(["user", "system", "user"])
|
||||
|
|
@ -1146,7 +1146,7 @@ describe("SessionRunnerLLM", () => {
|
|||
|
||||
// Deleting the row announces removal through the stored removal text.
|
||||
yield* contextEntries.remove({ sessionID, key: "deploy-target" })
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Third" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Third" }), resume: false })
|
||||
yield* session.resume(sessionID)
|
||||
|
||||
expect(requests[2]?.messages.map((message) => message.role)).toEqual(["user", "system", "user", "system", "user"])
|
||||
|
|
@ -1162,20 +1162,20 @@ describe("SessionRunnerLLM", () => {
|
|||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
const events = yield* EventV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
response = []
|
||||
yield* session.resume(sessionID)
|
||||
systemBaseline = "Changed context"
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Second" }), resume: false })
|
||||
yield* session.resume(sessionID)
|
||||
yield* events.publish(SessionEvent.ModelSelected, {
|
||||
sessionID,
|
||||
model: { id: ModelV2.ID.make("replacement"), providerID: ProviderV2.ID.make("fake") },
|
||||
})
|
||||
systemBaseline = "Replacement context"
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Third" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Third" }), resume: false })
|
||||
yield* session.resume(sessionID)
|
||||
|
||||
expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([
|
||||
|
|
@ -1195,7 +1195,7 @@ describe("SessionRunnerLLM", () => {
|
|||
])
|
||||
yield* replaySessionProjection(sessionID)
|
||||
expect(yield* session.messages({ sessionID })).toHaveLength(6)
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Fourth" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Fourth" }), resume: false })
|
||||
yield* session.resume(sessionID)
|
||||
}),
|
||||
)
|
||||
|
|
@ -1205,7 +1205,7 @@ describe("SessionRunnerLLM", () => {
|
|||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
const events = yield* EventV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
response = []
|
||||
|
|
@ -1215,11 +1215,11 @@ describe("SessionRunnerLLM", () => {
|
|||
model: { id: ModelV2.ID.make("replacement"), providerID: ProviderV2.ID.make("fake") },
|
||||
})
|
||||
systemUnavailable = true
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Second" }), resume: false })
|
||||
yield* session.resume(sessionID)
|
||||
systemUnavailable = false
|
||||
systemBaseline = "Replacement context"
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Third" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Third" }), resume: false })
|
||||
yield* session.resume(sessionID)
|
||||
|
||||
expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([
|
||||
|
|
@ -1235,7 +1235,7 @@ describe("SessionRunnerLLM", () => {
|
|||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
const events = yield* EventV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
response = []
|
||||
|
|
@ -1252,7 +1252,7 @@ describe("SessionRunnerLLM", () => {
|
|||
recent: "",
|
||||
})
|
||||
systemBaseline = "Replacement context"
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Second" }), resume: false })
|
||||
yield* session.resume(sessionID)
|
||||
|
||||
expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([
|
||||
|
|
@ -1260,7 +1260,7 @@ describe("SessionRunnerLLM", () => {
|
|||
[defaultSystem, "Replacement context"],
|
||||
])
|
||||
yield* replaySessionProjection(sessionID)
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Third" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Third" }), resume: false })
|
||||
yield* session.resume(sessionID)
|
||||
}),
|
||||
)
|
||||
|
|
@ -1272,7 +1272,7 @@ describe("SessionRunnerLLM", () => {
|
|||
response = fragmentFixture("text", "text-first", ["Earlier answer"]).completeEvents
|
||||
yield* session.prompt({
|
||||
sessionID,
|
||||
prompt: Prompt.make({ text: "Earlier question ".repeat(180) }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Earlier question ".repeat(180) }),
|
||||
resume: false,
|
||||
})
|
||||
yield* session.resume(sessionID)
|
||||
|
|
@ -1285,7 +1285,7 @@ describe("SessionRunnerLLM", () => {
|
|||
]
|
||||
yield* session.prompt({
|
||||
sessionID,
|
||||
prompt: Prompt.make({ text: "Recent exact request ".repeat(180) }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Recent exact request ".repeat(180) }),
|
||||
resume: false,
|
||||
})
|
||||
yield* session.resume(sessionID)
|
||||
|
|
@ -1311,7 +1311,7 @@ describe("SessionRunnerLLM", () => {
|
|||
]
|
||||
yield* session.prompt({
|
||||
sessionID,
|
||||
prompt: Prompt.make({ text: "Newest exact request ".repeat(180) }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Newest exact request ".repeat(180) }),
|
||||
resume: false,
|
||||
})
|
||||
yield* session.resume(sessionID)
|
||||
|
|
@ -1339,7 +1339,7 @@ describe("SessionRunnerLLM", () => {
|
|||
fragmentFixture("text", "text-summary", ["## Goal\n- Recover overflow"]).completeEvents,
|
||||
fragmentFixture("text", "text-final", ["Recovered"]).completeEvents,
|
||||
]
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Continue" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Continue" }), resume: false })
|
||||
yield* session.resume(sessionID)
|
||||
|
||||
expect(requests).toHaveLength(3)
|
||||
|
|
@ -1369,7 +1369,7 @@ describe("SessionRunnerLLM", () => {
|
|||
fragmentFixture("text", "text-summary", ["## Goal\n- Recover once"]).completeEvents,
|
||||
overflow(),
|
||||
]
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Continue" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Continue" }), resume: false })
|
||||
yield* session.resume(sessionID)
|
||||
|
||||
expect(requests).toHaveLength(3)
|
||||
|
|
@ -1397,7 +1397,7 @@ describe("SessionRunnerLLM", () => {
|
|||
fragmentFixture("text", "text-summary", ["## Goal\n- Recover raw overflow"]).completeEvents,
|
||||
fragmentFixture("text", "text-final", ["Recovered"]).completeEvents,
|
||||
]
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Continue" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Continue" }), resume: false })
|
||||
yield* session.resume(sessionID)
|
||||
|
||||
expect(requests).toHaveLength(3)
|
||||
|
|
@ -1415,7 +1415,7 @@ describe("SessionRunnerLLM", () => {
|
|||
[LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" })],
|
||||
[LLMEvent.providerError({ message: "summary unavailable" })],
|
||||
]
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Continue" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Continue" }), resume: false })
|
||||
yield* session.resume(sessionID)
|
||||
|
||||
expect(requests).toHaveLength(2)
|
||||
|
|
@ -1438,7 +1438,7 @@ describe("SessionRunnerLLM", () => {
|
|||
const firstGate = yield* Deferred.make<void>()
|
||||
const summaryGate = yield* Deferred.make<void>()
|
||||
streamGate = firstGate
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Continue" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Continue" }), resume: false })
|
||||
const run = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
||||
while (requests.length < 1) yield* Effect.yieldNow
|
||||
streamGate = summaryGate
|
||||
|
|
@ -1458,13 +1458,13 @@ describe("SessionRunnerLLM", () => {
|
|||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
const events = yield* EventV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
response = []
|
||||
yield* session.resume(sessionID)
|
||||
systemBaseline = "Changed context"
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Second" }), resume: false })
|
||||
yield* session.resume(sessionID)
|
||||
const compactionID = SessionMessage.ID.create()
|
||||
yield* events.publish(SessionEvent.Compaction.Started, {
|
||||
|
|
@ -1478,7 +1478,7 @@ describe("SessionRunnerLLM", () => {
|
|||
recent: "",
|
||||
})
|
||||
systemUnavailable = true
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Third" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Third" }), resume: false })
|
||||
yield* session.resume(sessionID)
|
||||
|
||||
// The rebaseline proceeds while the source is unobservable, restating the model's belief.
|
||||
|
|
@ -1491,7 +1491,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Use tools" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Use tools" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
responses = undefined
|
||||
|
|
@ -1589,7 +1589,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Echo this" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Echo this" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
authorizations.length = 0
|
||||
|
|
@ -1648,7 +1648,7 @@ describe("SessionRunnerLLM", () => {
|
|||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
const events = yield* EventV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Echo this" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Echo this" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
responses = [
|
||||
|
|
@ -1690,7 +1690,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Think first" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Think first" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
response = [
|
||||
|
|
@ -1728,7 +1728,7 @@ describe("SessionRunnerLLM", () => {
|
|||
},
|
||||
])
|
||||
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Continue" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Continue" }), resume: false })
|
||||
response = []
|
||||
yield* session.resume(sessionID)
|
||||
|
||||
|
|
@ -1747,7 +1747,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Search first" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Search first" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
response = [
|
||||
|
|
@ -1772,7 +1772,7 @@ describe("SessionRunnerLLM", () => {
|
|||
yield* session.resume(sessionID)
|
||||
yield* replaySessionProjection(sessionID)
|
||||
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Continue" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Continue" }), resume: false })
|
||||
response = []
|
||||
yield* session.resume(sessionID)
|
||||
|
||||
|
|
@ -1802,7 +1802,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Echo five times" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Echo five times" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
executions.length = 0
|
||||
|
|
@ -1863,7 +1863,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Echo twice" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Echo twice" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
executions.length = 0
|
||||
|
|
@ -1951,7 +1951,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Run once" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Run once" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
responses = undefined
|
||||
|
|
@ -1990,7 +1990,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Start working" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Start working" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
responses = [
|
||||
|
|
@ -2010,7 +2010,7 @@ describe("SessionRunnerLLM", () => {
|
|||
|
||||
const first = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
||||
yield* Deferred.await(streamStarted)
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Change direction" }) })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Change direction" }) })
|
||||
yield* Deferred.succeed(streamGate, undefined)
|
||||
yield* Fiber.join(first)
|
||||
streamGate = undefined
|
||||
|
|
@ -2033,7 +2033,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Start working" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Start working" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
responses = [
|
||||
|
|
@ -2061,7 +2061,7 @@ describe("SessionRunnerLLM", () => {
|
|||
yield* Deferred.await(streamStarted)
|
||||
yield* session.prompt({
|
||||
sessionID,
|
||||
prompt: Prompt.make({ text: "Wait until continuation ends" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Wait until continuation ends" }),
|
||||
delivery: "queue",
|
||||
})
|
||||
yield* Deferred.succeed(streamGate, undefined)
|
||||
|
|
@ -2081,7 +2081,7 @@ describe("SessionRunnerLLM", () => {
|
|||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
const { db } = yield* Database.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Interrupt current work" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Interrupt current work" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
responses = [
|
||||
|
|
@ -2099,7 +2099,7 @@ describe("SessionRunnerLLM", () => {
|
|||
yield* Deferred.await(streamStarted)
|
||||
yield* session.prompt({
|
||||
sessionID,
|
||||
prompt: Prompt.make({ text: "Run after interrupt" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Run after interrupt" }),
|
||||
delivery: "queue",
|
||||
})
|
||||
yield* session.interrupt(sessionID)
|
||||
|
|
@ -2124,7 +2124,7 @@ describe("SessionRunnerLLM", () => {
|
|||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
const { db } = yield* Database.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Interrupt current work" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Interrupt current work" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
responses = [
|
||||
|
|
@ -2142,7 +2142,7 @@ describe("SessionRunnerLLM", () => {
|
|||
yield* Deferred.await(streamStarted)
|
||||
yield* session.prompt({
|
||||
sessionID,
|
||||
prompt: Prompt.make({ text: "Steer after interrupt" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Steer after interrupt" }),
|
||||
})
|
||||
yield* session.interrupt(sessionID)
|
||||
expect(yield* Fiber.await(run)).toMatchObject({ _tag: "Failure" })
|
||||
|
|
@ -2166,7 +2166,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Start working" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Start working" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
responses = [
|
||||
|
|
@ -2191,8 +2191,8 @@ describe("SessionRunnerLLM", () => {
|
|||
|
||||
const first = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
||||
yield* Deferred.await(streamStarted)
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Queue first" }), delivery: "queue" })
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Queue second" }), delivery: "queue" })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Queue first" }), delivery: "queue" })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Queue second" }), delivery: "queue" })
|
||||
yield* Deferred.succeed(streamGate, undefined)
|
||||
yield* Fiber.join(first)
|
||||
streamGate = undefined
|
||||
|
|
@ -2209,10 +2209,10 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Start steering" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Start steering" }), resume: false })
|
||||
yield* session.prompt({
|
||||
sessionID,
|
||||
prompt: Prompt.make({ text: "Queue for later" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Queue for later" }),
|
||||
delivery: "queue",
|
||||
resume: false,
|
||||
})
|
||||
|
|
@ -2243,7 +2243,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Start working" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Start working" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
responses = [
|
||||
|
|
@ -2274,13 +2274,13 @@ describe("SessionRunnerLLM", () => {
|
|||
|
||||
const first = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
||||
while (requests.length < 1) yield* Effect.yieldNow
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Queue first" }), delivery: "queue" })
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Queue second" }), delivery: "queue" })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Queue first" }), delivery: "queue" })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Queue second" }), delivery: "queue" })
|
||||
streamGate = secondGate
|
||||
yield* Deferred.succeed(firstGate, undefined)
|
||||
while (requests.length < 2) yield* Effect.yieldNow
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Steer before next queued input" }) })
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Also steer before next queued input" }) })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Steer before next queued input" }) })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Also steer before next queued input" }) })
|
||||
yield* Deferred.succeed(secondGate, undefined)
|
||||
yield* Fiber.join(first)
|
||||
streamGate = undefined
|
||||
|
|
@ -2308,7 +2308,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Start working" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Start working" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
responses = [
|
||||
|
|
@ -2328,8 +2328,8 @@ describe("SessionRunnerLLM", () => {
|
|||
|
||||
const first = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
||||
yield* Deferred.await(streamStarted)
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First steer" }) })
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second steer" }) })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First steer" }) })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Second steer" }) })
|
||||
yield* Deferred.succeed(streamGate, undefined)
|
||||
yield* Fiber.join(first)
|
||||
streamGate = undefined
|
||||
|
|
@ -2348,7 +2348,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Start working" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Start working" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
responses = undefined
|
||||
|
|
@ -2359,7 +2359,7 @@ describe("SessionRunnerLLM", () => {
|
|||
|
||||
const first = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
||||
yield* Deferred.await(streamStarted)
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Recover with this" }) })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Recover with this" }) })
|
||||
yield* Deferred.succeed(streamGate, undefined)
|
||||
expect(yield* Fiber.join(first).pipe(Effect.flip)).toBe(streamFailure)
|
||||
|
||||
|
|
@ -2378,7 +2378,7 @@ describe("SessionRunnerLLM", () => {
|
|||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
const events = yield* EventV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Recover interrupted tool" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Recover interrupted tool" }), resume: false })
|
||||
yield* SessionInput.promoteSteers((yield* Database.Service).db, events, sessionID)
|
||||
const assistantMessageID = SessionMessage.ID.create()
|
||||
yield* events.publish(SessionEvent.Step.Started, {
|
||||
|
|
@ -2436,7 +2436,7 @@ describe("SessionRunnerLLM", () => {
|
|||
const events = yield* EventV2.Service
|
||||
yield* session.prompt({
|
||||
sessionID,
|
||||
prompt: Prompt.make({ text: "Recover interrupted hosted tool" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Recover interrupted hosted tool" }),
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInput.promoteSteers((yield* Database.Service).db, events, sessionID)
|
||||
|
|
@ -2492,7 +2492,7 @@ describe("SessionRunnerLLM", () => {
|
|||
const events = yield* EventV2.Service
|
||||
yield* session.prompt({
|
||||
sessionID,
|
||||
prompt: Prompt.make({ text: "Recover interrupted tool input" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Recover interrupted tool input" }),
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInput.promoteSteers((yield* Database.Service).db, events, sessionID)
|
||||
|
|
@ -2528,7 +2528,7 @@ describe("SessionRunnerLLM", () => {
|
|||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({
|
||||
sessionID,
|
||||
prompt: Prompt.make({ text: "Wait in queue" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Wait in queue" }),
|
||||
delivery: "queue",
|
||||
resume: false,
|
||||
})
|
||||
|
|
@ -2550,7 +2550,7 @@ describe("SessionRunnerLLM", () => {
|
|||
const defect = new Error("fail after prompt promotion")
|
||||
let fail = true
|
||||
yield* events.project(SessionEvent.PromptPromoted, () => (fail ? Effect.die(defect) : Effect.void))
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Recover promoted input" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Recover promoted input" }), resume: false })
|
||||
|
||||
expect(yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))).toBe(defect)
|
||||
fail = false
|
||||
|
|
@ -2580,7 +2580,7 @@ describe("SessionRunnerLLM", () => {
|
|||
)
|
||||
yield* session.prompt({
|
||||
sessionID,
|
||||
prompt: Prompt.make({ text: "Run committed promotion" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Run committed promotion" }),
|
||||
resume: false,
|
||||
})
|
||||
|
||||
|
|
@ -2597,8 +2597,8 @@ describe("SessionRunnerLLM", () => {
|
|||
yield* setup
|
||||
yield* insertSession(otherSessionID)
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Run first" }), resume: false })
|
||||
yield* session.prompt({ sessionID: otherSessionID, prompt: Prompt.make({ text: "Run second" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Run first" }), resume: false })
|
||||
yield* session.prompt({ sessionID: otherSessionID, prompt: PromptInput.Prompt.make({ text: "Run second" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
responses = undefined
|
||||
|
|
@ -2635,12 +2635,12 @@ describe("SessionRunnerLLM", () => {
|
|||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({
|
||||
sessionID: longSessionID,
|
||||
prompt: Prompt.make({ text: "Run long session" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Run long session" }),
|
||||
resume: false,
|
||||
})
|
||||
yield* session.prompt({
|
||||
sessionID: otherLongSessionID,
|
||||
prompt: Prompt.make({ text: "Run other long session" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Run other long session" }),
|
||||
resume: false,
|
||||
})
|
||||
|
||||
|
|
@ -2659,7 +2659,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Retry after failure" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Retry after failure" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
responses = undefined
|
||||
|
|
@ -2690,7 +2690,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Call missing" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Call missing" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
responses = [
|
||||
|
|
@ -2736,7 +2736,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Call defect" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Call defect" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
responses = [
|
||||
|
|
@ -2784,7 +2784,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Call storefail" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Call storefail" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
responses = [
|
||||
|
|
@ -2836,7 +2836,7 @@ describe("SessionRunnerLLM", () => {
|
|||
execute: () => Effect.die(new QuestionTool.CancelledError()),
|
||||
}),
|
||||
})
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Ask then stop" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Ask then stop" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
responses = [
|
||||
|
|
@ -2875,7 +2875,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Settle before failing" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Settle before failing" }), resume: false })
|
||||
const failure = providerUnavailable()
|
||||
toolExecutionGate = yield* Deferred.make<void>()
|
||||
responseStream = Stream.concat(
|
||||
|
|
@ -2909,7 +2909,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Interrupt blocked tool" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Interrupt blocked tool" }), resume: false })
|
||||
executions.length = 0
|
||||
toolExecutionGate = yield* Deferred.make<void>()
|
||||
responseStream = Stream.concat(
|
||||
|
|
@ -2959,7 +2959,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Interrupt provider" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Interrupt provider" }), resume: false })
|
||||
requests.length = 0
|
||||
response = []
|
||||
streamGate = yield* Deferred.make<void>()
|
||||
|
|
@ -2987,7 +2987,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Interrupt tool settlement" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Interrupt tool settlement" }), resume: false })
|
||||
executions.length = 0
|
||||
toolExecutionGate = yield* Deferred.make<void>()
|
||||
toolExecutionsStarted = yield* Deferred.make<void>()
|
||||
|
|
@ -3037,7 +3037,7 @@ describe("SessionRunnerLLM", () => {
|
|||
}),
|
||||
)
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Finish at the limit" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Finish at the limit" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
executions.length = 0
|
||||
|
|
@ -3085,7 +3085,7 @@ describe("SessionRunnerLLM", () => {
|
|||
}),
|
||||
)
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Start work" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Start work" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
executions.length = 0
|
||||
|
|
@ -3113,7 +3113,7 @@ describe("SessionRunnerLLM", () => {
|
|||
|
||||
const run = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
||||
yield* Deferred.await(streamStarted)
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Change direction" }) })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Change direction" }) })
|
||||
yield* Deferred.succeed(streamGate, undefined)
|
||||
yield* Fiber.join(run)
|
||||
streamGate = undefined
|
||||
|
|
@ -3131,7 +3131,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Fail durably" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Fail durably" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
responses = undefined
|
||||
|
|
@ -3153,7 +3153,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Fail before step" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Fail before step" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
response = [LLMEvent.providerError({ message: "Provider unavailable" })]
|
||||
|
|
@ -3172,7 +3172,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Fail after output" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Fail after output" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
response = [
|
||||
|
|
@ -3201,7 +3201,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Fail raw stream durably" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Fail raw stream durably" }), resume: false })
|
||||
const failure = providerUnavailable()
|
||||
responseStream = Stream.fail(failure)
|
||||
|
||||
|
|
@ -3220,7 +3220,7 @@ describe("SessionRunnerLLM", () => {
|
|||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({
|
||||
sessionID,
|
||||
prompt: Prompt.make({ text: "Do not continue failed provider" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Do not continue failed provider" }),
|
||||
resume: false,
|
||||
})
|
||||
|
||||
|
|
@ -3243,7 +3243,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Fail hosted tool durably" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Fail hosted tool durably" }), resume: false })
|
||||
|
||||
requests.length = 0
|
||||
response = [
|
||||
|
|
@ -3274,7 +3274,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Fail hosted tool at EOF" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Fail hosted tool at EOF" }), resume: false })
|
||||
response = [
|
||||
LLMEvent.stepStart({ index: 0 }),
|
||||
LLMEvent.toolCall({
|
||||
|
|
@ -3301,7 +3301,7 @@ describe("SessionRunnerLLM", () => {
|
|||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({
|
||||
sessionID,
|
||||
prompt: Prompt.make({ text: "Fail hosted tool on raw failure" }),
|
||||
prompt: PromptInput.Prompt.make({ text: "Fail hosted tool on raw failure" }),
|
||||
resume: false,
|
||||
})
|
||||
const failure = providerUnavailable()
|
||||
|
|
@ -3336,7 +3336,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Two blocks" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Two blocks" }), resume: false })
|
||||
|
||||
responses = undefined
|
||||
streamGate = undefined
|
||||
|
|
@ -3400,7 +3400,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Call provider tool" }), resume: false })
|
||||
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Call provider tool" }), resume: false })
|
||||
|
||||
responses = undefined
|
||||
streamGate = undefined
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
|||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { Prompt } from "@opencode-ai/core/session/prompt"
|
||||
import { Prompt } from "@opencode-ai/schema/prompt"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ import { SessionV2 } from "@opencode-ai/core/session"
|
|||
import { Agent } from "@opencode-ai/schema/agent"
|
||||
import { Location } from "@opencode-ai/schema/location"
|
||||
import { Model } from "@opencode-ai/schema/model"
|
||||
import { AgentAttachment, FileAttachment, Prompt, Source } from "@opencode-ai/schema/prompt"
|
||||
import { Provider } from "@opencode-ai/schema/provider"
|
||||
import { Project } from "@opencode-ai/schema/project"
|
||||
import { ProjectDirectories } from "@opencode-ai/schema/project-directories"
|
||||
import { PermissionV1 } from "@opencode-ai/schema/permission-v1"
|
||||
import { Prompt } from "@opencode-ai/schema/prompt"
|
||||
import { Session } from "@opencode-ai/schema/session"
|
||||
import { SessionInput } from "@opencode-ai/schema/session-input"
|
||||
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||
|
|
@ -47,7 +47,6 @@ test("Core reuses the canonical shared schemas", async () => {
|
|||
coreSessionInput,
|
||||
coreSessionMessage,
|
||||
coreSessionTodo,
|
||||
corePrompt,
|
||||
coreSkill,
|
||||
coreV2Schema,
|
||||
coreSchema,
|
||||
|
|
@ -69,7 +68,6 @@ test("Core reuses the canonical shared schemas", async () => {
|
|||
import("@opencode-ai/core/session/input"),
|
||||
import("@opencode-ai/core/session/message"),
|
||||
import("@opencode-ai/core/session/todo"),
|
||||
import("@opencode-ai/core/session/prompt"),
|
||||
import("@opencode-ai/core/skill"),
|
||||
import("@opencode-ai/core/v2-schema"),
|
||||
import("@opencode-ai/core/schema"),
|
||||
|
|
@ -164,10 +162,6 @@ test("Core reuses the canonical shared schemas", async () => {
|
|||
[coreSessionMessage.Message, SessionMessage.Message],
|
||||
[coreSessionTodo.Info, SessionTodo.Info],
|
||||
[coreSessionTodo.Event, SessionTodo.Event],
|
||||
[corePrompt.Source, Source],
|
||||
[corePrompt.FileAttachment, FileAttachment],
|
||||
[corePrompt.AgentAttachment, AgentAttachment],
|
||||
[corePrompt.Prompt, Prompt],
|
||||
[coreSkill.DirectorySource, Skill.DirectorySource],
|
||||
[coreSkill.UrlSource, Skill.UrlSource],
|
||||
[coreSkill.EmbeddedSource, Skill.EmbeddedSource],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue