@deep-assistant/agent
Version:
A minimal, public domain AI CLI agent compatible with OpenCode's JSON interface. Bun-only runtime.
180 lines (167 loc) • 5.9 kB
text/typescript
import { Provider } from "../provider/provider"
import { fn } from "../util/fn"
import z from "zod"
import { Session } from "."
import { generateText, type ModelMessage } from "ai"
import { MessageV2 } from "./message-v2"
import { Identifier } from "../id/id"
import { Snapshot } from "../snapshot"
import { ProviderTransform } from "../provider/transform"
import { SystemPrompt } from "./system"
import { Log } from "../util/log"
import path from "path"
import { Instance } from "../project/instance"
import { Storage } from "../storage/storage"
import { Bus } from "../bus"
export namespace SessionSummary {
const log = Log.create({ service: "session.summary" })
export const summarize = fn(
z.object({
sessionID: z.string(),
messageID: z.string(),
}),
async (input) => {
const all = await Session.messages({ sessionID: input.sessionID })
await Promise.all([
summarizeSession({ sessionID: input.sessionID, messages: all }),
summarizeMessage({ messageID: input.messageID, messages: all }),
])
},
)
async function summarizeSession(input: { sessionID: string; messages: MessageV2.WithParts[] }) {
const files = new Set(
input.messages
.flatMap((x) => x.parts)
.filter((x) => x.type === "patch")
.flatMap((x) => x.files)
.map((x) => path.relative(Instance.worktree, x)),
)
const diffs = await computeDiff({ messages: input.messages }).then((x) =>
x.filter((x) => {
return files.has(x.file)
}),
)
await Session.update(input.sessionID, (draft) => {
draft.summary = {
additions: diffs.reduce((sum, x) => sum + x.additions, 0),
deletions: diffs.reduce((sum, x) => sum + x.deletions, 0),
files: diffs.length,
}
})
await Storage.write(["session_diff", input.sessionID], diffs)
Bus.publish(Session.Event.Diff, {
sessionID: input.sessionID,
diff: diffs,
})
}
async function summarizeMessage(input: { messageID: string; messages: MessageV2.WithParts[] }) {
const messages = input.messages.filter(
(m) => m.info.id === input.messageID || (m.info.role === "assistant" && m.info.parentID === input.messageID),
)
const msgWithParts = messages.find((m) => m.info.id === input.messageID)!
const userMsg = msgWithParts.info as MessageV2.User
const diffs = await computeDiff({ messages })
userMsg.summary = {
...userMsg.summary,
diffs,
}
await Session.updateMessage(userMsg)
const assistantMsg = messages.find((m) => m.info.role === "assistant")!.info as MessageV2.Assistant
const small = await Provider.getSmallModel(assistantMsg.providerID)
if (!small) return
const textPart = msgWithParts.parts.find((p) => p.type === "text" && !p.synthetic) as MessageV2.TextPart
if (textPart && !userMsg.summary?.title) {
const result = await generateText({
maxOutputTokens: small.info.reasoning ? 1500 : 20,
providerOptions: ProviderTransform.providerOptions(small.npm, small.providerID, {}),
messages: [
...SystemPrompt.title(small.providerID).map(
(x): ModelMessage => ({
role: "system",
content: x,
}),
),
{
role: "user" as const,
content: `
The following is the text to summarize:
<text>
${textPart?.text ?? ""}
</text>
`,
},
],
headers: small.info.headers,
model: small.language,
})
log.info("title", { title: result.text })
userMsg.summary.title = result.text
await Session.updateMessage(userMsg)
}
if (
messages.some(
(m) =>
m.info.role === "assistant" && m.parts.some((p) => p.type === "step-finish" && p.reason !== "tool-calls"),
)
) {
let summary = messages
.findLast((m) => m.info.role === "assistant")
?.parts.findLast((p) => p.type === "text")?.text
if (!summary || diffs.length > 0) {
const result = await generateText({
model: small.language,
maxOutputTokens: 100,
messages: [
{
role: "user",
content: `
Summarize the following conversation into 2 sentences MAX explaining what the assistant did and why. Do not explain the user's input. Do not speak in the third person about the assistant.
<conversation>
${JSON.stringify(MessageV2.toModelMessage(messages))}
</conversation>
`,
},
],
headers: small.info.headers,
}).catch(() => {})
if (result) summary = result.text
}
userMsg.summary.body = summary
log.info("body", { body: summary })
await Session.updateMessage(userMsg)
}
}
export const diff = fn(
z.object({
sessionID: Identifier.schema("session"),
messageID: Identifier.schema("message").optional(),
}),
async (input) => {
return Storage.read<Snapshot.FileDiff[]>(["session_diff", input.sessionID]).catch(() => [])
},
)
async function computeDiff(input: { messages: MessageV2.WithParts[] }) {
let from: string | undefined
let to: string | undefined
// scan assistant messages to find earliest from and latest to
// snapshot
for (const item of input.messages) {
if (!from) {
for (const part of item.parts) {
if (part.type === "step-start" && part.snapshot) {
from = part.snapshot
break
}
}
}
for (const part of item.parts) {
if (part.type === "step-finish" && part.snapshot) {
to = part.snapshot
break
}
}
}
if (from && to) return Snapshot.diffFull(from, to)
return []
}
}