mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-22 08:37:36 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e444c4b1b8 | ||
|
|
19e1357a06 | ||
|
|
4b381ac6a1 |
@@ -41,7 +41,7 @@ import { TypeSafeAI } from "@opencode/ai/providers"
|
||||
|
||||
const model = TypeSafeAI.configure().experimental.evaluation("jev-latest")
|
||||
|
||||
const program = Evaluation.evaluate({
|
||||
const program = Evaluation.run({
|
||||
model,
|
||||
state: "I was charged twice. Please refund the duplicate payment.",
|
||||
questions: {
|
||||
|
||||
@@ -218,11 +218,11 @@ export function request(input: EvaluationRequest | EvaluationRequestInput) {
|
||||
})
|
||||
}
|
||||
|
||||
export function evaluate<const Model extends object, const Questions extends EvaluationQuestions>(
|
||||
export function run<const Model extends object, const Questions extends EvaluationQuestions>(
|
||||
input: EvaluationRequestInput<Model, Questions>,
|
||||
): Effect.Effect<EvaluationResponseFor<Questions>, AIError, Service>
|
||||
export function evaluate(input: EvaluationRequest): Effect.Effect<EvaluationResponse, AIError, Service>
|
||||
export function evaluate(input: EvaluationRequest | EvaluationRequestInput) {
|
||||
export function run(input: EvaluationRequest): Effect.Effect<EvaluationResponse, AIError, Service>
|
||||
export function run(input: EvaluationRequest | EvaluationRequestInput) {
|
||||
return Effect.try({
|
||||
try: () => (input instanceof EvaluationRequest ? input : request(input)),
|
||||
catch: (cause) =>
|
||||
@@ -241,5 +241,5 @@ export function evaluate(input: EvaluationRequest | EvaluationRequestInput) {
|
||||
|
||||
export const Evaluation = {
|
||||
request,
|
||||
evaluate,
|
||||
run,
|
||||
} as const
|
||||
|
||||
@@ -9,7 +9,7 @@ import { dynamicResponse } from "./lib/http.js"
|
||||
describe("experimental Evaluation", () => {
|
||||
it.effect("evaluates typed questions through System One", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* Evaluation.evaluate({
|
||||
const response = yield* Evaluation.run({
|
||||
model: TypeSafeAI.configure({
|
||||
apiKey: "test",
|
||||
baseURL: "https://typesafe.test/v1/",
|
||||
@@ -116,7 +116,7 @@ describe("experimental Evaluation", () => {
|
||||
)
|
||||
|
||||
it.effect("configures the OpenCode Zen System One endpoint", () =>
|
||||
Evaluation.evaluate({
|
||||
Evaluation.run({
|
||||
model: OpenCodeZen.configure({ apiKey: "zen-key", baseURL: "https://zen.test/v1" }).experimental.evaluation(
|
||||
"jev-1.13",
|
||||
),
|
||||
@@ -156,7 +156,7 @@ describe("experimental Evaluation", () => {
|
||||
|
||||
it.effect("rejects malformed questions before network I/O", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* Evaluation.evaluate({
|
||||
const error = yield* Evaluation.run({
|
||||
model: TypeSafeAI.experimental.evaluation("jev-latest"),
|
||||
state: "hello",
|
||||
questions: { score: { type: "score", instructions: "How much?", criteria: ["only"] } },
|
||||
|
||||
@@ -31,7 +31,7 @@ void (true satisfies Choice)
|
||||
void (true satisfies ClientRequirements)
|
||||
|
||||
Effect.gen(function* () {
|
||||
const response = yield* Evaluation.evaluate({
|
||||
const response = yield* Evaluation.run({
|
||||
model: OpenCodeZen.experimental.evaluation("jev-1.13"),
|
||||
state: ["hello"],
|
||||
questions: { greeting: { type: "boolean", instructions: "Greeting?" } },
|
||||
@@ -45,14 +45,14 @@ Effect.gen(function* () {
|
||||
|
||||
declare const route: EvaluationRoute<{ readonly temperature?: number }>
|
||||
const custom = EvaluationModel.make({ id: "custom", provider: "custom", route })
|
||||
Evaluation.evaluate({
|
||||
Evaluation.run({
|
||||
model: custom,
|
||||
state: "hello",
|
||||
questions: { ok: { type: "boolean", instructions: "OK?" } },
|
||||
options: { temperature: 0.5 },
|
||||
})
|
||||
// @ts-expect-error Selected evaluation models retain their request option types.
|
||||
Evaluation.evaluate({
|
||||
Evaluation.run({
|
||||
model: custom,
|
||||
state: "hello",
|
||||
questions: { ok: { type: "boolean", instructions: "OK?" } },
|
||||
|
||||
@@ -40,7 +40,7 @@ describe("public exports", () => {
|
||||
expect(TestLLM.layer).toBeFunction()
|
||||
expect(TestLLM.testLayer).toBeFunction()
|
||||
expect(TestLLM.Test.of).toBeFunction()
|
||||
expect(Evaluation.evaluate).toBeFunction()
|
||||
expect(Evaluation.run).toBeFunction()
|
||||
expect(EvaluationClient.layer).toBeDefined()
|
||||
expect(EvaluationClient.fetchLayer).toBeDefined()
|
||||
})
|
||||
|
||||
@@ -63,7 +63,7 @@ const assertEvaluation = (
|
||||
metadataKey: "typesafe" | "opencode",
|
||||
) =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* Evaluation.evaluate({ model, state, questions })
|
||||
const response = yield* Evaluation.run({ model, state, questions })
|
||||
expect(response.model).toStartWith("jev-")
|
||||
expect(response.answers.department.type).toBe("choice")
|
||||
expect(response.answers.department.choice).toBe("billing")
|
||||
|
||||
@@ -703,7 +703,7 @@ function messageContent(
|
||||
return {
|
||||
type: "reasoning",
|
||||
text: part.text,
|
||||
native: jsonRecord(part.metadata),
|
||||
state: jsonRecord(part.metadata),
|
||||
time: part.time
|
||||
? { created: part.time.start, ...(part.time.end === undefined ? {} : { completed: part.time.end }) }
|
||||
: undefined,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { OpenCode } from "@opencode/client"
|
||||
import { Service } from "@opencode/client/effect/service"
|
||||
import { Session } from "@opencode/schema/session"
|
||||
import { SessionMessage } from "@opencode/schema/session-message"
|
||||
import { SessionTransfer } from "@opencode/schema/session-transfer"
|
||||
import { Effect, Option, Predicate, Schema } from "effect"
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import { EOL } from "node:os"
|
||||
import path from "node:path"
|
||||
import { Commands } from "../../commands"
|
||||
@@ -24,13 +23,7 @@ export default Runtime.handler(
|
||||
catch: (cause) =>
|
||||
new Error(`Failed to read session data: ${cause instanceof Error ? cause.message : String(cause)}`),
|
||||
})
|
||||
const raw = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown))(text)
|
||||
// Exports written before provider blobs were renamed to `native` still carry the old keys.
|
||||
const data = yield* Schema.decodeUnknownEffect(SessionTransfer.Data)(
|
||||
Predicate.isObject(raw) && Array.isArray(raw.messages)
|
||||
? { ...raw, messages: raw.messages.map(SessionMessage.persisted) }
|
||||
: raw,
|
||||
)
|
||||
const data = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(SessionTransfer.Data))(text)
|
||||
const encoded = Schema.encodeSync(SessionTransfer.Data)(data)
|
||||
const server = yield* ServerConnection.resolve({
|
||||
server: Option.getOrUndefined(input.server),
|
||||
|
||||
@@ -564,7 +564,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
||||
messageID: message.id,
|
||||
type: "reasoning",
|
||||
text,
|
||||
metadata: item.native,
|
||||
metadata: item.state,
|
||||
time: { start: message.time.created, end: timestamp },
|
||||
}
|
||||
renderedReasoning.set(key, item.text)
|
||||
|
||||
@@ -509,12 +509,12 @@ export type PromptAgentAttachment = { name: string; mention?: PromptMention }
|
||||
|
||||
export type PromptSkillAttachment = { id: string; name: string; text?: string; mention?: PromptMention }
|
||||
|
||||
export type SessionMessageAssistantText = { type: "text"; text: string; native?: SessionMessageProviderState }
|
||||
export type SessionMessageAssistantText = { type: "text"; text: string; state?: SessionMessageProviderState }
|
||||
|
||||
export type SessionMessageAssistantReasoning = {
|
||||
type: "reasoning"
|
||||
text: string
|
||||
native?: SessionMessageProviderState
|
||||
state?: SessionMessageProviderState
|
||||
time?: { created: number; completed?: number }
|
||||
}
|
||||
|
||||
@@ -1354,6 +1354,15 @@ export type SessionToolCalled = {
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionMessageAssistantText1 = { type: "text"; text: string; state?: SessionMessageProviderState1 }
|
||||
|
||||
export type SessionMessageAssistantReasoning1 = {
|
||||
type: "reasoning"
|
||||
text: string
|
||||
state?: SessionMessageProviderState1
|
||||
time?: { created: number; completed?: number }
|
||||
}
|
||||
|
||||
export type ToolContent1 = ToolTextContent | ToolFileContent1
|
||||
|
||||
export type FormNumberField = {
|
||||
@@ -1753,7 +1762,7 @@ export type SessionMessageCompactionCompleted = {
|
||||
status: "completed"
|
||||
reason: "auto" | "manual"
|
||||
model?: ModelRef
|
||||
native?: SessionMessageProviderState
|
||||
providerState?: SessionMessageProviderState
|
||||
summary: string
|
||||
recent: string
|
||||
providerContext?: SessionProviderContext
|
||||
@@ -2219,7 +2228,7 @@ export type SessionMessageAssistant = {
|
||||
snapshot?: { start?: string; end?: string; files?: Array<string> }
|
||||
finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
|
||||
rawFinish?: string
|
||||
native?: SessionMessageProviderState
|
||||
providerState?: SessionMessageProviderState
|
||||
cost?: MoneyUSD
|
||||
tokens?: TokenUsageInfo
|
||||
error?: SessionStructuredError
|
||||
@@ -2227,13 +2236,8 @@ export type SessionMessageAssistant = {
|
||||
}
|
||||
|
||||
export type SessionMessageAssistantContentEncoded =
|
||||
| { type: "text"; text: string; state?: SessionMessageProviderState1 }
|
||||
| {
|
||||
type: "reasoning"
|
||||
text: string
|
||||
time?: { created: number; completed?: number }
|
||||
state?: SessionMessageProviderState1
|
||||
}
|
||||
| SessionMessageAssistantText1
|
||||
| SessionMessageAssistantReasoning1
|
||||
| SessionMessageAssistantTool1
|
||||
|
||||
export type FormInfo = { id: string; sessionID: string; title: string; metadata?: FormMetadata; fields: FormFields }
|
||||
@@ -3098,11 +3102,11 @@ export type SessionImportInput = {
|
||||
readonly agent: string
|
||||
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
||||
readonly content: ReadonlyArray<
|
||||
| { readonly type: "text"; readonly text: string; readonly native?: { readonly [x: string]: JsonValue } }
|
||||
| { readonly type: "text"; readonly text: string; readonly state?: { readonly [x: string]: JsonValue } }
|
||||
| {
|
||||
readonly type: "reasoning"
|
||||
readonly text: string
|
||||
readonly native?: { readonly [x: string]: JsonValue }
|
||||
readonly state?: { readonly [x: string]: JsonValue }
|
||||
readonly time?: { readonly created: number; readonly completed?: number }
|
||||
}
|
||||
| {
|
||||
@@ -3176,7 +3180,7 @@ export type SessionImportInput = {
|
||||
readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray<string> }
|
||||
readonly finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
|
||||
readonly rawFinish?: string
|
||||
readonly native?: { readonly [x: string]: JsonValue }
|
||||
readonly providerState?: { readonly [x: string]: JsonValue }
|
||||
readonly cost?: number
|
||||
readonly tokens?: {
|
||||
readonly input: number
|
||||
@@ -3210,7 +3214,7 @@ export type SessionImportInput = {
|
||||
readonly status: "completed"
|
||||
readonly reason: "auto" | "manual"
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
||||
readonly native?: { readonly [x: string]: JsonValue }
|
||||
readonly providerState?: { readonly [x: string]: JsonValue }
|
||||
readonly summary: string
|
||||
readonly recent: string
|
||||
readonly providerContext?: {
|
||||
@@ -3415,11 +3419,11 @@ export type SessionImportInput = {
|
||||
readonly agent: string
|
||||
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
||||
readonly content: ReadonlyArray<
|
||||
| { readonly type: "text"; readonly text: string; readonly native?: { readonly [x: string]: JsonValue } }
|
||||
| { readonly type: "text"; readonly text: string; readonly state?: { readonly [x: string]: JsonValue } }
|
||||
| {
|
||||
readonly type: "reasoning"
|
||||
readonly text: string
|
||||
readonly native?: { readonly [x: string]: JsonValue }
|
||||
readonly state?: { readonly [x: string]: JsonValue }
|
||||
readonly time?: { readonly created: number; readonly completed?: number }
|
||||
}
|
||||
| {
|
||||
@@ -3493,7 +3497,7 @@ export type SessionImportInput = {
|
||||
readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray<string> }
|
||||
readonly finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
|
||||
readonly rawFinish?: string
|
||||
readonly native?: { readonly [x: string]: JsonValue }
|
||||
readonly providerState?: { readonly [x: string]: JsonValue }
|
||||
readonly cost?: number
|
||||
readonly tokens?: {
|
||||
readonly input: number
|
||||
@@ -3527,7 +3531,7 @@ export type SessionImportInput = {
|
||||
readonly status: "completed"
|
||||
readonly reason: "auto" | "manual"
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
||||
readonly native?: { readonly [x: string]: JsonValue }
|
||||
readonly providerState?: { readonly [x: string]: JsonValue }
|
||||
readonly summary: string
|
||||
readonly recent: string
|
||||
readonly providerContext?: {
|
||||
@@ -3732,11 +3736,11 @@ export type SessionImportInput = {
|
||||
readonly agent: string
|
||||
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
||||
readonly content: ReadonlyArray<
|
||||
| { readonly type: "text"; readonly text: string; readonly native?: { readonly [x: string]: JsonValue } }
|
||||
| { readonly type: "text"; readonly text: string; readonly state?: { readonly [x: string]: JsonValue } }
|
||||
| {
|
||||
readonly type: "reasoning"
|
||||
readonly text: string
|
||||
readonly native?: { readonly [x: string]: JsonValue }
|
||||
readonly state?: { readonly [x: string]: JsonValue }
|
||||
readonly time?: { readonly created: number; readonly completed?: number }
|
||||
}
|
||||
| {
|
||||
@@ -3810,7 +3814,7 @@ export type SessionImportInput = {
|
||||
readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray<string> }
|
||||
readonly finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
|
||||
readonly rawFinish?: string
|
||||
readonly native?: { readonly [x: string]: JsonValue }
|
||||
readonly providerState?: { readonly [x: string]: JsonValue }
|
||||
readonly cost?: number
|
||||
readonly tokens?: {
|
||||
readonly input: number
|
||||
@@ -3844,7 +3848,7 @@ export type SessionImportInput = {
|
||||
readonly status: "completed"
|
||||
readonly reason: "auto" | "manual"
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
||||
readonly native?: { readonly [x: string]: JsonValue }
|
||||
readonly providerState?: { readonly [x: string]: JsonValue }
|
||||
readonly summary: string
|
||||
readonly recent: string
|
||||
readonly providerContext?: {
|
||||
|
||||
@@ -846,7 +846,7 @@ export function createData(config: CreateDataInput) {
|
||||
existing.error = undefined
|
||||
existing.finish = undefined
|
||||
existing.rawFinish = undefined
|
||||
existing.native = undefined
|
||||
existing.providerState = undefined
|
||||
existing.time.created = event.data.started
|
||||
existing.time.streamed = undefined
|
||||
existing.time.completed = undefined
|
||||
@@ -880,7 +880,7 @@ export function createData(config: CreateDataInput) {
|
||||
assistant.time.completed = event.created
|
||||
assistant.finish = event.data.finish
|
||||
assistant.rawFinish = event.data.rawFinish
|
||||
assistant.native = event.data.providerState
|
||||
assistant.providerState = event.data.providerState
|
||||
assistant.cost = event.data.cost
|
||||
assistant.tokens = event.data.tokens
|
||||
if (event.data.snapshot) assistant.snapshot = { ...assistant.snapshot, end: event.data.snapshot }
|
||||
@@ -892,7 +892,7 @@ export function createData(config: CreateDataInput) {
|
||||
assistant.time.completed = event.created
|
||||
assistant.finish = event.data.finish ?? "error"
|
||||
assistant.rawFinish = event.data.rawFinish
|
||||
assistant.native = event.data.providerState
|
||||
assistant.providerState = event.data.providerState
|
||||
assistant.error = event.data.error
|
||||
assistant.retry = undefined
|
||||
if (event.data.cost !== undefined && event.data.tokens !== undefined) {
|
||||
@@ -914,7 +914,6 @@ export function createData(config: CreateDataInput) {
|
||||
case "session.text.ended":
|
||||
message.editText(event.data.sessionID, event.data.assistantMessageID, (text) => {
|
||||
text.text = event.data.text
|
||||
text.native = event.data.state
|
||||
})
|
||||
return
|
||||
case "session.tool.input.started":
|
||||
@@ -985,7 +984,7 @@ export function createData(config: CreateDataInput) {
|
||||
assistant.content.push({
|
||||
type: "reasoning",
|
||||
text: "",
|
||||
native: event.data.state,
|
||||
state: event.data.state,
|
||||
time: { created: event.created },
|
||||
})
|
||||
})
|
||||
@@ -999,7 +998,7 @@ export function createData(config: CreateDataInput) {
|
||||
message.editReasoning(event.data.sessionID, event.data.assistantMessageID, (reasoning) => {
|
||||
reasoning.text = event.data.text
|
||||
reasoning.time = { created: reasoning.time?.created ?? event.created, completed: event.created }
|
||||
if (event.data.state !== undefined) reasoning.native = event.data.state
|
||||
if (event.data.state !== undefined) reasoning.state = event.data.state
|
||||
})
|
||||
return
|
||||
case "session.retry.scheduled":
|
||||
@@ -1106,7 +1105,7 @@ export function createData(config: CreateDataInput) {
|
||||
status: "completed",
|
||||
reason: event.data.reason,
|
||||
model: event.data.model,
|
||||
native: event.data.providerState,
|
||||
providerState: event.data.providerState,
|
||||
providerContext: event.data.providerContext,
|
||||
summary: event.data.text,
|
||||
recent: event.data.recent,
|
||||
@@ -1121,7 +1120,7 @@ export function createData(config: CreateDataInput) {
|
||||
status: "completed",
|
||||
reason: event.data.reason,
|
||||
model: event.data.model,
|
||||
native: event.data.providerState,
|
||||
providerState: event.data.providerState,
|
||||
providerContext: event.data.providerContext,
|
||||
summary: event.data.text,
|
||||
recent: event.data.recent,
|
||||
|
||||
@@ -135,7 +135,7 @@ test.each(["started", "cancelled", "failed"])(
|
||||
status: "completed",
|
||||
summary: "Summary",
|
||||
model,
|
||||
native: providerState,
|
||||
providerState,
|
||||
providerContext,
|
||||
cost: 0.01,
|
||||
tokens,
|
||||
|
||||
@@ -379,13 +379,13 @@ export function transformSession(input: TransformInput): TransformResult {
|
||||
return []
|
||||
const content = owned.flatMap((part): Array<Record<string, unknown>> => {
|
||||
if (part.type === "text")
|
||||
return [{ type: "text", text: part.text, ...(part.metadata ? { native: part.metadata } : {}) }]
|
||||
return [{ type: "text", text: part.text, ...(part.metadata ? { state: part.metadata } : {}) }]
|
||||
if (part.type === "reasoning")
|
||||
return [
|
||||
{
|
||||
type: "reasoning",
|
||||
text: part.text,
|
||||
...(part.metadata ? { native: part.metadata } : {}),
|
||||
...(part.metadata ? { state: part.metadata } : {}),
|
||||
time: { created: part.time.start, ...(part.time.end === undefined ? {} : { completed: part.time.end }) },
|
||||
},
|
||||
]
|
||||
|
||||
@@ -60,7 +60,7 @@ export const latestCompaction = Effect.fnUntraced(function* (
|
||||
})
|
||||
|
||||
export const decodeMessageRow = (row: typeof SessionMessageTable.$inferSelect) =>
|
||||
decode(SessionMessage.persisted({ ...row.data, id: row.id, type: row.type })).pipe(
|
||||
decode({ ...row.data, id: row.id, type: row.type }).pipe(
|
||||
Effect.tap((message) =>
|
||||
SessionProviderContext.isCheckpoint(message)
|
||||
? SessionProviderContext.validate(message.providerContext)
|
||||
|
||||
@@ -125,7 +125,7 @@ const promotedFromMessage = Effect.fn("SessionInbox.promotedFromMessage")(functi
|
||||
if (row === undefined) return undefined
|
||||
if (row.session_id !== sessionID || (row.type !== "user" && row.type !== "synthetic"))
|
||||
return yield* new LifecycleConflict({ id })
|
||||
const message = decodeMessage(SessionMessage.persisted({ ...row.data, id: row.id, type: row.type }))
|
||||
const message = decodeMessage({ ...row.data, id: row.id, type: row.type })
|
||||
const base = { id, sessionID, time: { created: message.time.created }, delivery }
|
||||
if (message.type === "user")
|
||||
return User.make({
|
||||
|
||||
@@ -83,9 +83,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
|
||||
"session.message.content.updated": (event) =>
|
||||
updateOwnedAssistant(event.data.messageID, (draft) => {
|
||||
draft.content = castDraft(
|
||||
Schema.decodeUnknownSync(Schema.Array(SessionMessage.AssistantContent))(
|
||||
SessionMessage.persistedContent(event.data.content),
|
||||
),
|
||||
Schema.decodeUnknownSync(Schema.Array(SessionMessage.AssistantContent))(event.data.content),
|
||||
)
|
||||
}),
|
||||
"session.usage.recorded": () => Effect.void,
|
||||
@@ -224,7 +222,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
|
||||
draft.error = undefined
|
||||
draft.finish = undefined
|
||||
draft.rawFinish = undefined
|
||||
draft.native = undefined
|
||||
draft.providerState = undefined
|
||||
draft.time.created = DateTime.makeUnsafe(event.data.started)
|
||||
draft.time.streamed = undefined
|
||||
draft.time.completed = undefined
|
||||
@@ -265,7 +263,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
|
||||
draft.time.completed = created
|
||||
draft.finish = event.data.finish
|
||||
draft.rawFinish = event.data.rawFinish
|
||||
draft.native = castDraft(event.data.providerState)
|
||||
draft.providerState = castDraft(event.data.providerState)
|
||||
draft.cost = event.data.cost
|
||||
draft.tokens = event.data.tokens
|
||||
projectTerminalSnapshot(draft, event)
|
||||
@@ -276,7 +274,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
|
||||
draft.time.completed = created
|
||||
draft.finish = event.data.finish ?? "error"
|
||||
draft.rawFinish = event.data.rawFinish
|
||||
draft.native = castDraft(event.data.providerState)
|
||||
draft.providerState = castDraft(event.data.providerState)
|
||||
draft.error = castDraft(event.data.error)
|
||||
draft.retry = undefined
|
||||
if (event.data.cost !== undefined && event.data.tokens !== undefined) {
|
||||
@@ -296,7 +294,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
|
||||
const match = latestText(draft)
|
||||
if (match) {
|
||||
match.text = event.data.text
|
||||
match.native = castDraft(event.data.state)
|
||||
match.state = castDraft(event.data.state)
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -384,7 +382,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
|
||||
SessionMessage.AssistantReasoning.make({
|
||||
type: "reasoning",
|
||||
text: "",
|
||||
native: event.data.state,
|
||||
state: event.data.state,
|
||||
time: { created },
|
||||
}),
|
||||
),
|
||||
@@ -397,7 +395,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
|
||||
if (match) {
|
||||
match.text = event.data.text
|
||||
match.time = { created: match.time?.created ?? created, completed: created }
|
||||
if (event.data.state !== undefined) match.native = event.data.state
|
||||
if (event.data.state !== undefined) match.state = event.data.state
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -433,7 +431,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
|
||||
metadata: event.metadata ? { ...current.metadata, ...event.metadata } : current.metadata,
|
||||
reason: event.data.reason,
|
||||
model: event.data.model,
|
||||
native: event.data.providerState,
|
||||
providerState: event.data.providerState,
|
||||
summary: event.data.text,
|
||||
providerContext: event.data.providerContext,
|
||||
recent: event.data.recent,
|
||||
@@ -450,7 +448,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
|
||||
metadata: event.metadata,
|
||||
reason: event.data.reason,
|
||||
model: event.data.model,
|
||||
native: event.data.providerState,
|
||||
providerState: event.data.providerState,
|
||||
summary: event.data.text,
|
||||
providerContext: event.data.providerContext,
|
||||
recent: event.data.recent,
|
||||
|
||||
@@ -228,7 +228,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
|
||||
function run(db: DatabaseService, event: MessageEvent) {
|
||||
return Effect.gen(function* () {
|
||||
const decodeRow = (row: typeof SessionMessageTable.$inferSelect) =>
|
||||
decodeMessage(SessionMessage.persisted({ ...row.data, id: row.id, type: row.type }))
|
||||
decodeMessage({ ...row.data, id: row.id, type: row.type })
|
||||
const updateMessage = (message: SessionMessage.Info) => {
|
||||
const encoded = encodeMessage(message)
|
||||
const { id, type, ...data } = encoded
|
||||
|
||||
@@ -107,9 +107,7 @@ const plan = Effect.fn("SessionRevert.plan")(function* (db: Database.Interface["
|
||||
const decode = Schema.decodeUnknownEffect(SessionMessage.Info)
|
||||
const files = new Map<RelativePath, Snapshot.ID>()
|
||||
for (const row of rows) {
|
||||
const message = yield* decode(SessionMessage.persisted({ ...row.data, id: row.id, type: row.type })).pipe(
|
||||
Effect.orDie,
|
||||
)
|
||||
const message = yield* decode({ ...row.data, id: row.id, type: row.type }).pipe(Effect.orDie)
|
||||
if (message.type !== "assistant" || !message.snapshot?.start) continue
|
||||
for (const file of message.snapshot.files ?? [])
|
||||
if (!files.has(file)) files.set(file, Snapshot.ID.make(message.snapshot.start))
|
||||
|
||||
@@ -162,7 +162,7 @@ const assistant = (message: SessionMessage.Assistant, model: Model.Ref, provider
|
||||
text: item.text,
|
||||
// Text can carry provider-bound state (e.g. Gemini thought signatures),
|
||||
// which is only replayable against the model that produced it.
|
||||
providerMetadata: reuseProviderMetadata ? providerMetadata(providerMetadataKey, item.native) : undefined,
|
||||
providerMetadata: reuseProviderMetadata ? providerMetadata(providerMetadataKey, item.state) : undefined,
|
||||
},
|
||||
]
|
||||
// Let the destination adapter handle readable reasoning after a model/provider switch.
|
||||
@@ -172,7 +172,7 @@ const assistant = (message: SessionMessage.Assistant, model: Model.Ref, provider
|
||||
{
|
||||
type: "reasoning",
|
||||
text: item.text,
|
||||
providerMetadata: providerMetadata(providerMetadataKey, item.native),
|
||||
providerMetadata: providerMetadata(providerMetadataKey, item.state),
|
||||
},
|
||||
]
|
||||
: item.text.length > 0
|
||||
|
||||
@@ -269,13 +269,13 @@ function sanitizeMessage(message: SessionMessage.Info): SessionMessage.Info {
|
||||
return {
|
||||
...content,
|
||||
text: redact("text", message.id, content.text),
|
||||
native: content.native ? { redacted: `text-native:${message.id}` } : undefined,
|
||||
state: content.state ? { redacted: `text-state:${message.id}` } : undefined,
|
||||
}
|
||||
if (content.type === "reasoning")
|
||||
return {
|
||||
...content,
|
||||
text: redact("reasoning", message.id, content.text),
|
||||
native: content.native ? { redacted: `reasoning-native:${message.id}` } : undefined,
|
||||
state: content.state ? { redacted: `reasoning-state:${message.id}` } : undefined,
|
||||
}
|
||||
return {
|
||||
...content,
|
||||
@@ -299,7 +299,7 @@ function sanitizeMessage(message: SessionMessage.Info): SessionMessage.Info {
|
||||
summary: redact("compaction-summary", message.id, message.summary),
|
||||
recent: redact("compaction-recent", message.id, message.recent),
|
||||
...(message.status === "completed"
|
||||
? { native: metadata("compaction-native", message.id, message.native) }
|
||||
? { providerState: metadata("compaction-provider-state", message.id, message.providerState) }
|
||||
: {}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,6 +422,15 @@ describe("MCP OAuth", () => {
|
||||
expect(tokenRequests[0]?.get("grant_type")).toBe("refresh_token")
|
||||
})
|
||||
|
||||
test("requests offline_access without forcing a consent prompt", async () => {
|
||||
const { server } = authorizationServer({ scopes_supported: ["read", "offline_access"] })
|
||||
const { url } = await Effect.runPromise(
|
||||
Effect.scoped(start(server, { client_id: "client", scope: "read" })),
|
||||
).finally(() => server.stop(true))
|
||||
expect(url.searchParams.get("scope")).toBe("read offline_access")
|
||||
expect(url.searchParams.has("prompt")).toBe(false)
|
||||
})
|
||||
|
||||
test("forwards iss from the redirect so issuer-advertising servers can complete", async () => {
|
||||
const { server } = authorizationServer({ authorization_response_iss_parameter_supported: true })
|
||||
const result = await Effect.runPromise(
|
||||
|
||||
@@ -1272,7 +1272,7 @@ describe("SessionTransfer", () => {
|
||||
const runningCompactionID = SessionMessage.ID.create()
|
||||
const completedCompactionID = SessionMessage.ID.create()
|
||||
const model = Model.Ref.make({ id: Model.ID.make("model"), providerID: Provider.ID.make("provider") })
|
||||
const native = { responseId: "summary-response" }
|
||||
const providerState = { responseId: "summary-response" }
|
||||
|
||||
yield* transfer.import({
|
||||
data: {
|
||||
@@ -1328,7 +1328,7 @@ describe("SessionTransfer", () => {
|
||||
status: "completed",
|
||||
reason: "manual",
|
||||
model,
|
||||
native,
|
||||
providerState,
|
||||
summary: "summary",
|
||||
recent: "recent",
|
||||
time: { created: DateTime.makeUnsafe(9) },
|
||||
@@ -1345,10 +1345,10 @@ describe("SessionTransfer", () => {
|
||||
completedCompactionID,
|
||||
])
|
||||
expect(yield* Bus.latestSequence(db, sessionID)).toBe(4)
|
||||
expect((yield* transfer.export({ sessionID })).messages.at(-1)).toMatchObject({ model, native })
|
||||
expect((yield* transfer.export({ sessionID })).messages.at(-1)).toMatchObject({ model, providerState })
|
||||
expect((yield* transfer.export({ sessionID, sanitize: true })).messages.at(-1)).toMatchObject({
|
||||
model,
|
||||
native: { redacted: `compaction-native:${completedCompactionID}` },
|
||||
providerState: { redacted: `compaction-provider-state:${completedCompactionID}` },
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -426,7 +426,7 @@ it.live("compaction hooks supply the summary instead of provider compaction", ()
|
||||
status: "completed",
|
||||
summary: "## Objective\n- hooked summary",
|
||||
recent: "",
|
||||
native: { responseId: "plugin" },
|
||||
providerState: { responseId: "plugin" },
|
||||
metadata: { plugin: "custom" },
|
||||
tokens: { input: 10, output: 5 },
|
||||
})
|
||||
|
||||
@@ -690,7 +690,7 @@ describe("SessionProjector", () => {
|
||||
type: "assistant",
|
||||
finish: "stop",
|
||||
rawFinish: "stop_sequence",
|
||||
native: { response: "ended" },
|
||||
providerState: { response: "ended" },
|
||||
cost: Money.USD.make(1),
|
||||
tokens: { input: 2, output: 3, reasoning: 4, cache: { read: 5, write: 6 } },
|
||||
snapshot: { end: "snap_ended", files: ["src/ended.ts"] },
|
||||
@@ -700,7 +700,7 @@ describe("SessionProjector", () => {
|
||||
type: "assistant",
|
||||
finish: "content-filter",
|
||||
rawFinish: "blocked",
|
||||
native: { response: "failed" },
|
||||
providerState: { response: "failed" },
|
||||
error: { type: "provider.invalid-request", message: "Failed" },
|
||||
snapshot: { end: "snap_failed", files: ["src/failed.ts"] },
|
||||
time: { completed: created },
|
||||
|
||||
@@ -72,7 +72,7 @@ describe("toLLMMessages", () => {
|
||||
SessionMessage.AssistantReasoning.make({
|
||||
type: "reasoning",
|
||||
text: "",
|
||||
native: { signature: "sig_1" },
|
||||
state: { signature: "sig_1" },
|
||||
}),
|
||||
]),
|
||||
],
|
||||
@@ -711,7 +711,7 @@ Recent work
|
||||
SessionMessage.AssistantReasoning.make({
|
||||
type: "reasoning",
|
||||
text: "Think",
|
||||
native: { signature: "sig_1" },
|
||||
state: { signature: "sig_1" },
|
||||
}),
|
||||
SessionMessage.AssistantTool.make({
|
||||
type: "tool",
|
||||
@@ -860,7 +860,7 @@ Recent work
|
||||
SessionMessage.AssistantReasoning.make({
|
||||
type: "reasoning",
|
||||
text: "Think",
|
||||
native: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" },
|
||||
state: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" },
|
||||
}),
|
||||
],
|
||||
time: { created, completed: created },
|
||||
@@ -891,7 +891,7 @@ Recent work
|
||||
SessionMessage.AssistantReasoning.make({
|
||||
type: "reasoning",
|
||||
text: "Think",
|
||||
native: { signature: "signed" },
|
||||
state: { signature: "signed" },
|
||||
}),
|
||||
],
|
||||
time: { created, completed: created },
|
||||
@@ -918,7 +918,7 @@ Recent work
|
||||
SessionMessage.AssistantReasoning.make({
|
||||
type: "reasoning",
|
||||
text: "Partial thought",
|
||||
native: { itemId: "rs_failed", reasoningEncryptedContent: null },
|
||||
state: { itemId: "rs_failed", reasoningEncryptedContent: null },
|
||||
}),
|
||||
SessionMessage.AssistantTool.make({
|
||||
type: "tool",
|
||||
@@ -1016,7 +1016,7 @@ Recent work
|
||||
SessionMessage.AssistantReasoning.make({
|
||||
type: "reasoning",
|
||||
text: "Visible thought",
|
||||
native: { signature: "sig_old" },
|
||||
state: { signature: "sig_old" },
|
||||
}),
|
||||
SessionMessage.AssistantTool.make({
|
||||
type: "tool",
|
||||
@@ -1110,7 +1110,7 @@ Recent work
|
||||
SessionMessage.AssistantReasoning.make({
|
||||
type: "reasoning",
|
||||
text: "Visible thought",
|
||||
native: { reasoningEncryptedContent: "encrypted" },
|
||||
state: { reasoningEncryptedContent: "encrypted" },
|
||||
}),
|
||||
],
|
||||
time: { created, completed: created },
|
||||
@@ -1140,7 +1140,7 @@ Recent work
|
||||
SessionMessage.AssistantText.make({
|
||||
type: "text",
|
||||
text: "Checking.",
|
||||
native: { phase: "commentary" },
|
||||
state: { phase: "commentary" },
|
||||
}),
|
||||
],
|
||||
error: { type: "provider.unknown", message: "Interrupted after commentary" },
|
||||
@@ -1171,7 +1171,7 @@ Recent work
|
||||
SessionMessage.AssistantText.make({
|
||||
type: "text",
|
||||
text: "Checking.",
|
||||
native: { phase: "commentary" },
|
||||
state: { phase: "commentary" },
|
||||
}),
|
||||
],
|
||||
time: { created, completed: created },
|
||||
|
||||
@@ -2558,7 +2558,7 @@ describe("SessionRunnerLLM", () => {
|
||||
expect(s.executions).toEqual(["x".repeat(4_000)])
|
||||
expect((yield* s.messages).find((message) => message.type === "compaction")).toMatchObject({
|
||||
model: { id: s.currentModel.id, providerID: s.currentModel.provider, variant },
|
||||
native: { responseId: "summary" },
|
||||
providerState: { responseId: "summary" },
|
||||
})
|
||||
|
||||
// Compare wire content without the cache breakpoints that move to the new final message.
|
||||
@@ -3508,12 +3508,12 @@ describe("SessionRunnerLLM", () => {
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "Signed thought",
|
||||
native: { signature: "sig_1" },
|
||||
state: { signature: "sig_1" },
|
||||
},
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "Encrypted thought",
|
||||
native: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" },
|
||||
state: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" },
|
||||
},
|
||||
]),
|
||||
])
|
||||
@@ -3565,7 +3565,7 @@ describe("SessionRunnerLLM", () => {
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "thinking",
|
||||
native: { reasoningField: "reasoning", reasoningDetails: details },
|
||||
state: { reasoningField: "reasoning", reasoningDetails: details },
|
||||
},
|
||||
{ type: "text", text: "Hello world" },
|
||||
]),
|
||||
@@ -3609,7 +3609,7 @@ describe("SessionRunnerLLM", () => {
|
||||
expect(yield* s.context).toMatchObject([
|
||||
Expected.user("Check first"),
|
||||
Expected.assistant({}, [
|
||||
{ type: "text", text: "Checking.", native: { itemId: "msg_commentary", phase: "commentary" } },
|
||||
{ type: "text", text: "Checking.", state: { itemId: "msg_commentary", phase: "commentary" } },
|
||||
]),
|
||||
])
|
||||
|
||||
@@ -4966,7 +4966,7 @@ describe("SessionRunnerLLM", () => {
|
||||
type: "assistant",
|
||||
finish: "stop",
|
||||
rawFinish: "end_turn",
|
||||
native: { responseId: "response-1", serviceTier: "priority" },
|
||||
providerState: { responseId: "response-1", serviceTier: "priority" },
|
||||
content: [Expected.text("Complete")],
|
||||
},
|
||||
])
|
||||
@@ -4997,7 +4997,7 @@ describe("SessionRunnerLLM", () => {
|
||||
type: "assistant",
|
||||
finish: "content-filter",
|
||||
rawFinish: "SAFETY",
|
||||
native: {
|
||||
providerState: {
|
||||
responseId: "response-blocked",
|
||||
refusal: { category: "safety", explanation: "Prompt blocked" },
|
||||
},
|
||||
@@ -5444,7 +5444,7 @@ describe("SessionRunnerLLM", () => {
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "",
|
||||
native: { itemId: "rs_disconnected", reasoningEncryptedContent: "encrypted-state" },
|
||||
state: { itemId: "rs_disconnected", reasoningEncryptedContent: "encrypted-state" },
|
||||
},
|
||||
]),
|
||||
{ type: "synthetic", text: INCOMPLETE_STREAM_CONTINUATION },
|
||||
|
||||
@@ -337,8 +337,8 @@ describe("V1Migration.transformSession", () => {
|
||||
agent: "build",
|
||||
model: { id: "model", providerID: "provider", variant: "fast" },
|
||||
content: [
|
||||
{ type: "text", text: "", native: { separator: true } },
|
||||
{ type: "reasoning", text: "think", native: { provider: 1 }, time: { created: 21, completed: 22 } },
|
||||
{ type: "text", text: "", state: { separator: true } },
|
||||
{ type: "reasoning", text: "think", state: { provider: 1 }, time: { created: 21, completed: 22 } },
|
||||
],
|
||||
snapshot: { start: "snap_start", end: "snap_end", files: ["a.ts", "b.ts", "c.ts"] },
|
||||
finish: "stop",
|
||||
|
||||
@@ -17320,7 +17320,7 @@
|
||||
"rawFinish": {
|
||||
"type": "string"
|
||||
},
|
||||
"native": {
|
||||
"providerState": {
|
||||
"$ref": "#/components/schemas/Session.Message.ProviderState_4"
|
||||
},
|
||||
"cost": {
|
||||
@@ -17349,7 +17349,7 @@
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"native": {
|
||||
"state": {
|
||||
"$ref": "#/components/schemas/Session.Message.ProviderState_1"
|
||||
},
|
||||
"time": {
|
||||
@@ -17396,7 +17396,7 @@
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"native": {
|
||||
"state": {
|
||||
"$ref": "#/components/schemas/Session.Message.ProviderState"
|
||||
}
|
||||
},
|
||||
@@ -17509,7 +17509,7 @@
|
||||
"model": {
|
||||
"$ref": "#/components/schemas/Model.Ref"
|
||||
},
|
||||
"native": {
|
||||
"providerState": {
|
||||
"$ref": "#/components/schemas/Session.Message.ProviderState_5"
|
||||
},
|
||||
"summary": {
|
||||
|
||||
@@ -601,7 +601,7 @@ export namespace Compaction {
|
||||
...Base,
|
||||
reason: Started.data.fields.reason,
|
||||
model: SessionMessage.CompactionCompleted.fields.model,
|
||||
providerState: SessionMessage.CompactionCompleted.fields.native,
|
||||
providerState: SessionMessage.CompactionCompleted.fields.providerState,
|
||||
providerContext: SessionMessage.CompactionCompleted.fields.providerContext,
|
||||
text: Schema.String,
|
||||
recent: Schema.String,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as SessionMessage from "./session-message.js"
|
||||
|
||||
import { Predicate, Schema, Struct } from "effect"
|
||||
import { Schema } from "effect"
|
||||
import { SessionProviderContext } from "./session-provider-context.js"
|
||||
import { optional } from "./schema.js"
|
||||
import { Content } from "./tool.js"
|
||||
@@ -177,14 +177,14 @@ export interface AssistantText extends Schema.Schema.Type<typeof AssistantText>
|
||||
export const AssistantText = Schema.Struct({
|
||||
type: Schema.tag("text"),
|
||||
text: Schema.String,
|
||||
native: ProviderState.pipe(optional),
|
||||
state: ProviderState.pipe(optional),
|
||||
}).annotate({ identifier: "Session.Message.Assistant.Text" })
|
||||
|
||||
export interface AssistantReasoning extends Schema.Schema.Type<typeof AssistantReasoning> {}
|
||||
export const AssistantReasoning = Schema.Struct({
|
||||
type: Schema.tag("reasoning"),
|
||||
text: Schema.String,
|
||||
native: ProviderState.pipe(optional),
|
||||
state: ProviderState.pipe(optional),
|
||||
time: Schema.Struct({
|
||||
created: DateTimeUtcFromMillis,
|
||||
completed: DateTimeUtcFromMillis.pipe(optional),
|
||||
@@ -196,18 +196,7 @@ export const AssistantContent = Schema.Union([AssistantText, AssistantReasoning,
|
||||
)
|
||||
export type AssistantContent = AssistantText | AssistantReasoning | AssistantTool
|
||||
|
||||
/**
|
||||
* Frozen at the shape older releases stored: text and reasoning carried their
|
||||
* provider blob as `state`. Only replayed durable events still use it; read it
|
||||
* through `persistedContent` before decoding as `AssistantContent`.
|
||||
*/
|
||||
export const AssistantContentEncoded = Schema.toEncoded(
|
||||
Schema.Union([
|
||||
Schema.Struct({ ...Struct.omit(AssistantText.fields, ["native"]), state: ProviderState.pipe(optional) }),
|
||||
Schema.Struct({ ...Struct.omit(AssistantReasoning.fields, ["native"]), state: ProviderState.pipe(optional) }),
|
||||
AssistantTool,
|
||||
]).pipe(Schema.toTaggedUnion("type")),
|
||||
).annotate({
|
||||
export const AssistantContentEncoded = Schema.toEncoded(AssistantContent).annotate({
|
||||
identifier: "Session.Message.AssistantContent.Encoded",
|
||||
})
|
||||
export type AssistantContentEncoded = typeof AssistantContentEncoded.Type
|
||||
@@ -233,7 +222,7 @@ export const Assistant = Schema.Struct({
|
||||
}).pipe(optional),
|
||||
finish: FinishReason.pipe(optional),
|
||||
rawFinish: Schema.String.pipe(optional),
|
||||
native: ProviderState.pipe(optional),
|
||||
providerState: ProviderState.pipe(optional),
|
||||
cost: Money.USD.pipe(optional),
|
||||
tokens: TokenUsage.Info.pipe(optional),
|
||||
error: SessionError.Error.pipe(optional),
|
||||
@@ -269,7 +258,7 @@ export const CompactionCompleted = Schema.Struct({
|
||||
status: Schema.tag("completed"),
|
||||
reason: Schema.Literals(["auto", "manual"]),
|
||||
model: Model.Ref.pipe(optional),
|
||||
native: ProviderState.pipe(optional),
|
||||
providerState: ProviderState.pipe(optional),
|
||||
summary: Schema.String,
|
||||
recent: Schema.String,
|
||||
providerContext: SessionProviderContext.Info.pipe(optional),
|
||||
@@ -329,29 +318,3 @@ export type Info =
|
||||
| Compaction
|
||||
| Idle
|
||||
export type Type = Info["type"]
|
||||
|
||||
/** Reads messages stored before provider blobs were renamed to `native`. Tool parts are unchanged. */
|
||||
export function persisted(input: unknown) {
|
||||
if (!Predicate.isObject(input)) return input
|
||||
const message =
|
||||
input.type === "assistant" || input.type === "compaction" ? rename(input, "providerState", "native") : input
|
||||
if (message.type !== "assistant" || !Array.isArray(message.content)) return message
|
||||
const content = persistedContent(message.content)
|
||||
return content === message.content ? message : { ...message, content }
|
||||
}
|
||||
|
||||
/** Reads assistant content stored before text and reasoning blobs were renamed to `native`. */
|
||||
export function persistedContent(content: ReadonlyArray<unknown>) {
|
||||
const next = content.map((part) => {
|
||||
if (!Predicate.isObject(part) || (part.type !== "text" && part.type !== "reasoning")) return part
|
||||
return rename(part, "state", "native")
|
||||
})
|
||||
return next.every((part, index) => part === content[index]) ? content : next
|
||||
}
|
||||
|
||||
function rename(record: Record<string, unknown>, from: string, to: string) {
|
||||
if (record[from] === undefined || record[to] !== undefined) return record
|
||||
const value = record[from]
|
||||
const rest = Object.fromEntries(Object.entries(record).filter(([key]) => key !== from))
|
||||
return { ...rest, [to]: value }
|
||||
}
|
||||
|
||||
@@ -257,8 +257,8 @@ describe("contract hygiene", () => {
|
||||
text: "hello",
|
||||
})
|
||||
expect(
|
||||
SessionMessage.AssistantReasoning.make({ type: "reasoning", text: "thinking", native: { id: "opaque" } }),
|
||||
).toEqual({ type: "reasoning", text: "thinking", native: { id: "opaque" } })
|
||||
SessionMessage.AssistantReasoning.make({ type: "reasoning", text: "thinking", state: { id: "opaque" } }),
|
||||
).toEqual({ type: "reasoning", text: "thinking", state: { id: "opaque" } })
|
||||
expect(
|
||||
SessionMessage.AssistantTool.make({
|
||||
type: "tool",
|
||||
|
||||
@@ -23,48 +23,14 @@ test("assistant terminal diagnostics remain optional and round trip", () => {
|
||||
...assistant,
|
||||
finish: "content-filter",
|
||||
rawFinish: "SAFETY",
|
||||
native: { promptFeedback: { blockReason: "SAFETY" } },
|
||||
providerState: { promptFeedback: { blockReason: "SAFETY" } },
|
||||
}),
|
||||
),
|
||||
).toMatchObject({
|
||||
finish: "content-filter",
|
||||
rawFinish: "SAFETY",
|
||||
native: { promptFeedback: { blockReason: "SAFETY" } },
|
||||
})
|
||||
const legacy = SessionMessage.persisted({
|
||||
...assistant,
|
||||
providerState: { promptFeedback: { blockReason: "SAFETY" } },
|
||||
content: [{ type: "text", text: "hello", state: { signature: "sig" } }],
|
||||
})
|
||||
expect(decode(legacy)).toMatchObject({
|
||||
native: { promptFeedback: { blockReason: "SAFETY" } },
|
||||
content: [{ type: "text", native: { signature: "sig" } }],
|
||||
})
|
||||
expect(encode(decode(legacy))).not.toHaveProperty("providerState")
|
||||
expect(SessionMessage.persisted(assistant)).toBe(assistant)
|
||||
})
|
||||
|
||||
test("replayed content updates keep the stored provider blob shape", () => {
|
||||
const content = [
|
||||
{ type: "text", text: "hello", state: { signature: "sig" } },
|
||||
{ type: "reasoning", text: "think", state: { id: "rs_1" }, time: { created: 1 } },
|
||||
{ type: "tool", id: "call", name: "read", state: { status: "streaming", input: "" }, time: { created: 1 } },
|
||||
] as const
|
||||
const decoded = Schema.decodeUnknownSync(SessionEvent.MessageContentUpdated.data)({
|
||||
sessionID: "ses_terminal",
|
||||
messageID: "msg_terminal",
|
||||
content,
|
||||
})
|
||||
expect(decoded.content).toEqual(content)
|
||||
expect(
|
||||
Schema.decodeUnknownSync(Schema.Array(SessionMessage.AssistantContent))(
|
||||
SessionMessage.persistedContent(decoded.content),
|
||||
),
|
||||
).toMatchObject([
|
||||
{ type: "text", native: { signature: "sig" } },
|
||||
{ type: "reasoning", native: { id: "rs_1" } },
|
||||
{ type: "tool", state: { status: "streaming" } },
|
||||
])
|
||||
})
|
||||
|
||||
test("failed steps only override the assistant finish for content filters", () => {
|
||||
|
||||
@@ -190,13 +190,13 @@ export const streamingDocument = document(
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "## Checking the current contract\n\nThe assistant content is nested on each current Session message.",
|
||||
native: { phase: "streaming" },
|
||||
state: { phase: "streaming" },
|
||||
time: { created: STORY_TIME + 11_100 },
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: "I have the typed rows in place. Next I am checking the streaming presentation",
|
||||
native: { phase: "streaming" },
|
||||
state: { phase: "streaming" },
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
@@ -47,7 +47,7 @@ function MermaidTimeline(props: { streaming: boolean }) {
|
||||
"```mermaid\nsequenceDiagram\n Client->>Server: Send prompt\n Server->>Model: Generate response\n Model-->>Client: Response\n" +
|
||||
(completed() ? "```" : ""),
|
||||
].join("\n\n"),
|
||||
...(completed() ? {} : { native: { phase: "streaming" } }),
|
||||
...(completed() ? {} : { state: { phase: "streaming" } }),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -727,7 +727,7 @@ function App(props: { pair?: DialogPairCredentials }) {
|
||||
title: "New session",
|
||||
suggested: route.data.type === "session",
|
||||
category: "Session",
|
||||
slash: { name: "new", aliases: ["clear"] },
|
||||
slash: { name: "new" },
|
||||
run: () => {
|
||||
const model = local.model.current()
|
||||
const agent = local.agent.current()
|
||||
@@ -749,6 +749,33 @@ function App(props: { pair?: DialogPairCredentials }) {
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "session.clear",
|
||||
title: "Clear session",
|
||||
category: "Session",
|
||||
slash: { name: "clear" },
|
||||
run: () => {
|
||||
const model = local.model.current()
|
||||
const agent = local.agent.current()
|
||||
const current =
|
||||
route.data.type === "session"
|
||||
? (data.session.get(route.data.sessionID)?.location ?? location.ref)
|
||||
: undefined
|
||||
sessionTabs.close()
|
||||
route.navigate({
|
||||
type: "home",
|
||||
location: newSessionLocation(
|
||||
config.data.session.new_location,
|
||||
data.location.default().directory,
|
||||
current,
|
||||
location.error?.location,
|
||||
),
|
||||
})
|
||||
if (agent) local.agent.set(agent.id)
|
||||
if (model) local.model.set(model)
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "open.menu",
|
||||
title: "Open session or project",
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { createAppFixture } from "./fixture/app"
|
||||
import { directory, json } from "./fixture/tui-client"
|
||||
import { tmpdir } from "./fixture/fixture"
|
||||
|
||||
const location = { directory, project: { id: "project", directory, canonical: directory } }
|
||||
const session = {
|
||||
id: "ses_clear",
|
||||
title: "Session to clear",
|
||||
projectID: "project",
|
||||
location: { directory },
|
||||
agent: "build",
|
||||
model: { providerID: "provider", id: "model" },
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
time: { created: 0, updated: 0 },
|
||||
}
|
||||
|
||||
function render(state: string) {
|
||||
return createAppFixture({
|
||||
state,
|
||||
args: { sessionID: session.id },
|
||||
config: { animations: false, tabs: { mode: "on" } },
|
||||
fetch: (url) => {
|
||||
if (url.pathname === "/api/fs/list") return json({ location, data: [] })
|
||||
if (url.pathname === "/api/location") return json(location)
|
||||
if (url.pathname === "/api/session") return json({ data: [session], cursor: {} })
|
||||
if (url.pathname === `/api/session/${session.id}`) return json({ data: session })
|
||||
if (/^\/api\/session\/[^/]+\/(message|inbox|permission)$/.test(url.pathname))
|
||||
return json({ data: [], cursor: {} })
|
||||
if (url.pathname === "/api/agent")
|
||||
return json({ location, data: [{ id: "build", mode: "primary", hidden: false, permissions: [] }] })
|
||||
if (url.pathname === "/api/provider") return json({ location, data: [{ id: "provider", name: "Provider" }] })
|
||||
if (url.pathname === "/api/model")
|
||||
return json({
|
||||
location,
|
||||
data: [{ id: "model", providerID: "provider", name: "Model", variants: [] }],
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
test("/clear replaces the active session tab", async () => {
|
||||
await using state = await tmpdir()
|
||||
await using setup = await render(state.path)
|
||||
|
||||
await setup.waitForFrame((frame) => frame.includes("Session to clear"))
|
||||
await setup.mockInput.typeText("/clear")
|
||||
setup.mockInput.pressEnter()
|
||||
const frame = await setup.waitForFrame((frame) => !frame.includes("Session to clear"))
|
||||
|
||||
expect(frame).not.toContain("Session to clear")
|
||||
})
|
||||
|
||||
test("/new keeps the active session tab", async () => {
|
||||
await using state = await tmpdir()
|
||||
await using setup = await render(state.path)
|
||||
|
||||
await setup.waitForFrame((frame) => frame.includes("Session to clear"))
|
||||
await setup.mockInput.typeText("/new")
|
||||
setup.mockInput.pressEnter()
|
||||
const frame = await setup.waitForFrame(
|
||||
(frame) => frame.includes("New session") && frame.includes("Session to clear"),
|
||||
)
|
||||
|
||||
expect(frame).toContain("Session to clear")
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/dist/index.cjs b/dist/index.cjs
|
||||
index 635f1c0134274c89e7efbcae2adf3d9ecba575ee..0054b779ff7b550ecf6d4008e177a888003f9c92 100644
|
||||
index 635f1c0134274c89e7efbcae2adf3d9ecba575ee..5eaa7b451c8c7e12e51f61c2a835ff6f7a8ea0ae 100644
|
||||
--- a/dist/index.cjs
|
||||
+++ b/dist/index.cjs
|
||||
@@ -977,7 +977,6 @@ async function discoverMetadataWithFallback(serverUrl, wellKnownType, fetchFn, o
|
||||
@@ -10,8 +10,16 @@ index 635f1c0134274c89e7efbcae2adf3d9ecba575ee..0054b779ff7b550ecf6d4008e177a888
|
||||
}
|
||||
let response = await tryMetadataDiscovery(url, protocolVersion, fetchFn);
|
||||
if (!opts?.metadataUrl && shouldAttemptFallback(response, issuer.pathname)) response = await tryMetadataDiscovery(new URL(`/.well-known/${wellKnownType}`, issuer), protocolVersion, fetchFn);
|
||||
@@ -1158,7 +1157,6 @@ async function startAuthorization(authorizationServerUrl, { metadata, clientInfo
|
||||
authorizationUrl.searchParams.set("redirect_uri", String(redirectUrl));
|
||||
if (state) authorizationUrl.searchParams.set("state", state);
|
||||
if (scope) authorizationUrl.searchParams.set("scope", scope);
|
||||
- if (scope?.split(" ").includes("offline_access")) authorizationUrl.searchParams.append("prompt", "consent");
|
||||
if (resource) authorizationUrl.searchParams.set("resource", resource.href);
|
||||
return {
|
||||
authorizationUrl,
|
||||
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||
index f02ce3ca394e826fc27c9848dbe9a214bf6ba7a9..8e27c9320e14ba8816fda752f250f6ddb1cc7ed0 100644
|
||||
index f02ce3ca394e826fc27c9848dbe9a214bf6ba7a9..73a93a066c3540e131fa0770a74b4a7e8d9343a0 100644
|
||||
--- a/dist/index.mjs
|
||||
+++ b/dist/index.mjs
|
||||
@@ -974,7 +974,6 @@ async function discoverMetadataWithFallback(serverUrl, wellKnownType, fetchFn, o
|
||||
@@ -22,3 +30,11 @@ index f02ce3ca394e826fc27c9848dbe9a214bf6ba7a9..8e27c9320e14ba8816fda752f250f6dd
|
||||
}
|
||||
let response = await tryMetadataDiscovery(url, protocolVersion, fetchFn);
|
||||
if (!opts?.metadataUrl && shouldAttemptFallback(response, issuer.pathname)) response = await tryMetadataDiscovery(new URL(`/.well-known/${wellKnownType}`, issuer), protocolVersion, fetchFn);
|
||||
@@ -1155,7 +1154,6 @@ async function startAuthorization(authorizationServerUrl, { metadata, clientInfo
|
||||
authorizationUrl.searchParams.set("redirect_uri", String(redirectUrl));
|
||||
if (state) authorizationUrl.searchParams.set("state", state);
|
||||
if (scope) authorizationUrl.searchParams.set("scope", scope);
|
||||
- if (scope?.split(" ").includes("offline_access")) authorizationUrl.searchParams.append("prompt", "consent");
|
||||
if (resource) authorizationUrl.searchParams.set("resource", resource.href);
|
||||
return {
|
||||
authorizationUrl,
|
||||
|
||||
@@ -17320,7 +17320,7 @@
|
||||
"rawFinish": {
|
||||
"type": "string"
|
||||
},
|
||||
"native": {
|
||||
"providerState": {
|
||||
"$ref": "#/components/schemas/Session.Message.ProviderState_4"
|
||||
},
|
||||
"cost": {
|
||||
@@ -17349,7 +17349,7 @@
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"native": {
|
||||
"state": {
|
||||
"$ref": "#/components/schemas/Session.Message.ProviderState_1"
|
||||
},
|
||||
"time": {
|
||||
@@ -17396,7 +17396,7 @@
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"native": {
|
||||
"state": {
|
||||
"$ref": "#/components/schemas/Session.Message.ProviderState"
|
||||
}
|
||||
},
|
||||
@@ -17509,7 +17509,7 @@
|
||||
"model": {
|
||||
"$ref": "#/components/schemas/Model.Ref"
|
||||
},
|
||||
"native": {
|
||||
"providerState": {
|
||||
"$ref": "#/components/schemas/Session.Message.ProviderState_5"
|
||||
},
|
||||
"summary": {
|
||||
|
||||
@@ -17320,7 +17320,7 @@
|
||||
"rawFinish": {
|
||||
"type": "string"
|
||||
},
|
||||
"native": {
|
||||
"providerState": {
|
||||
"$ref": "#/components/schemas/Session.Message.ProviderState_4"
|
||||
},
|
||||
"cost": {
|
||||
@@ -17349,7 +17349,7 @@
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"native": {
|
||||
"state": {
|
||||
"$ref": "#/components/schemas/Session.Message.ProviderState_1"
|
||||
},
|
||||
"time": {
|
||||
@@ -17396,7 +17396,7 @@
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"native": {
|
||||
"state": {
|
||||
"$ref": "#/components/schemas/Session.Message.ProviderState"
|
||||
}
|
||||
},
|
||||
@@ -17509,7 +17509,7 @@
|
||||
"model": {
|
||||
"$ref": "#/components/schemas/Model.Ref"
|
||||
},
|
||||
"native": {
|
||||
"providerState": {
|
||||
"$ref": "#/components/schemas/Session.Message.ProviderState_5"
|
||||
},
|
||||
"summary": {
|
||||
|
||||
Reference in New Issue
Block a user