Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/client/src/effect/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -965,6 +965,8 @@ export type SessionLogOutput =
readonly data: {
readonly sessionID: Session.ID
readonly reason: "auto" | "manual"
readonly model?: Model.Ref | undefined
readonly providerState?: SessionMessage.ProviderState | undefined
readonly text: string
readonly recent: string
}
Expand Down
57 changes: 36 additions & 21 deletions packages/client/src/promise/generated/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,17 +138,6 @@ export type SessionMessageCompactionRunning = {
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 SessionActive = { type: "running" }

export type SessionInboxDelivery = "steer" | "queue"
Expand Down Expand Up @@ -521,6 +510,19 @@ export type SessionMessageAssistantReasoning = {
time?: { created: number; completed?: number }
}

export type SessionMessageCompactionCompleted = {
type: "compaction"
id: string
metadata?: { [x: string]: JsonValue }
time: { created: number }
status: "completed"
reason: "auto" | "manual"
model?: ModelRef
providerState?: SessionMessageProviderState
summary: string
recent: string
}

export type ToolContent = ToolTextContent | ToolFileContent

export type SessionMessageAssistantRetry = { attempt: number; at: number; error: SessionStructuredError }
Expand Down Expand Up @@ -809,16 +811,6 @@ export type SessionCompactionStarted = {
data: { sessionID: string; reason: "auto" | "manual"; recent: string; inputID?: string }
}

export type SessionCompactionEnded = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.compaction.ended"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; reason: "auto" | "manual"; text: string; recent: string }
}

export type SessionCompactionFailed = {
id: string
created: number
Expand Down Expand Up @@ -1351,6 +1343,23 @@ export type SessionToolCalled = {
}
}

export type SessionCompactionEnded = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.compaction.ended"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: {
sessionID: string
reason: "auto" | "manual"
model?: ModelRef
providerState?: SessionMessageProviderState1
text: string
recent: string
}
}

export type SessionMessageAssistantText1 = { type: "text"; text: string; state?: SessionMessageProviderState1 }

