mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-06 17:19:49 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 76f205d260 |
@@ -117,17 +117,6 @@ export type SessionMessageCompactionRunning = {
|
|||||||
recent: string
|
recent: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionMessageCompactionCompleted = {
|
|
||||||
type: "compaction"
|
|
||||||
id: string
|
|
||||||
metadata?: { [x: string]: JsonValue }
|
|
||||||
time: { created: number }
|
|
||||||
status: "completed"
|
|
||||||
reason: "auto" | "manual"
|
|
||||||
summary: string
|
|
||||||
recent: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type InstructionEntryKey = string
|
export type InstructionEntryKey = string
|
||||||
|
|
||||||
export type SessionPendingSyntheticData1 = { text: string; description?: string; metadata?: { [x: string]: any } }
|
export type SessionPendingSyntheticData1 = { text: string; description?: string; metadata?: { [x: string]: any } }
|
||||||
@@ -507,6 +496,18 @@ export type SessionMessageModelSelected = {
|
|||||||
previous?: ModelRef
|
previous?: ModelRef
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SessionMessageCompactionCompleted = {
|
||||||
|
type: "compaction"
|
||||||
|
id: string
|
||||||
|
metadata?: { [x: string]: JsonValue }
|
||||||
|
time: { created: number }
|
||||||
|
status: "completed"
|
||||||
|
reason: "auto" | "manual"
|
||||||
|
model?: ModelRef
|
||||||
|
summary: string
|
||||||
|
recent: string
|
||||||
|
}
|
||||||
|
|
||||||
export type CommandInfo = {
|
export type CommandInfo = {
|
||||||
name: string
|
name: string
|
||||||
template: string
|
template: string
|
||||||
@@ -749,7 +750,7 @@ export type SessionCompactionEnded = {
|
|||||||
type: "session.compaction.ended"
|
type: "session.compaction.ended"
|
||||||
durable: { aggregateID: string; seq: number; version: 1 }
|
durable: { aggregateID: string; seq: number; version: 1 }
|
||||||
location?: LocationRef
|
location?: LocationRef
|
||||||
data: { sessionID: string; reason: "auto" | "manual"; text: string; recent: string }
|
data: { sessionID: string; reason: "auto" | "manual"; model?: ModelRef; text: string; recent: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionRevertCleared = {
|
export type SessionRevertCleared = {
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export type AutoInput = {
|
|||||||
readonly sessionID: SessionSchema.ID
|
readonly sessionID: SessionSchema.ID
|
||||||
readonly messages: readonly SessionMessage.Info[]
|
readonly messages: readonly SessionMessage.Info[]
|
||||||
readonly model: Model
|
readonly model: Model
|
||||||
|
readonly ref: SessionMessage.Assistant["model"]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ManualInput = {
|
export type ManualInput = {
|
||||||
@@ -80,6 +81,7 @@ export type ManualInput = {
|
|||||||
type Plan = {
|
type Plan = {
|
||||||
readonly sessionID: SessionSchema.ID
|
readonly sessionID: SessionSchema.ID
|
||||||
readonly model: Model
|
readonly model: Model
|
||||||
|
readonly ref: SessionMessage.Assistant["model"]
|
||||||
readonly reason: SessionMessage.Compaction["reason"]
|
readonly reason: SessionMessage.Compaction["reason"]
|
||||||
readonly prompt: string
|
readonly prompt: string
|
||||||
readonly recent: string
|
readonly recent: string
|
||||||
@@ -291,6 +293,7 @@ const make = (dependencies: Dependencies) => {
|
|||||||
yield* dependencies.events.publish(SessionEvent.Compaction.Ended, {
|
yield* dependencies.events.publish(SessionEvent.Compaction.Ended, {
|
||||||
sessionID: plan.sessionID,
|
sessionID: plan.sessionID,
|
||||||
reason: plan.reason,
|
reason: plan.reason,
|
||||||
|
model: plan.ref,
|
||||||
text: summary,
|
text: summary,
|
||||||
recent: plan.recent,
|
recent: plan.recent,
|
||||||
})
|
})
|
||||||
@@ -302,6 +305,7 @@ const make = (dependencies: Dependencies) => {
|
|||||||
return yield* execute({
|
return yield* execute({
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
model: input.model,
|
model: input.model,
|
||||||
|
ref: input.ref,
|
||||||
reason: "auto",
|
reason: "auto",
|
||||||
...content,
|
...content,
|
||||||
})
|
})
|
||||||
@@ -350,6 +354,7 @@ const make = (dependencies: Dependencies) => {
|
|||||||
return yield* execute({
|
return yield* execute({
|
||||||
sessionID: input.session.id,
|
sessionID: input.session.id,
|
||||||
model: resolved.model,
|
model: resolved.model,
|
||||||
|
ref: resolved.ref,
|
||||||
reason: "manual",
|
reason: "manual",
|
||||||
inputID: input.inputID,
|
inputID: input.inputID,
|
||||||
...content,
|
...content,
|
||||||
|
|||||||
@@ -470,6 +470,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||||||
...current,
|
...current,
|
||||||
status: "completed",
|
status: "completed",
|
||||||
reason: event.data.reason,
|
reason: event.data.reason,
|
||||||
|
model: event.data.model,
|
||||||
summary: event.data.text,
|
summary: event.data.text,
|
||||||
recent: event.data.recent,
|
recent: event.data.recent,
|
||||||
})
|
})
|
||||||
@@ -482,6 +483,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||||||
status: "completed",
|
status: "completed",
|
||||||
metadata: event.metadata,
|
metadata: event.metadata,
|
||||||
reason: event.data.reason,
|
reason: event.data.reason,
|
||||||
|
model: event.data.model,
|
||||||
summary: event.data.text,
|
summary: event.data.text,
|
||||||
recent: event.data.recent,
|
recent: event.data.recent,
|
||||||
time: { created: event.created },
|
time: { created: event.created },
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ const layer = Layer.effect(
|
|||||||
const providerMetadataKey = model.route.providerMetadataKey ?? model.provider
|
const providerMetadataKey = model.route.providerMetadataKey ?? model.provider
|
||||||
const history = yield* SessionHistory.entriesForRunner(db, session.id, instructions)
|
const history = yield* SessionHistory.entriesForRunner(db, session.id, instructions)
|
||||||
const context = history.entries.map((entry) => entry.message)
|
const context = history.entries.map((entry) => entry.message)
|
||||||
const compactionInput = { sessionID: session.id, messages: context, model }
|
const compactionInput = { sessionID: session.id, messages: context, model, ref: resolved.ref }
|
||||||
if (compaction.required(compactionInput) && !(yield* SessionPending.compaction(db, session.id))) {
|
if (compaction.required(compactionInput) && !(yield* SessionPending.compaction(db, session.id))) {
|
||||||
const compacted = yield* compaction.compact(compactionInput)
|
const compacted = yield* compaction.compact(compactionInput)
|
||||||
if (compacted.status === "completed") return { _tag: "RestartAfterCompaction", step: currentStep } as const
|
if (compacted.status === "completed") return { _tag: "RestartAfterCompaction", step: currentStep } as const
|
||||||
@@ -331,8 +331,8 @@ const layer = Layer.effect(
|
|||||||
recoverOverflow &&
|
recoverOverflow &&
|
||||||
!publisher.hasRetryEvidence() &&
|
!publisher.hasRetryEvidence() &&
|
||||||
isContextOverflowFailure(overflowFailure ?? streamFailure) &&
|
isContextOverflowFailure(overflowFailure ?? streamFailure) &&
|
||||||
(yield* restore(recoverOverflow({ sessionID: session.id, messages: context, model }))).status ===
|
(yield* restore(recoverOverflow({ sessionID: session.id, messages: context, model, ref: resolved.ref })))
|
||||||
"completed"
|
.status === "completed"
|
||||||
)
|
)
|
||||||
return { _tag: "RestartAfterOverflowCompaction", step: currentStep } as const
|
return { _tag: "RestartAfterOverflowCompaction", step: currentStep } as const
|
||||||
|
|
||||||
|
|||||||
@@ -154,7 +154,13 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
|
|||||||
expect(requests[0]?.generation).toBeUndefined()
|
expect(requests[0]?.generation).toBeUndefined()
|
||||||
expect(JSON.stringify(requests[0]?.messages)).toContain("Manual compaction should include this short conversation.")
|
expect(JSON.stringify(requests[0]?.messages)).toContain("Manual compaction should include this short conversation.")
|
||||||
expect(yield* store.context(sessionID)).toMatchObject([
|
expect(yield* store.context(sessionID)).toMatchObject([
|
||||||
{ type: "compaction", reason: "manual", summary: "manual summary", recent: "" },
|
{
|
||||||
|
type: "compaction",
|
||||||
|
reason: "manual",
|
||||||
|
model: { id: "summary-model", providerID: "test" },
|
||||||
|
summary: "manual summary",
|
||||||
|
recent: "",
|
||||||
|
},
|
||||||
])
|
])
|
||||||
expect(
|
expect(
|
||||||
yield* db
|
yield* db
|
||||||
|
|||||||
@@ -488,6 +488,7 @@ export namespace Compaction {
|
|||||||
schema: {
|
schema: {
|
||||||
...Base,
|
...Base,
|
||||||
reason: Started.data.fields.reason,
|
reason: Started.data.fields.reason,
|
||||||
|
model: Model.Ref.pipe(optional),
|
||||||
text: Schema.String,
|
text: Schema.String,
|
||||||
recent: Schema.String,
|
recent: Schema.String,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -222,6 +222,7 @@ export const CompactionCompleted = Schema.Struct({
|
|||||||
...CompactionBase,
|
...CompactionBase,
|
||||||
status: Schema.tag("completed"),
|
status: Schema.tag("completed"),
|
||||||
reason: Schema.Literals(["auto", "manual"]),
|
reason: Schema.Literals(["auto", "manual"]),
|
||||||
|
model: Model.Ref.pipe(optional),
|
||||||
summary: Schema.String,
|
summary: Schema.String,
|
||||||
recent: Schema.String,
|
recent: Schema.String,
|
||||||
}).annotate({ identifier: "Session.Message.Compaction.Completed" })
|
}).annotate({ identifier: "Session.Message.Compaction.Completed" })
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { Schema } from "effect"
|
||||||
|
import { SessionMessage } from "../src/session-message.js"
|
||||||
|
|
||||||
|
describe("session message", () => {
|
||||||
|
test("decodes completed compactions recorded before model attribution", () => {
|
||||||
|
const encoded = {
|
||||||
|
type: "compaction" as const,
|
||||||
|
id: "msg_compaction",
|
||||||
|
time: { created: 0 },
|
||||||
|
status: "completed" as const,
|
||||||
|
reason: "manual" as const,
|
||||||
|
summary: "summary",
|
||||||
|
recent: "",
|
||||||
|
}
|
||||||
|
const message = Schema.decodeUnknownSync(SessionMessage.CompactionCompleted)(encoded)
|
||||||
|
|
||||||
|
expect(message.model).toBeUndefined()
|
||||||
|
expect(Schema.encodeSync(SessionMessage.CompactionCompleted)(message)).toEqual(encoded)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -732,6 +732,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||||||
Object.assign(current, {
|
Object.assign(current, {
|
||||||
status: "completed",
|
status: "completed",
|
||||||
reason: event.data.reason,
|
reason: event.data.reason,
|
||||||
|
model: event.data.model,
|
||||||
summary: event.data.text,
|
summary: event.data.text,
|
||||||
recent: event.data.recent,
|
recent: event.data.recent,
|
||||||
})
|
})
|
||||||
@@ -742,6 +743,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||||||
type: "compaction",
|
type: "compaction",
|
||||||
status: "completed",
|
status: "completed",
|
||||||
reason: event.data.reason,
|
reason: event.data.reason,
|
||||||
|
model: event.data.model,
|
||||||
summary: event.data.text,
|
summary: event.data.text,
|
||||||
recent: event.data.recent,
|
recent: event.data.recent,
|
||||||
time: { created: event.created },
|
time: { created: event.created },
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ import { OPENCODE_BASE_MODE, useBindings, useCommandShortcut } from "../../keyma
|
|||||||
import { usePathFormatter } from "../../context/path-format"
|
import { usePathFormatter } from "../../context/path-format"
|
||||||
import { LocationProvider } from "../../context/location"
|
import { LocationProvider } from "../../context/location"
|
||||||
import { createSessionRows, resolvePart, type PartRef, type SessionRow } from "./rows"
|
import { createSessionRows, resolvePart, type PartRef, type SessionRow } from "./rows"
|
||||||
import { switchLabel } from "../../util/model"
|
import { compactionMarker, switchLabel } from "../../util/model"
|
||||||
|
|
||||||
addDefaultParsers(parsers.parsers)
|
addDefaultParsers(parsers.parsers)
|
||||||
|
|
||||||
@@ -1306,10 +1306,14 @@ function SessionSkillMessage(props: { message: Extract<SessionMessageInfo, { typ
|
|||||||
|
|
||||||
function CompactionMessage(props: { message: Extract<SessionMessageInfo, { type: "compaction" }> }) {
|
function CompactionMessage(props: { message: Extract<SessionMessageInfo, { type: "compaction" }> }) {
|
||||||
const ctx = use()
|
const ctx = use()
|
||||||
|
const local = useLocal()
|
||||||
const { theme, syntax } = useTheme()
|
const { theme, syntax } = useTheme()
|
||||||
const status = () => props.message.status
|
const status = () => props.message.status
|
||||||
const text = () => (props.message.status === "failed" ? props.message.error.message : props.message.summary)
|
const text = () => (props.message.status === "failed" ? props.message.error.message : props.message.summary)
|
||||||
const content = createMemo(() => text().trim())
|
const content = createMemo(() => text().trim())
|
||||||
|
const marker = createMemo(() =>
|
||||||
|
compactionMarker(props.message.status === "completed" ? props.message.model : undefined, ctx.models()),
|
||||||
|
)
|
||||||
const color = () => (status() === "failed" ? theme.error : theme.textMuted)
|
const color = () => (status() === "failed" ? theme.error : theme.textMuted)
|
||||||
return (
|
return (
|
||||||
<box>
|
<box>
|
||||||
@@ -1344,6 +1348,16 @@ function CompactionMessage(props: { message: Extract<SessionMessageInfo, { type:
|
|||||||
/>
|
/>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
|
<Show when={marker()}>
|
||||||
|
{(value) => (
|
||||||
|
<box paddingLeft={3} marginTop={1}>
|
||||||
|
<text>
|
||||||
|
<span style={{ fg: local.agent.color(value().agent) }}>{Locale.titlecase(value().agent)}</span>
|
||||||
|
<span style={{ fg: theme.textMuted }}> · {value().model}</span>
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,18 @@ export function formatRef(model: { providerID: string; id: string; variant?: str
|
|||||||
return [model.providerID, model.id, model.variant].filter((value) => value !== undefined).join("/")
|
return [model.providerID, model.id, model.variant].filter((value) => value !== undefined).join("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function compactionMarker(
|
||||||
|
model: { providerID: string; id: string; variant?: string } | undefined,
|
||||||
|
models?: readonly { providerID: string; id: string; name: string }[],
|
||||||
|
) {
|
||||||
|
if (!model) return
|
||||||
|
return {
|
||||||
|
agent: "compaction",
|
||||||
|
model:
|
||||||
|
models?.find((item) => item.providerID === model.providerID && item.id === model.id)?.name ?? formatRef(model),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function switchLabel(
|
export function switchLabel(
|
||||||
model: { providerID: string; id: string; variant?: string },
|
model: { providerID: string; id: string; variant?: string },
|
||||||
models?: readonly { providerID: string; id: string; name: string }[],
|
models?: readonly { providerID: string; id: string; name: string }[],
|
||||||
|
|||||||
@@ -1126,7 +1126,13 @@ test("tracks session status from active sessions and execution events", async ()
|
|||||||
created: 0,
|
created: 0,
|
||||||
type: "session.compaction.ended",
|
type: "session.compaction.ended",
|
||||||
durable: durable("session-live", 5),
|
durable: durable("session-live", 5),
|
||||||
data: { sessionID: "session-live", reason: "auto", text: "Live summary", recent: "recent" },
|
data: {
|
||||||
|
sessionID: "session-live",
|
||||||
|
reason: "auto",
|
||||||
|
model: { providerID: "anthropic", id: "claude-sonnet" },
|
||||||
|
text: "Live summary",
|
||||||
|
recent: "recent",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
await wait(() => {
|
await wait(() => {
|
||||||
const message = data.session.message.get("session-live", "msg_compaction_started")
|
const message = data.session.message.get("session-live", "msg_compaction_started")
|
||||||
@@ -1135,6 +1141,7 @@ test("tracks session status from active sessions and execution events", async ()
|
|||||||
expect(data.session.message.get("session-live", "msg_compaction_started")).toMatchObject({
|
expect(data.session.message.get("session-live", "msg_compaction_started")).toMatchObject({
|
||||||
type: "compaction",
|
type: "compaction",
|
||||||
status: "completed",
|
status: "completed",
|
||||||
|
model: { providerID: "anthropic", id: "claude-sonnet" },
|
||||||
summary: "Live summary",
|
summary: "Live summary",
|
||||||
})
|
})
|
||||||
expect(rows.find((row) => row.type === "message" && row.messageID === "msg_compaction_started")).toBe(
|
expect(rows.find((row) => row.type === "message" && row.messageID === "msg_compaction_started")).toBe(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { formatRef, parse, switchLabel } from "../../src/util/model"
|
import { compactionMarker, formatRef, parse, switchLabel } from "../../src/util/model"
|
||||||
|
|
||||||
describe("util.model", () => {
|
describe("util.model", () => {
|
||||||
test("splits provider from a nested model identifier", () => {
|
test("splits provider from a nested model identifier", () => {
|
||||||
@@ -12,6 +12,20 @@ describe("util.model", () => {
|
|||||||
expect(formatRef({ providerID: "anthropic", id: "sonnet" })).toBe("anthropic/sonnet")
|
expect(formatRef({ providerID: "anthropic", id: "sonnet" })).toBe("anthropic/sonnet")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("labels completed compactions with their actual model", () => {
|
||||||
|
const models = [{ providerID: "anthropic", id: "sonnet", name: "Claude Sonnet" }]
|
||||||
|
|
||||||
|
expect(compactionMarker({ providerID: "anthropic", id: "sonnet" }, models)).toEqual({
|
||||||
|
agent: "compaction",
|
||||||
|
model: "Claude Sonnet",
|
||||||
|
})
|
||||||
|
expect(compactionMarker({ providerID: "removed", id: "gone", variant: "high" }, models)).toEqual({
|
||||||
|
agent: "compaction",
|
||||||
|
model: "removed/gone/high",
|
||||||
|
})
|
||||||
|
expect(compactionMarker(undefined, models)).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
test("includes the selected variant in model switch notices", () => {
|
test("includes the selected variant in model switch notices", () => {
|
||||||
expect(switchLabel({ providerID: "anthropic", id: "sonnet", variant: "thinking" })).toBe(
|
expect(switchLabel({ providerID: "anthropic", id: "sonnet", variant: "thinking" })).toBe(
|
||||||
"Switched model to anthropic/sonnet/thinking",
|
"Switched model to anthropic/sonnet/thinking",
|
||||||
|
|||||||
Reference in New Issue
Block a user