@100xprompt/brain 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +48 -0
- package/README.md +235 -2
- package/package.json +48 -4
- package/src/adapters/aider/index.ts +8 -0
- package/src/adapters/aider/reader.ts +129 -0
- package/src/adapters/aider/records.ts +220 -0
- package/src/adapters/aider/writer.ts +269 -0
- package/src/adapters/amazon-q-cli/index.ts +8 -0
- package/src/adapters/amazon-q-cli/reader.ts +84 -0
- package/src/adapters/amazon-q-cli/records.ts +191 -0
- package/src/adapters/amazon-q-cli/writer.ts +370 -0
- package/src/adapters/amazon-q-ide/index.ts +9 -0
- package/src/adapters/amazon-q-ide/reader.ts +131 -0
- package/src/adapters/amazon-q-ide/records.ts +141 -0
- package/src/adapters/amazon-q-ide/writer.ts +325 -0
- package/src/adapters/antigravity/index.ts +9 -0
- package/src/adapters/antigravity/proto.ts +194 -0
- package/src/adapters/antigravity/reader.ts +424 -0
- package/src/adapters/antigravity/records.ts +236 -0
- package/src/adapters/antigravity/writer.ts +463 -0
- package/src/adapters/auggie/index.ts +36 -0
- package/src/adapters/claude-code/index.ts +8 -0
- package/src/adapters/claude-code/reader.ts +639 -0
- package/src/adapters/claude-code/records.ts +88 -0
- package/src/adapters/claude-code/writer.ts +329 -0
- package/src/adapters/cline/index.ts +12 -0
- package/src/adapters/cline/reader.ts +432 -0
- package/src/adapters/cline/records.ts +151 -0
- package/src/adapters/cline/vscdb.ts +56 -0
- package/src/adapters/cline/writer.ts +656 -0
- package/src/adapters/cline/xml.ts +177 -0
- package/src/adapters/cline4/index.ts +9 -0
- package/src/adapters/cline4/reader.ts +179 -0
- package/src/adapters/cline4/records.ts +92 -0
- package/src/adapters/cline4/writer.ts +361 -0
- package/src/adapters/codebuff/index.ts +8 -0
- package/src/adapters/codebuff/project.ts +207 -0
- package/src/adapters/codebuff/reader.ts +215 -0
- package/src/adapters/codebuff/records.ts +173 -0
- package/src/adapters/codebuff/writer.ts +383 -0
- package/src/adapters/codex/index.ts +8 -0
- package/src/adapters/codex/native.ts +239 -0
- package/src/adapters/codex/reader.ts +747 -0
- package/src/adapters/codex/records.ts +114 -0
- package/src/adapters/codex/writer.ts +479 -0
- package/src/adapters/continue/index.ts +9 -0
- package/src/adapters/continue/reader.ts +68 -0
- package/src/adapters/continue/records.ts +418 -0
- package/src/adapters/continue/writer.ts +162 -0
- package/src/adapters/copilot-cli/index.ts +9 -0
- package/src/adapters/copilot-cli/reader.ts +186 -0
- package/src/adapters/copilot-cli/records.ts +241 -0
- package/src/adapters/copilot-cli/writer.ts +353 -0
- package/src/adapters/crush/index.ts +8 -0
- package/src/adapters/crush/reader.ts +180 -0
- package/src/adapters/crush/records.ts +169 -0
- package/src/adapters/crush/writer.ts +333 -0
- package/src/adapters/cursor-cli/index.ts +405 -0
- package/src/adapters/cursor-ide/agentkv.ts +95 -0
- package/src/adapters/cursor-ide/index.ts +501 -0
- package/src/adapters/cursor-ide/transcripts.ts +122 -0
- package/src/adapters/cursor-ide/workspaces.ts +119 -0
- package/src/adapters/cursor-ide/writer.ts +170 -0
- package/src/adapters/devin/index.ts +8 -0
- package/src/adapters/devin/reader.ts +305 -0
- package/src/adapters/devin/records.ts +277 -0
- package/src/adapters/devin/writer.ts +523 -0
- package/src/adapters/factory-droid/index.ts +12 -0
- package/src/adapters/factory-droid/reader.ts +184 -0
- package/src/adapters/factory-droid/records.ts +79 -0
- package/src/adapters/factory-droid/writer.ts +350 -0
- package/src/adapters/gemini-cli/exchanges.ts +228 -0
- package/src/adapters/gemini-cli/index.ts +396 -0
- package/src/adapters/gemini-cli/records.ts +521 -0
- package/src/adapters/gemini-cli/writer.ts +354 -0
- package/src/adapters/goose/index.ts +8 -0
- package/src/adapters/goose/reader.ts +138 -0
- package/src/adapters/goose/records.ts +199 -0
- package/src/adapters/goose/writer.ts +340 -0
- package/src/adapters/grok-cli/image.ts +44 -0
- package/src/adapters/grok-cli/index.ts +8 -0
- package/src/adapters/grok-cli/reader.ts +302 -0
- package/src/adapters/grok-cli/records.ts +172 -0
- package/src/adapters/grok-cli/writer.ts +329 -0
- package/src/adapters/hermes/index.ts +8 -0
- package/src/adapters/hermes/reader.ts +249 -0
- package/src/adapters/hermes/records.ts +285 -0
- package/src/adapters/hermes/writer.ts +452 -0
- package/src/adapters/iflow-cli/index.ts +72 -0
- package/src/adapters/iflow-cli/records.ts +215 -0
- package/src/adapters/iflow-cli/writer.ts +204 -0
- package/src/adapters/index.ts +112 -0
- package/src/adapters/kilo-cli/index.ts +21 -0
- package/src/adapters/kilo-code/index.ts +11 -0
- package/src/adapters/kilo-code/writer.ts +92 -0
- package/src/adapters/kimi/index.ts +9 -0
- package/src/adapters/kimi/reader.ts +92 -0
- package/src/adapters/kimi/records.ts +221 -0
- package/src/adapters/kimi/writer.ts +367 -0
- package/src/adapters/kiro-cli/index.ts +8 -0
- package/src/adapters/kiro-cli/reader.ts +87 -0
- package/src/adapters/kiro-cli/records.ts +260 -0
- package/src/adapters/kiro-cli/writer.ts +305 -0
- package/src/adapters/kiro-ide/index.ts +8 -0
- package/src/adapters/kiro-ide/writer.ts +52 -0
- package/src/adapters/letta/index.ts +8 -0
- package/src/adapters/letta/reader.ts +177 -0
- package/src/adapters/letta/records.ts +119 -0
- package/src/adapters/letta/writer.ts +413 -0
- package/src/adapters/mastracode/index.ts +8 -0
- package/src/adapters/mastracode/reader.ts +323 -0
- package/src/adapters/mastracode/records.ts +336 -0
- package/src/adapters/mastracode/writer.ts +385 -0
- package/src/adapters/mistral-vibe/index.ts +9 -0
- package/src/adapters/mistral-vibe/reader.ts +62 -0
- package/src/adapters/mistral-vibe/records.ts +104 -0
- package/src/adapters/mistral-vibe/writer.ts +359 -0
- package/src/adapters/muse/index.ts +10 -0
- package/src/adapters/muse/reader.ts +332 -0
- package/src/adapters/muse/records.ts +104 -0
- package/src/adapters/muse/writer.ts +387 -0
- package/src/adapters/omp/index.ts +13 -0
- package/src/adapters/omp/reader.ts +314 -0
- package/src/adapters/omp/records.ts +153 -0
- package/src/adapters/omp/writer.ts +216 -0
- package/src/adapters/opencode/ids.ts +52 -0
- package/src/adapters/opencode/index.ts +31 -0
- package/src/adapters/opencode/note.ts +8 -0
- package/src/adapters/opencode/reader.ts +452 -0
- package/src/adapters/opencode/tools.ts +174 -0
- package/src/adapters/opencode/v2.ts +367 -0
- package/src/adapters/opencode/writer.ts +379 -0
- package/src/adapters/openhands/index.ts +8 -0
- package/src/adapters/openhands/reader.ts +222 -0
- package/src/adapters/openhands/records.ts +136 -0
- package/src/adapters/openhands/writer.ts +404 -0
- package/src/adapters/pearai/index.ts +10 -0
- package/src/adapters/pi/index.ts +28 -0
- package/src/adapters/pi/reader.ts +261 -0
- package/src/adapters/pi/records.ts +99 -0
- package/src/adapters/pi/writer.ts +362 -0
- package/src/adapters/qoder-cli/index.ts +8 -0
- package/src/adapters/qoder-cli/reader.ts +83 -0
- package/src/adapters/qoder-cli/records.ts +45 -0
- package/src/adapters/qoder-cli/writer.ts +72 -0
- package/src/adapters/qwen-code/index.ts +84 -0
- package/src/adapters/qwen-code/records.ts +194 -0
- package/src/adapters/qwen-code/writer.ts +224 -0
- package/src/adapters/tabnine-cli/index.ts +8 -0
- package/src/adapters/tabnine-cli/reader.ts +106 -0
- package/src/adapters/tabnine-cli/records.ts +33 -0
- package/src/adapters/tabnine-cli/writer.ts +156 -0
- package/src/adapters/types.ts +73 -0
- package/src/adapters/void/index.ts +9 -0
- package/src/adapters/void/reader.ts +48 -0
- package/src/adapters/void/records.ts +285 -0
- package/src/adapters/void/writer.ts +93 -0
- package/src/adapters/vscode-copilot/index.ts +9 -0
- package/src/adapters/vscode-copilot/reader.ts +212 -0
- package/src/adapters/vscode-copilot/records.ts +388 -0
- package/src/adapters/vscode-copilot/shared.ts +68 -0
- package/src/adapters/vscode-copilot/writer.ts +546 -0
- package/src/adapters/zed/index.ts +8 -0
- package/src/adapters/zed/reader.ts +198 -0
- package/src/adapters/zed/records.ts +87 -0
- package/src/adapters/zed/writer.ts +341 -0
- package/src/brief-writers.ts +71 -0
- package/src/brief.ts +238 -0
- package/src/cli.ts +351 -0
- package/src/dashboard/cli.ts +148 -0
- package/src/dashboard/conversions.ts +170 -0
- package/src/dashboard/report.ts +261 -0
- package/src/dashboard/send.ts +213 -0
- package/src/dashboard/state.ts +95 -0
- package/src/fsutil.ts +133 -0
- package/src/gateway/common.ts +232 -0
- package/src/gateway/connect.ts +847 -0
- package/src/gateway/connectors/aider.ts +356 -0
- package/src/gateway/connectors/amazon-q-cli.ts +6 -0
- package/src/gateway/connectors/amazon-q-ide.ts +6 -0
- package/src/gateway/connectors/antigravity.ts +6 -0
- package/src/gateway/connectors/auggie.ts +6 -0
- package/src/gateway/connectors/cline-sdk.ts +85 -0
- package/src/gateway/connectors/cline.ts +34 -0
- package/src/gateway/connectors/codebuff.ts +6 -0
- package/src/gateway/connectors/continue.ts +579 -0
- package/src/gateway/connectors/copilot-cli.ts +138 -0
- package/src/gateway/connectors/crush.ts +313 -0
- package/src/gateway/connectors/cursor-cli.ts +6 -0
- package/src/gateway/connectors/cursor.ts +6 -0
- package/src/gateway/connectors/devin.ts +6 -0
- package/src/gateway/connectors/factory-droid.ts +308 -0
- package/src/gateway/connectors/goose.ts +79 -0
- package/src/gateway/connectors/grok-cli.ts +82 -0
- package/src/gateway/connectors/hermes.ts +230 -0
- package/src/gateway/connectors/iflow-cli.ts +50 -0
- package/src/gateway/connectors/index.ts +86 -0
- package/src/gateway/connectors/kilo-cli.ts +434 -0
- package/src/gateway/connectors/kilo-code.ts +32 -0
- package/src/gateway/connectors/kimi-code.ts +241 -0
- package/src/gateway/connectors/kiro-cli.ts +6 -0
- package/src/gateway/connectors/kiro-ide.ts +6 -0
- package/src/gateway/connectors/letta-code.ts +170 -0
- package/src/gateway/connectors/mastracode.ts +91 -0
- package/src/gateway/connectors/mistral-vibe.ts +495 -0
- package/src/gateway/connectors/muse.ts +27 -0
- package/src/gateway/connectors/omp.ts +279 -0
- package/src/gateway/connectors/openhands.ts +296 -0
- package/src/gateway/connectors/pearai.ts +52 -0
- package/src/gateway/connectors/pi.ts +357 -0
- package/src/gateway/connectors/qoder-cli.ts +6 -0
- package/src/gateway/connectors/qwen-code.ts +411 -0
- package/src/gateway/connectors/roo.ts +43 -0
- package/src/gateway/connectors/tabnine-cli.ts +6 -0
- package/src/gateway/connectors/void.ts +6 -0
- package/src/gateway/connectors/vscode-copilot.ts +286 -0
- package/src/gateway/connectors/windsurf.ts +6 -0
- package/src/gateway/connectors/zed.ts +29 -0
- package/src/gateway/signin.ts +323 -0
- package/src/handoff.ts +104 -0
- package/src/hooks.ts +455 -0
- package/src/memory/audit.ts +514 -0
- package/src/memory/blobs.ts +94 -0
- package/src/memory/cli.ts +117 -0
- package/src/memory/distill.ts +381 -0
- package/src/memory/externalize.ts +435 -0
- package/src/memory/lookups.ts +70 -0
- package/src/memory/mcp-install.ts +401 -0
- package/src/memory/mcp-server.ts +296 -0
- package/src/memory/prepare.ts +179 -0
- package/src/memory/preview.ts +154 -0
- package/src/memory/project.ts +35 -0
- package/src/memory/redact.ts +72 -0
- package/src/memory/relevance.ts +187 -0
- package/src/memory/repomap.ts +516 -0
- package/src/memory/store.ts +270 -0
- package/src/memory/sync.ts +166 -0
- package/src/memory/tidy.ts +193 -0
- package/src/memory/types.ts +118 -0
- package/src/org/admin-ops.ts +96 -0
- package/src/org/auth.ts +112 -0
- package/src/org/backup.ts +115 -0
- package/src/org/cli.ts +438 -0
- package/src/org/client.ts +404 -0
- package/src/org/config.ts +44 -0
- package/src/org/crypto.ts +537 -0
- package/src/org/db.ts +435 -0
- package/src/org/graph.ts +318 -0
- package/src/org/oidc.ts +435 -0
- package/src/org/policy.ts +398 -0
- package/src/org/server.ts +920 -0
- package/src/org/sso.ts +316 -0
- package/src/org/tls.ts +66 -0
- package/src/org/types.ts +193 -0
- package/src/self.ts +68 -0
- package/src/session/attachments.ts +53 -0
- package/src/session/peekcache.ts +50 -0
- package/src/session/types.ts +104 -0
- package/src/session/util.ts +130 -0
- package/src/sqlite.ts +48 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { join } from "node:path"
|
|
2
|
+
import { exists, gitInfo, readJson, readJsonl } from "../../fsutil.ts"
|
|
3
|
+
import type { Session, Turn, Usage } from "../../session/types.ts"
|
|
4
|
+
import { clip, header, iso, lossCounter, noteModel } from "../../session/util.ts"
|
|
5
|
+
import type { Reader, SessionRef } from "../types.ts"
|
|
6
|
+
import { CONTINUED_NOTE, LABEL, MANIFEST, REMINDER, TOOL, conversationDirs, lettaToolKind } from "./records.ts"
|
|
7
|
+
|
|
8
|
+
// Letta Code's local-backend conversations (records.ts has the layout). Every conversation that has a
|
|
9
|
+
// transcript is listed, the agents' default conversations included; hidden ones (subagents, workflow
|
|
10
|
+
// workers) are not. A message Letta rewrote is read as its last snapshot, where it first appeared. A
|
|
11
|
+
// compaction turn goes where the model's context starts over (before the first message it kept), with the
|
|
12
|
+
// turns it replaced kept in front of it. Server-side ("cloud") Letta conversations are not on disk.
|
|
13
|
+
|
|
14
|
+
type Json = Record<string, any>
|
|
15
|
+
type Lost = ReturnType<typeof lossCounter>
|
|
16
|
+
|
|
17
|
+
/** A user message's words: its text parts without the system reminders Letta (or brain) put there. */
|
|
18
|
+
function userText(content: unknown, lost: Lost): { text: string; reminders: string[] } {
|
|
19
|
+
const parts: string[] = []
|
|
20
|
+
const reminders: string[] = []
|
|
21
|
+
for (const c of typeof content === "string" ? [{ type: "text", text: content }] : Array.isArray(content) ? content : []) {
|
|
22
|
+
if (c?.type === "text" && typeof c.text === "string") {
|
|
23
|
+
reminders.push(...(c.text.match(REMINDER) ?? []))
|
|
24
|
+
const t = c.text.replace(REMINDER, "").trim()
|
|
25
|
+
if (t) parts.push(t)
|
|
26
|
+
} else if (c?.type === "image") lost.add("images")
|
|
27
|
+
}
|
|
28
|
+
return { text: parts.join("\n\n"), reminders }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const usageOf = (u: Json | undefined): Usage | undefined => {
|
|
32
|
+
if (!u || typeof u !== "object") return undefined
|
|
33
|
+
const out: Usage = {}
|
|
34
|
+
if (u.input) out.input = u.input
|
|
35
|
+
if (u.output) out.output = u.output
|
|
36
|
+
if (u.cacheRead) out.cacheRead = u.cacheRead
|
|
37
|
+
if (u.cacheWrite) out.cacheWrite = u.cacheWrite
|
|
38
|
+
return Object.keys(out).length ? out : undefined
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A packed summary's words: Letta stores the summary itself on the compaction entry and in metadata. */
|
|
42
|
+
const summaryOf = (entry: Json) =>
|
|
43
|
+
typeof entry.summary === "string" ? entry.summary : typeof entry.message?.metadata?.compaction?.summary === "string" ? entry.message.metadata.compaction.summary : ""
|
|
44
|
+
|
|
45
|
+
export const lettaReader: Reader = {
|
|
46
|
+
tool: TOOL,
|
|
47
|
+
label: LABEL,
|
|
48
|
+
async list(home) {
|
|
49
|
+
const refs: SessionRef[] = []
|
|
50
|
+
for (const { dir, key, record } of conversationDirs(home)) {
|
|
51
|
+
if (!record?.id || record.hidden === true || record.agent_free === true || record.is_subagent === true) continue
|
|
52
|
+
const transcript = join(dir, "messages.jsonl")
|
|
53
|
+
if (!exists(transcript)) continue
|
|
54
|
+
const head = readJsonl(transcript).rows[0]
|
|
55
|
+
const cwd = head?.type === "session" && typeof head.cwd === "string" ? head.cwd : undefined
|
|
56
|
+
const updated = iso(record.last_message_at) ?? iso(record.updated_at)
|
|
57
|
+
refs.push({
|
|
58
|
+
tool: TOOL,
|
|
59
|
+
sourceId: key,
|
|
60
|
+
path: dir,
|
|
61
|
+
title: typeof record.summary === "string" && record.summary ? record.summary : record.id === "default" ? `Default conversation of ${record.agent_id}` : String(record.id),
|
|
62
|
+
...(cwd ? { cwd } : {}),
|
|
63
|
+
...(updated ? { updatedAt: updated } : {}),
|
|
64
|
+
extra: { agentId: record.agent_id, conversationId: record.id },
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
return refs.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
|
|
68
|
+
},
|
|
69
|
+
async read(ref, home) {
|
|
70
|
+
const dir = ref.path
|
|
71
|
+
const record = readJson<Json>(join(dir, "conversation.json"))
|
|
72
|
+
if (!record) throw new Error(`${dir} has no conversation.json`)
|
|
73
|
+
const manifest = readJson<Json>(join(dir, "manifest.json"))
|
|
74
|
+
if (manifest?.message_format !== MANIFEST.message_format)
|
|
75
|
+
throw new Error(`${dir} is in Letta's transcript format ${String(manifest?.message_format ?? "without a manifest")}; run \`letta local-backend migrate-transcripts\` first`)
|
|
76
|
+
const { rows, bad } = readJsonl(join(dir, "messages.jsonl"))
|
|
77
|
+
return parseTranscript(rows, { key: ref.sourceId, dir, record, home, bad })
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A conversation's transcript rows as a portable session. */
|
|
82
|
+
export function parseTranscript(rows: Json[], ctx: { key: string; dir: string; record: Json; home: string; bad?: number }): Session {
|
|
83
|
+
const head = rows[0]?.type === "session" ? rows[0] : undefined
|
|
84
|
+
const cwd = typeof head?.cwd === "string" && head.cwd ? head.cwd : undefined
|
|
85
|
+
const h = header(TOOL, ctx.key, {
|
|
86
|
+
sourcePath: join(ctx.dir, "messages.jsonl"),
|
|
87
|
+
...(typeof ctx.record.summary === "string" && ctx.record.summary ? { title: ctx.record.summary } : {}),
|
|
88
|
+
project: cwd ? { cwd, ...gitInfo(cwd, ctx.home) } : {},
|
|
89
|
+
...(iso(ctx.record.created_at) ?? iso(head?.timestamp) ? { startedAt: iso(ctx.record.created_at) ?? iso(head?.timestamp) } : {}),
|
|
90
|
+
...(iso(ctx.record.last_message_at) ?? iso(ctx.record.updated_at) ? { updatedAt: iso(ctx.record.last_message_at) ?? iso(ctx.record.updated_at) } : {}),
|
|
91
|
+
})
|
|
92
|
+
const lost = lossCounter()
|
|
93
|
+
if (ctx.bad) lost.add("unreadable transcript lines", ctx.bad)
|
|
94
|
+
|
|
95
|
+
// The last snapshot of each message, kept where it first appeared; compactions placed before what they kept.
|
|
96
|
+
const latest = new Map<string, Json>()
|
|
97
|
+
const order: Json[] = []
|
|
98
|
+
const entryIndex = new Map<string, number>()
|
|
99
|
+
for (const r of rows) {
|
|
100
|
+
if ((r.type !== "message" && r.type !== "compaction") || typeof r.id !== "string" || !r.message || typeof r.message.id !== "string") continue
|
|
101
|
+
if (r.type === "message") {
|
|
102
|
+
if (!latest.has(r.message.id)) {
|
|
103
|
+
entryIndex.set(r.id, order.length)
|
|
104
|
+
order.push(r)
|
|
105
|
+
} else entryIndex.set(r.id, order.findIndex((o) => o.type === "message" && o.message.id === r.message.id))
|
|
106
|
+
latest.set(r.message.id, r.message)
|
|
107
|
+
} else order.push(r)
|
|
108
|
+
}
|
|
109
|
+
const before = new Map<number, Json[]>()
|
|
110
|
+
const placed = new Set<Json>()
|
|
111
|
+
for (const r of order) {
|
|
112
|
+
if (r.type !== "compaction" || typeof r.firstKeptEntryId !== "string") continue
|
|
113
|
+
const k = entryIndex.get(r.firstKeptEntryId)
|
|
114
|
+
if (k === undefined || order[k] === r) continue
|
|
115
|
+
before.set(k, [...(before.get(k) ?? []), r])
|
|
116
|
+
placed.add(r)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const turns: Turn[] = []
|
|
120
|
+
const compaction = (r: Json) => {
|
|
121
|
+
const summary = summaryOf(r)
|
|
122
|
+
if (summary.trim()) turns.push({ kind: "compaction", ...(iso(r.timestamp) ? { at: iso(r.timestamp)! } : {}), summary })
|
|
123
|
+
}
|
|
124
|
+
const summaries = new Set(order.filter((r) => r.type === "compaction").map((r) => r.message.id as string))
|
|
125
|
+
for (const [i, r] of order.entries()) {
|
|
126
|
+
for (const c of before.get(i) ?? []) compaction(c)
|
|
127
|
+
if (r.type === "compaction") {
|
|
128
|
+
if (!placed.has(r)) compaction(r)
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
const m = latest.get(r.message.id)!
|
|
132
|
+
if (summaries.has(m.id)) continue // the compaction entry carries it
|
|
133
|
+
const at = iso(m.metadata?.created_at) ?? iso(r.timestamp)
|
|
134
|
+
const when = at ? { at } : {}
|
|
135
|
+
if (m.role === "user") {
|
|
136
|
+
if (m.metadata?.compaction) {
|
|
137
|
+
compaction({ summary: m.metadata.compaction.summary, timestamp: at })
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
const { text, reminders } = userText(m.content, lost)
|
|
141
|
+
for (const rem of reminders) {
|
|
142
|
+
const continued = rem.match(CONTINUED_NOTE)
|
|
143
|
+
if (continued?.[1] && !h.parent) h.parent = { id: continued[1], kind: "continued" }
|
|
144
|
+
}
|
|
145
|
+
if (text) turns.push({ kind: "user", ...when, text })
|
|
146
|
+
} else if (m.role === "assistant") {
|
|
147
|
+
const model = typeof m.model === "string" && m.model ? m.model : undefined
|
|
148
|
+
noteModel(h, model)
|
|
149
|
+
const usage = usageOf(m.usage)
|
|
150
|
+
let usageGiven = false
|
|
151
|
+
for (const c of Array.isArray(m.content) ? m.content : []) {
|
|
152
|
+
if (c?.type === "text" && typeof c.text === "string") {
|
|
153
|
+
if (!c.text.trim()) continue
|
|
154
|
+
turns.push({ kind: "assistant", ...when, text: c.text, ...(model ? { model } : {}), ...(usage && !usageGiven ? { usage } : {}) })
|
|
155
|
+
usageGiven = true
|
|
156
|
+
} else if (c?.type === "thinking") {
|
|
157
|
+
if (c.redacted) lost.add("redacted reasoning")
|
|
158
|
+
else if (typeof c.thinking === "string" && c.thinking.trim()) turns.push({ kind: "reasoning", ...when, text: c.thinking })
|
|
159
|
+
} else if (c?.type === "toolCall" && typeof c.id === "string") {
|
|
160
|
+
const name = String(c.name ?? "tool")
|
|
161
|
+
turns.push({ kind: "tool_call", ...when, id: c.id, name, toolKind: lettaToolKind(name), input: c.arguments ?? {} })
|
|
162
|
+
} else lost.add("unknown assistant content")
|
|
163
|
+
}
|
|
164
|
+
if (m.stopReason === "error" || m.stopReason === "aborted") lost.add("failed or aborted model responses")
|
|
165
|
+
} else if (m.role === "toolResult") {
|
|
166
|
+
const parts: string[] = []
|
|
167
|
+
for (const c of Array.isArray(m.content) ? m.content : []) {
|
|
168
|
+
if (c?.type === "text" && typeof c.text === "string") parts.push(c.text)
|
|
169
|
+
else if (c?.type === "image") lost.add("images in tool results")
|
|
170
|
+
}
|
|
171
|
+
const out = clip(parts.join("\n"))
|
|
172
|
+
turns.push({ kind: "tool_result", ...when, callId: String(m.toolCallId), ok: m.isError !== true, output: out.text, ...(out.truncated ? { truncated: true } : {}) })
|
|
173
|
+
} else lost.add("unknown message roles")
|
|
174
|
+
}
|
|
175
|
+
lost.flush(h)
|
|
176
|
+
return { header: h, turns }
|
|
177
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { readdirSync, statSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { readJson } from "../../fsutil.ts"
|
|
4
|
+
import type { ToolKind } from "../../session/types.ts"
|
|
5
|
+
import { toolKindOf } from "../../session/util.ts"
|
|
6
|
+
|
|
7
|
+
// What Letta Code's local backend stores, as far as this adapter relies on it. Checked against letta-code's
|
|
8
|
+
// source at 0.34.44 (letta-ai/letta-code 9176d8c, 2026-11-06): src/backend/local/local-store.ts (LocalStore:
|
|
9
|
+
// conversation records, ids, fork, compaction), local-transcript.ts (the session-entry JSONL and its manifest),
|
|
10
|
+
// local-message.ts (pi-ai messages with an id and metadata), local-conversation-record.ts,
|
|
11
|
+
// local-conversation-list.ts and src/utils/local-backend-paths.ts. Catalogue entry 16.
|
|
12
|
+
//
|
|
13
|
+
// Letta keeps its default ("cloud") agents and conversations on a Letta server, never on disk. Only the
|
|
14
|
+
// local backend (`letta --backend local`, agent ids `agent-local-*`) is a store a writer can reach:
|
|
15
|
+
//
|
|
16
|
+
// ~/.letta/lc-local-backend/agents/<base64url(agentId)>.json the agent (system prompt, model, tags)
|
|
17
|
+
// ~/.letta/lc-local-backend/memfs/<agentId>/memory/ its memory, a git repo
|
|
18
|
+
// ~/.letta/lc-local-backend/conversations/<base64url(key)>/ one conversation, key below
|
|
19
|
+
// conversation.json {id, agent_id, summary (the title /resume shows), in_context_message_ids, times}
|
|
20
|
+
// manifest.json {schema_version 2, message_format "pi-session-entry-jsonl", provider_stack "pi-ai"}
|
|
21
|
+
// messages.jsonl a {type:"session", version:3} header, then message and compaction entries on one
|
|
22
|
+
// parentId chain; each holds a pi-ai message (user, assistant, toolResult) with an id
|
|
23
|
+
// system-prompt.json the compiled system prompt, written by Letta on the first turn
|
|
24
|
+
//
|
|
25
|
+
// The key is "default:<agentId>" for an agent's default conversation and "conversation:<id>" for the others;
|
|
26
|
+
// new conversation ids are local-conv-<n> and message ids ui-msg-<n>. Only the messages whose ids are in
|
|
27
|
+
// in_context_message_ids go to the model; a compaction entry's message is the summary that replaces the rest.
|
|
28
|
+
|
|
29
|
+
export const TOOL = "letta-code"
|
|
30
|
+
export const LABEL = "Letta Code"
|
|
31
|
+
|
|
32
|
+
/** Letta Code releases whose local store was checked in source; the writer was run against 0.34.44. */
|
|
33
|
+
export const TESTED_VERSIONS = ["0.34.44"]
|
|
34
|
+
|
|
35
|
+
export const MANIFEST = { schema_version: 2, message_format: "pi-session-entry-jsonl", provider_stack: "pi-ai" } as const
|
|
36
|
+
export const SESSION_HEADER_VERSION = 3
|
|
37
|
+
|
|
38
|
+
export const storageDir = (home: string) => join(home, ".letta", "lc-local-backend")
|
|
39
|
+
export const conversationsDir = (home: string) => join(storageDir(home), "conversations")
|
|
40
|
+
export const agentsDir = (home: string) => join(storageDir(home), "agents")
|
|
41
|
+
|
|
42
|
+
/** LocalStore's encodePathSegment. */
|
|
43
|
+
export const encodeSegment = (v: string) => Buffer.from(v).toString("base64url")
|
|
44
|
+
export const decodeSegment = (v: string) => Buffer.from(v, "base64url").toString("utf8")
|
|
45
|
+
|
|
46
|
+
/** localConversationKey. */
|
|
47
|
+
export const conversationKey = (conversationId: string, agentId: string) =>
|
|
48
|
+
conversationId === "default" ? `default:${agentId}` : `conversation:${conversationId}`
|
|
49
|
+
|
|
50
|
+
export const isLocalAgentId = (id: unknown): id is string => typeof id === "string" && id.startsWith("agent-local-")
|
|
51
|
+
|
|
52
|
+
/** Letta's own tools (src/tools/toolset-catalog.ts), where the name alone does not say what they do. */
|
|
53
|
+
const KINDS: Record<string, ToolKind> = {
|
|
54
|
+
Bash: "shell",
|
|
55
|
+
exec_command: "shell",
|
|
56
|
+
write_stdin: "shell",
|
|
57
|
+
Monitor: "shell",
|
|
58
|
+
Read: "read",
|
|
59
|
+
ViewImage: "read",
|
|
60
|
+
Edit: "edit",
|
|
61
|
+
ApplyPatch: "edit",
|
|
62
|
+
Write: "write",
|
|
63
|
+
Task: "task",
|
|
64
|
+
SendAgentMessage: "task",
|
|
65
|
+
TaskCreate: "todo",
|
|
66
|
+
TaskGet: "todo",
|
|
67
|
+
TaskList: "todo",
|
|
68
|
+
TaskUpdate: "todo",
|
|
69
|
+
UpdatePlan: "todo",
|
|
70
|
+
}
|
|
71
|
+
export const lettaToolKind = (name: string): ToolKind => KINDS[name] ?? toolKindOf(name)
|
|
72
|
+
|
|
73
|
+
/** Letta hides these from the person (backfill.ts removeSystemContextBlocks) but sends them to the model. */
|
|
74
|
+
export const REMINDER = /<system-reminder>[\s\S]*?<\/system-reminder>|<system-alert>[\s\S]*?<\/system-alert>/g
|
|
75
|
+
|
|
76
|
+
/** The note saying where a conversation came from, carried in a system reminder so it is not the person's words. */
|
|
77
|
+
export const continuedNote = (label: string, id: string, when: string) =>
|
|
78
|
+
`<system-reminder>\nThis conversation continues ${label} session ${id}, carried over by brain session on ${when}. The messages before this one are its history; reasoning and anything listed as lost did not carry over. Check files before relying on details, since they may have changed.\n</system-reminder>`
|
|
79
|
+
export const CONTINUED_NOTE = /This conversation continues .+? session (\S+), carried over by brain session/
|
|
80
|
+
|
|
81
|
+
/** The local agents on disk, newest first: id, name, and whether Letta hides it (subagents, workers). */
|
|
82
|
+
export function localAgents(home: string): Array<{ id: string; name: string; hidden: boolean; mtimeMs: number }> {
|
|
83
|
+
const dir = agentsDir(home)
|
|
84
|
+
let names: string[]
|
|
85
|
+
try {
|
|
86
|
+
names = readdirSync(dir).filter((n) => n.endsWith(".json") && !n.startsWith("._"))
|
|
87
|
+
} catch {
|
|
88
|
+
return []
|
|
89
|
+
}
|
|
90
|
+
const out: Array<{ id: string; name: string; hidden: boolean; mtimeMs: number }> = []
|
|
91
|
+
for (const n of names) {
|
|
92
|
+
const rec = readJson<Record<string, unknown>>(join(dir, n))
|
|
93
|
+
if (!rec || !isLocalAgentId(rec.id)) continue
|
|
94
|
+
let mtimeMs = 0
|
|
95
|
+
try {
|
|
96
|
+
mtimeMs = statSync(join(dir, n)).mtimeMs
|
|
97
|
+
} catch {}
|
|
98
|
+
out.push({ id: rec.id, name: typeof rec.name === "string" ? rec.name : rec.id, hidden: rec.hidden === true, mtimeMs })
|
|
99
|
+
}
|
|
100
|
+
return out.sort((a, b) => b.mtimeMs - a.mtimeMs)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Every conversation folder: its decoded key and its record, when the record parses. */
|
|
104
|
+
export function conversationDirs(home: string): Array<{ dir: string; key: string; record?: Record<string, any> }> {
|
|
105
|
+
const root = conversationsDir(home)
|
|
106
|
+
let names: string[]
|
|
107
|
+
try {
|
|
108
|
+
names = readdirSync(root)
|
|
109
|
+
} catch {
|
|
110
|
+
return []
|
|
111
|
+
}
|
|
112
|
+
return names
|
|
113
|
+
.filter((n) => !n.startsWith("."))
|
|
114
|
+
.map((n) => ({ dir: join(root, n), key: decodeSegment(n), record: readJson<Record<string, any>>(join(root, n, "conversation.json")) }))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The number after `prefix` in `id`, 0 when there is none (LocalStore's numericSuffix). */
|
|
118
|
+
export const numericSuffix = (id: unknown, prefix: string) =>
|
|
119
|
+
typeof id === "string" && id.startsWith(prefix) ? Number.parseInt(id.slice(prefix.length), 10) || 0 : 0
|