export type SessionMessageAssistantReasoning1 = {
Expand Down Expand Up @@ -3063,6 +3072,8 @@ export type SessionImportInput = {
readonly time: { readonly created: number }
readonly status: "completed"
readonly reason: "auto" | "manual"
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string }
readonly providerState?: { readonly [x: string]: JsonValue }
readonly summary: string
readonly recent: string
}
Expand Down Expand Up @@ -3340,6 +3351,8 @@ export type SessionImportInput = {
readonly time: { readonly created: number }
readonly status: "completed"
readonly reason: "auto" | "manual"
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string }
readonly providerState?: { readonly [x: string]: JsonValue }
readonly summary: string
readonly recent: string
}
Expand Down Expand Up @@ -3617,6 +3630,8 @@ export type SessionImportInput = {
readonly time: { readonly created: number }
readonly status: "completed"
readonly reason: "auto" | "manual"
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string }
readonly providerState?: { readonly [x: string]: JsonValue }
readonly summary: string
readonly recent: string
}
Expand Down
4 changes: 4 additions & 0 deletions packages/client/src/solid/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1038,6 +1038,8 @@ export function createData(config: CreateDataInput) {
Object.assign(current, {
status: "completed",
reason: event.data.reason,
model: event.data.model,
providerState: event.data.providerState,
summary: event.data.text,
recent: event.data.recent,
})
Expand All @@ -1048,6 +1050,8 @@ export function createData(config: CreateDataInput) {
type: "compaction",
status: "completed",
reason: event.data.reason,
model: event.data.model,
providerState: event.data.providerState,
summary: event.data.text,
recent: event.data.recent,
time: { created: event.created },
Expand Down
6 changes: 4 additions & 2 deletions packages/client/test/solid-compaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,15 @@ test.each(["started", "cancelled", "failed"])(
expect(fixture.data.session.pending.list(sessionID)).toEqual([])
if (kind === "started") {
expect(fixture.data.session.message.list(sessionID)).toMatchObject([{ type: "compaction", status: "running" }])
const model = { providerID: "demo", id: "model" }
const providerState = { responseId: "summary-response" }
fixture.emit({
...event,
type: "session.compaction.ended",
data: { sessionID, reason: "manual", text: "Summary", recent: "Recent" },
data: { sessionID, reason: "manual", model, providerState, text: "Summary", recent: "Recent" },
})
expect(fixture.data.session.message.list(sessionID)).toMatchObject([
{ type: "compaction", status: "completed", summary: "Summary" },
{ type: "compaction", status: "completed", summary: "Summary", model, providerState },
])
}
},
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/session/compaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ export const layer = Layer.effect(
const chunks: string[] = []
let failure: SessionError.Error | undefined
let usage: SessionUsage.Recorded | undefined
let providerState: SessionMessage.ProviderState | undefined
const recordUsage = Effect.suspend(() =>
usage
? bus.publish(SessionEvent.UsageRecorded, {
Expand Down Expand Up @@ -407,6 +408,7 @@ export const layer = Layer.effect(
// Ignored tool calls never enter the follow-up history or need fabricated results.
for (let attempt = 0; attempt < 2; attempt++) {
chunks.length = 0
providerState = undefined
yield* llm
.stream(
attempt === 0
Expand Down Expand Up @@ -436,6 +438,10 @@ export const layer = Layer.effect(
})
}
if (LLMEvent.is.stepFinish(event)) {
providerState =
event.providerMetadata?.[
context.model.model.route.providerMetadataKey ?? context.model.model.provider
]
const step = SessionUsage.record(event.usage, context.model.cost)
usage = usage ? SessionUsage.add(usage, step) : step
}
Expand Down Expand Up @@ -482,6 +488,8 @@ export const layer = Layer.effect(
yield* bus.publish(SessionEvent.Compaction.Ended, {
sessionID: context.session.id,
reason: input.reason,
model: context.model.ref,
providerState,
text: summary,
recent: history.recent,
})
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/session/message-updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,8 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
...current,
status: "completed",
reason: event.data.reason,
model: event.data.model,
providerState: event.data.providerState,
summary: event.data.text,
recent: event.data.recent,
})
Expand All @@ -422,6 +424,8 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
status: "completed",
metadata: event.metadata,
reason: event.data.reason,
model: event.data.model,
providerState: event.data.providerState,
summary: event.data.text,
recent: event.data.recent,
time: { created },
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/session/transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,9 @@ function sanitizeMessage(message: SessionMessage.Info): SessionMessage.Info {
metadata: meta,
summary: redact("compaction-summary", message.id, message.summary),
recent: redact("compaction-recent", message.id, message.recent),
...(message.status === "completed"
? { providerState: metadata("compaction-provider-state", message.id, message.providerState) }
: {}),
}
}
return { ...message, metadata: meta }
Expand Down
8 changes: 8 additions & 0 deletions packages/core/test/session-create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1243,6 +1243,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 providerState = { responseId: "summary-response" }

yield* transfer.import({
data: {
Expand Down Expand Up @@ -1297,6 +1298,8 @@ describe("SessionTransfer", () => {
type: "compaction",
status: "completed",
reason: "manual",
model,
providerState,
summary: "summary",
recent: "recent",
time: { created: DateTime.makeUnsafe(9) },
Expand All @@ -1313,6 +1316,11 @@ describe("SessionTransfer", () => {
completedCompactionID,
])
expect(yield* Bus.latestSequence(db, sessionID)).toBe(4)
expect((yield* transfer.export({ sessionID })).messages.at(-1)).toMatchObject({ model, providerState })
expect((yield* transfer.export({ sessionID, sanitize: true })).messages.at(-1)).toMatchObject({
model,
providerState: { redacted: `compaction-provider-state:${completedCompactionID}` },
})
}),
)

Expand Down
23 changes: 20 additions & 3 deletions packages/core/test/session-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2141,7 +2141,13 @@ describe("SessionRunnerLLM", () => {
yield* s.llm.push(
TestLLM.tool("call-prefix", "echo", { text: "x".repeat(4_000) }),
TestLLM.textWithUsage("Earlier answer", "prefix-answer", 185_000),
TestLLM.text("## Objective\n- Checkpoint summary", "prefix-summary"),
TestLLM.complete(
{
reason: { normalized: "stop" },
providerMetadata: { [s.currentModel.provider]: { responseId: "summary" } },
},
LLMEvent.textDelta({ id: "prefix-summary", text: "## Objective\n- Checkpoint summary" }),
),
)
yield* s.runPrompt("Review these changes")
if (reason === "manual") {
Expand Down Expand Up @@ -2177,6 +2183,10 @@ describe("SessionRunnerLLM", () => {
expect(compact.system.map((part) => part.text)).toContain("Review the project carefully.")
expect(requestAgents[2]).toBe(Agent.ID.make("compaction"))
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 },
providerState: { responseId: "summary" },
})

// Compare wire content without the cache breakpoints that move to the new final message.
const before = yield* compileRequest(LLMRequest.update(normal, { cache: "none" }))
Expand Down Expand Up @@ -2209,8 +2219,14 @@ describe("SessionRunnerLLM", () => {
)
: TestLLM.text("Let me search the codebase. I will fill in ## Objective later.", "invalid-summary")
yield* s.llm.push(
invalid,
summary ? TestLLM.text("### Active\n- Recovered summary", "summary-recovered") : invalid,
invalid.map((event) =>
LLMEvent.is.stepFinish(event)
? { ...event, providerMetadata: { openai: { responseId: "rejected-summary-state" } } }
: event,
),
summary
? [LLMEvent.textDelta({ id: "summary-recovered", text: "### Active\n- Recovered summary" })]
: invalid,
)
const compact = yield* s.session.compact({ sessionID })
yield* s.resume
Expand All @@ -2220,6 +2236,7 @@ describe("SessionRunnerLLM", () => {
expect(userTexts(s.requests[1]).at(-1)).toContain("did not fill in the required summary template")
expect(s.requests.every((request) => request.toolChoice === undefined)).toBe(true)
expect(s.executions).toEqual([])
expect(JSON.stringify(yield* s.messages)).not.toContain("rejected-summary-state")
expect((yield* s.messages).find((message) => message.id === compact.id)).toMatchObject(
summary
? { status: "completed", summary: "### Active\n- Recovered summary" }
Expand Down
2 changes: 2 additions & 0 deletions packages/schema/src/session-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,8 @@ export namespace Compaction {
schema: {
...Base,
reason: Started.data.fields.reason,
model: SessionMessage.CompactionCompleted.fields.model,
providerState: SessionMessage.CompactionCompleted.fields.providerState,
text: Schema.String,
recent: Schema.String,
},
Expand Down
2 changes: 2 additions & 0 deletions packages/schema/src/session-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@ export const CompactionCompleted = Schema.Struct({
...CompactionBase,
status: Schema.tag("completed"),
reason: Schema.Literals(["auto", "manual"]),
model: Model.Ref.pipe(optional),
providerState: ProviderState.pipe(optional),
summary: Schema.String,
recent: Schema.String,
}).annotate({ identifier: "Session.Message.Compaction.Completed" })
Expand Down
Loading