@100xprompt/brain 0.0.0-stage → 0.1.0
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 +172 -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 +326 -0
- package/src/fsutil.ts +133 -0
- package/src/gateway/connect.ts +845 -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,269 @@
|
|
|
1
|
+
import { closeSync, ftruncateSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeSync } from "node:fs"
|
|
2
|
+
import { join, resolve } from "node:path"
|
|
3
|
+
import { toolLabel } from "../../brief.ts"
|
|
4
|
+
import { exists, isDir } from "../../fsutil.ts"
|
|
5
|
+
import type { Session, Turn } from "../../session/types.ts"
|
|
6
|
+
import type { Readiness, WriteOptions, WriteResult, Writer } from "../types.ts"
|
|
7
|
+
import { runId } from "./reader.ts"
|
|
8
|
+
import {
|
|
9
|
+
HISTORY_FILE,
|
|
10
|
+
INPUT_HISTORY_FILE,
|
|
11
|
+
LABEL,
|
|
12
|
+
SUMMARY_PREFIX,
|
|
13
|
+
TESTED_VERSIONS,
|
|
14
|
+
TOOL,
|
|
15
|
+
aiderVersion,
|
|
16
|
+
assistantText,
|
|
17
|
+
continuedNote,
|
|
18
|
+
escapeLine,
|
|
19
|
+
historyRoot,
|
|
20
|
+
parseRuns,
|
|
21
|
+
pyLines,
|
|
22
|
+
userText,
|
|
23
|
+
} from "./records.ts"
|
|
24
|
+
|
|
25
|
+
// Continues a portable session in Aider as a new run appended to the project's chat history
|
|
26
|
+
// (.aider.chat.history.md at the git root, else the folder), written the way Aider's InputOutput writes
|
|
27
|
+
// one: the "# aider chat started at" line, each request as "#### " lines ending in a Markdown line break,
|
|
28
|
+
// each reply as plain text between blank lines. `aider --restore-chat-history` then gives the model the
|
|
29
|
+
// whole file as messages (utils.split_chat_history_markdown), this run included.
|
|
30
|
+
//
|
|
31
|
+
// Why append, not a fresh file with the old one backed up: Aider treats the file as the project's one
|
|
32
|
+
// history. Every launch appends to it and --restore-chat-history replays all of it, so a continued session
|
|
33
|
+
// is one more launch in that history; moving the existing file aside would hide the person's earlier Aider
|
|
34
|
+
// work from Aider, which is modifying another session. Appending leaves every earlier byte as it was, and a
|
|
35
|
+
// run that does not read back is cut off again at the length the file had before.
|
|
36
|
+
//
|
|
37
|
+
// Aider has no record for tool calls or reasoning. Tool calls and their results travel as text inside the
|
|
38
|
+
// reply they belong to (that is what the model would see in an Aider chat anyway: edits and commands live in
|
|
39
|
+
// the reply's text), reasoning is dropped. Lines the parser would misread ("# ", "#### ", "> " at the start
|
|
40
|
+
// of a reply line) get one leading space, which Markdown ignores. A compaction is written the way Aider's
|
|
41
|
+
// own summariser hands one to the model: a user message starting "I spoke to you previously about a number
|
|
42
|
+
// of things." followed by the turns after it. The prompts also go into .aider.input.history, so the arrow
|
|
43
|
+
// keys recall them as they would in Aider.
|
|
44
|
+
|
|
45
|
+
const shellQuote = (s: string) => (/^[\w./@%+=:,-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`)
|
|
46
|
+
|
|
47
|
+
const pad = (n: number, w = 2) => String(n).padStart(w, "0")
|
|
48
|
+
/** Local time as Python's datetime.now().strftime("%Y-%m-%d %H:%M:%S"). */
|
|
49
|
+
const localStamp = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
|
50
|
+
/** Local time as str(datetime.now()), which prompt_toolkit's FileHistory writes before each entry. */
|
|
51
|
+
const pyNow = (d: Date) => `${localStamp(d)}.${pad(d.getMilliseconds(), 3)}000`
|
|
52
|
+
|
|
53
|
+
/** A fence longer than any run of backticks in the text, so the text cannot close it. */
|
|
54
|
+
const fence = (text: string) => "`".repeat(Math.max(3, ...[...text.matchAll(/`+/g)].map((m) => m[0].length + 1)))
|
|
55
|
+
const fenced = (text: string, lang = "") => {
|
|
56
|
+
const f = fence(text)
|
|
57
|
+
return `${f}${lang}\n${text.replace(/\n+$/, "")}\n${f}`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Text with every line break Aider's parser knows as "\n", so what is written is what reads back. */
|
|
61
|
+
const oneBreak = (text: string) => pyLines(text).join("\n")
|
|
62
|
+
|
|
63
|
+
type Call = Extract<Turn, { kind: "tool_call" }>
|
|
64
|
+
type Result = Extract<Turn, { kind: "tool_result" }>
|
|
65
|
+
|
|
66
|
+
/** A tool call and its result as reply text. */
|
|
67
|
+
function callText(call: Call, result: Result | undefined): string {
|
|
68
|
+
const input = typeof call.input === "string" ? call.input : JSON.stringify(call.input ?? {}, null, 2)
|
|
69
|
+
const parts = [`[Tool call: ${call.name} (${call.toolKind})]`, fenced(input, typeof call.input === "string" ? "" : "json")]
|
|
70
|
+
if (!result) parts.push("[No result was recorded for this call.]")
|
|
71
|
+
else {
|
|
72
|
+
let out = result.output || "(no output)"
|
|
73
|
+
if (result.fullPath) out += `\n\n[Full output: ${result.fullPath}]`
|
|
74
|
+
parts.push(result.ok ? "[Result]" : "[Result: failed]", fenced(out))
|
|
75
|
+
}
|
|
76
|
+
return parts.join("\n")
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface MdPlan {
|
|
80
|
+
/** The run's text, appended to the history file as it stands. */
|
|
81
|
+
history: string
|
|
82
|
+
/** The prompts, in prompt_toolkit FileHistory form, appended to the input history. */
|
|
83
|
+
input: string
|
|
84
|
+
/** The messages Aider's parser must find in the run, for the read-back. */
|
|
85
|
+
messages: Array<{ role: "user" | "assistant"; text: string }>
|
|
86
|
+
lost: string[]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The run that continues `session` in Aider. Pure. */
|
|
90
|
+
export function planRun(session: Session, now: Date): MdPlan {
|
|
91
|
+
const drops = new Map<string, number>()
|
|
92
|
+
const drop = (what: string, n = 1) => drops.set(what, (drops.get(what) ?? 0) + n)
|
|
93
|
+
const label = toolLabel(session.header.tool)
|
|
94
|
+
|
|
95
|
+
// From the last compaction on: Aider keeps no compaction in its file, it hands the model the summary instead.
|
|
96
|
+
let start = 0
|
|
97
|
+
let summary: string | undefined
|
|
98
|
+
session.turns.forEach((t, i) => {
|
|
99
|
+
if (t.kind === "compaction") {
|
|
100
|
+
start = i + 1
|
|
101
|
+
summary = t.summary
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
const before = session.turns.slice(0, start).filter((t) => t.kind !== "compaction").length
|
|
105
|
+
if (before) drop("turns before the last compaction, replaced by its summary", before)
|
|
106
|
+
|
|
107
|
+
const results = new Map<string, Result>()
|
|
108
|
+
for (const t of session.turns) if (t.kind === "tool_result" && !results.has(t.callId)) results.set(t.callId, t)
|
|
109
|
+
const callIds = new Set(session.turns.filter((t) => t.kind === "tool_call").map((t) => (t as Call).id))
|
|
110
|
+
|
|
111
|
+
const messages: MdPlan["messages"] = []
|
|
112
|
+
const prompts: string[] = []
|
|
113
|
+
let reply: string[] = []
|
|
114
|
+
const flush = () => {
|
|
115
|
+
const text = oneBreak(reply.join("\n\n")).trim()
|
|
116
|
+
if (text) messages.push({ role: "assistant", text })
|
|
117
|
+
reply = []
|
|
118
|
+
}
|
|
119
|
+
const user = (text: string, prompt: boolean) => {
|
|
120
|
+
flush()
|
|
121
|
+
messages.push({ role: "user", text: oneBreak(text) })
|
|
122
|
+
if (prompt) prompts.push(oneBreak(text))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
user(continuedNote(label, session.header.id, now.toISOString().slice(0, 10)), false)
|
|
126
|
+
if (summary) user(`${SUMMARY_PREFIX}${summary}`, false)
|
|
127
|
+
const seen = new Set<string>()
|
|
128
|
+
for (const t of session.turns.slice(start)) {
|
|
129
|
+
switch (t.kind) {
|
|
130
|
+
case "user":
|
|
131
|
+
if (t.attachments?.length) drop("attachments (Aider's history holds text only)", t.attachments.length)
|
|
132
|
+
if (t.text.trim()) user(t.text, true)
|
|
133
|
+
break
|
|
134
|
+
case "assistant":
|
|
135
|
+
if (t.text.trim()) reply.push(t.text.trim())
|
|
136
|
+
break
|
|
137
|
+
case "tool_call":
|
|
138
|
+
if (seen.has(t.id)) break
|
|
139
|
+
seen.add(t.id)
|
|
140
|
+
reply.push(callText(t, results.get(t.id)))
|
|
141
|
+
drop("tool calls, kept as text in the replies (Aider has no tool-call record)")
|
|
142
|
+
if (!results.has(t.id)) drop("tool calls without a result")
|
|
143
|
+
break
|
|
144
|
+
case "tool_result":
|
|
145
|
+
if (!callIds.has(t.callId)) drop("tool results without a call")
|
|
146
|
+
break
|
|
147
|
+
case "file_edit":
|
|
148
|
+
if (t.callId && callIds.has(t.callId)) break
|
|
149
|
+
reply.push([`[Edited ${t.path}]`, ...(t.patch ? [fenced(t.patch, "diff")] : [])].join("\n"))
|
|
150
|
+
drop("file edits outside a tool call, kept as text")
|
|
151
|
+
break
|
|
152
|
+
case "reasoning":
|
|
153
|
+
drop("reasoning (Aider's history has no place for it)")
|
|
154
|
+
break
|
|
155
|
+
case "subagent":
|
|
156
|
+
drop("subagent sessions")
|
|
157
|
+
break
|
|
158
|
+
case "compaction":
|
|
159
|
+
break
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
flush()
|
|
163
|
+
|
|
164
|
+
// As InputOutput writes them: user_input() and ai_output() through append_chat_history().
|
|
165
|
+
const lines = [`\n# aider chat started at ${localStamp(now)}\n\n`]
|
|
166
|
+
for (const m of messages) {
|
|
167
|
+
if (m.role === "user") {
|
|
168
|
+
const body = pyLines(m.text).join(" \n#### ").replace(/\s+$/, "")
|
|
169
|
+
lines.push(`\n#### ${body} \n`)
|
|
170
|
+
} else {
|
|
171
|
+
lines.push(`\n${pyLines(m.text).map(escapeLine).join("\n")}\n\n`)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const input = prompts.map((p) => `\n# ${pyNow(now)}\n${p.split("\n").map((l) => `+${l}\n`).join("")}`).join("")
|
|
175
|
+
return { history: lines.join(""), input, messages, lost: [...drops].map(([what, n]) => `${what} (${n})`) }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Appends `text` to `path` and returns how long the file was before, so a failed run can be cut off again. */
|
|
179
|
+
function append(path: string, text: string): number {
|
|
180
|
+
const size = exists(path) ? statSync(path).size : -1
|
|
181
|
+
const fd = openSync(path, "a")
|
|
182
|
+
try {
|
|
183
|
+
writeSync(fd, text)
|
|
184
|
+
} finally {
|
|
185
|
+
closeSync(fd)
|
|
186
|
+
}
|
|
187
|
+
return size
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Puts a file back to the length it had (or removes it when it did not exist). */
|
|
191
|
+
function restore(path: string, size: number) {
|
|
192
|
+
if (size < 0) return rmSync(path, { force: true })
|
|
193
|
+
const fd = openSync(path, "r+")
|
|
194
|
+
try {
|
|
195
|
+
ftruncateSync(fd, size)
|
|
196
|
+
} finally {
|
|
197
|
+
closeSync(fd)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Whether an Aider is running: its command line runs the `aider` script (a pip install runs it through Python,
|
|
203
|
+
* so the process name is not "aider"). A running Aider appends its own lines to the history it has open, and
|
|
204
|
+
* lines it wrote after the continued run would read as part of it.
|
|
205
|
+
*/
|
|
206
|
+
function aiderRunning(): boolean {
|
|
207
|
+
try {
|
|
208
|
+
return Bun.spawnSync(["pgrep", "-f", "(^|/)aider( |$)"], { stdout: "pipe", stderr: "pipe" }).exitCode === 0
|
|
209
|
+
} catch {
|
|
210
|
+
return false
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The Aider writer; `version` and `running` are for tests, which must not depend on an installed Aider. */
|
|
215
|
+
export function aiderWriter(opts: { version?: string; running?: () => boolean } = {}): Writer {
|
|
216
|
+
const installed = () => opts.version ?? aiderVersion()
|
|
217
|
+
const running = opts.running ?? aiderRunning
|
|
218
|
+
return {
|
|
219
|
+
tool: TOOL,
|
|
220
|
+
label: LABEL,
|
|
221
|
+
way: "native",
|
|
222
|
+
|
|
223
|
+
async ready(w: WriteOptions): Promise<Readiness> {
|
|
224
|
+
const v = installed()
|
|
225
|
+
if (!v) return { ok: false, why: "Aider is not installed (`aider --version` did not run)" }
|
|
226
|
+
if (!TESTED_VERSIONS.includes(v)) return { ok: false, why: `Aider ${v} is not a version this writer was checked against (${TESTED_VERSIONS.join(", ")})` }
|
|
227
|
+
if (!isDir(w.cwd)) return { ok: false, why: `${w.cwd} is not a folder; Aider keeps its history in the project` }
|
|
228
|
+
if (running()) return { ok: false, why: "Aider is running; quit it first, or its next lines would land inside the continued session's history" }
|
|
229
|
+
return { ok: true }
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
async write(session: Session, w: WriteOptions): Promise<WriteResult> {
|
|
233
|
+
const cwd = resolve(w.cwd)
|
|
234
|
+
const root = historyRoot(cwd)
|
|
235
|
+
const historyPath = join(root, HISTORY_FILE)
|
|
236
|
+
const inputPath = join(root, INPUT_HISTORY_FILE)
|
|
237
|
+
const plan = planRun(session, new Date())
|
|
238
|
+
let sessionId: string | undefined
|
|
239
|
+
if (!w.dryRun) {
|
|
240
|
+
mkdirSync(root, { recursive: true })
|
|
241
|
+
const historySize = append(historyPath, plan.history)
|
|
242
|
+
// Read back as Aider will: the last run must hold exactly the messages planned.
|
|
243
|
+
const runs = parseRuns(readFileSync(historyPath, "utf8"))
|
|
244
|
+
const got = (runs[runs.length - 1]?.messages ?? []).filter((m) => m.role !== "tool")
|
|
245
|
+
const same =
|
|
246
|
+
got.length === plan.messages.length &&
|
|
247
|
+
got.every((m, i) => m.role === plan.messages[i]!.role && (m.role === "user" ? userText(m.content) : assistantText(m.content)).trim() === plan.messages[i]!.text.trim())
|
|
248
|
+
if (!same) {
|
|
249
|
+
restore(historyPath, historySize)
|
|
250
|
+
throw new Error(`the Aider history did not read back as written (${got.length} of ${plan.messages.length} messages); the file was put back as it was`)
|
|
251
|
+
}
|
|
252
|
+
if (plan.input) append(inputPath, plan.input)
|
|
253
|
+
const run = runs[runs.length - 1]!
|
|
254
|
+
sessionId = runId(historyPath, run, runs)
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
tool: TOOL,
|
|
258
|
+
way: "native",
|
|
259
|
+
...(sessionId ? { sessionId } : {}),
|
|
260
|
+
written: plan.input ? [historyPath, inputPath] : [historyPath],
|
|
261
|
+
open: `cd ${shellQuote(cwd)} && aider --restore-chat-history`,
|
|
262
|
+
lost: plan.lost,
|
|
263
|
+
dryRun: !!w.dryRun,
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export const aiderNativeWriter = aiderWriter()
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Reader, Writer } from "../types.ts"
|
|
2
|
+
import { amazonQCliReader } from "./reader.ts"
|
|
3
|
+
import { amazonQCliNativeWriter } from "./writer.ts"
|
|
4
|
+
|
|
5
|
+
// Amazon Q Developer CLI (`q chat`, superseded by Kiro CLI): reads the per-folder conversations in
|
|
6
|
+
// data.sqlite3 and continues any session as the folder's conversation, which `q chat --resume` restores.
|
|
7
|
+
export const readers: Reader[] = [amazonQCliReader]
|
|
8
|
+
export const writers: Writer[] = [amazonQCliNativeWriter]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import { exists, gitInfo } from "../../fsutil.ts"
|
|
3
|
+
import { withSnapshot } from "../../sqlite.ts"
|
|
4
|
+
import type { Session } from "../../session/types.ts"
|
|
5
|
+
import { header, iso } from "../../session/util.ts"
|
|
6
|
+
import type { Reader, SessionRef } from "../types.ts"
|
|
7
|
+
import { type ConversationState, LABEL, TOOL, dbPath, turnsOf } from "./records.ts"
|
|
8
|
+
|
|
9
|
+
// Amazon Q CLI: one conversation per folder in data.sqlite3's `conversations` table, read from a copy with its
|
|
10
|
+
// write-ahead log. Only conversations with history are listed: those are the ones `q chat --resume` restores.
|
|
11
|
+
|
|
12
|
+
const firstPrompt = (s: ConversationState) => {
|
|
13
|
+
for (const e of s.history ?? []) {
|
|
14
|
+
const c = e.user?.content as Record<string, any> | undefined
|
|
15
|
+
const p = c?.Prompt?.prompt ?? c?.CancelledToolUses?.prompt
|
|
16
|
+
if (typeof p === "string" && p.trim()) return p.replace(/\s+/g, " ").trim().slice(0, 80)
|
|
17
|
+
}
|
|
18
|
+
return undefined
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const lastTime = (s: ConversationState) => {
|
|
22
|
+
for (let i = (s.history?.length ?? 0) - 1; i >= 0; i--) {
|
|
23
|
+
const t = iso(s.history[i]!.user?.timestamp)
|
|
24
|
+
if (t) return t
|
|
25
|
+
}
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function rows(d: Database): Array<{ key: string; state: ConversationState }> {
|
|
30
|
+
const out: Array<{ key: string; state: ConversationState }> = []
|
|
31
|
+
for (const r of d.query("SELECT key, value FROM conversations").all() as Array<{ key: string; value: string }>) {
|
|
32
|
+
try {
|
|
33
|
+
const state = JSON.parse(r.value) as ConversationState
|
|
34
|
+
if (Array.isArray(state.history) && state.history.length) out.push({ key: r.key, state })
|
|
35
|
+
} catch {}
|
|
36
|
+
}
|
|
37
|
+
return out
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The conversation stored for `key`, as a portable session. */
|
|
41
|
+
export function readQ(d: Database, path: string, key: string, home: string): Session {
|
|
42
|
+
const r = d.query("SELECT value FROM conversations WHERE key = ?").get(key) as { value: string } | null
|
|
43
|
+
if (!r) throw new Error(`no Amazon Q conversation for ${key} in ${path}`)
|
|
44
|
+
const state = JSON.parse(r.value) as ConversationState
|
|
45
|
+
const title = firstPrompt(state)
|
|
46
|
+
const h = header(TOOL, String(state.conversation_id), {
|
|
47
|
+
sourcePath: path,
|
|
48
|
+
...(title ? { title } : {}),
|
|
49
|
+
project: { cwd: key, ...gitInfo(key, home) },
|
|
50
|
+
})
|
|
51
|
+
const { turns, continued } = turnsOf(state, h)
|
|
52
|
+
if (continued) h.parent = { id: continued, kind: "continued" }
|
|
53
|
+
const times = turns.map((t) => t.at).filter((t): t is string => !!t)
|
|
54
|
+
if (times[0]) h.startedAt = times[0]
|
|
55
|
+
const last = lastTime(state)
|
|
56
|
+
if (last) h.updatedAt = last
|
|
57
|
+
return { header: h, turns }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const amazonQCliReader: Reader = {
|
|
61
|
+
tool: TOOL,
|
|
62
|
+
label: LABEL,
|
|
63
|
+
async list(home) {
|
|
64
|
+
const db = dbPath(home)
|
|
65
|
+
if (!exists(db)) return []
|
|
66
|
+
try {
|
|
67
|
+
return withSnapshot(db, (d) =>
|
|
68
|
+
rows(d).map(({ key, state }): SessionRef => {
|
|
69
|
+
const title = firstPrompt(state)
|
|
70
|
+
const updatedAt = lastTime(state)
|
|
71
|
+
return { tool: TOOL, sourceId: String(state.conversation_id), path: db, cwd: key, ...(title ? { title } : {}), ...(updatedAt ? { updatedAt } : {}), extra: { key } }
|
|
72
|
+
}),
|
|
73
|
+
).sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
|
|
74
|
+
} catch {
|
|
75
|
+
return []
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
async read(ref, home) {
|
|
79
|
+
if (!exists(ref.path)) throw new Error(`Amazon Q database not found: ${ref.path}`)
|
|
80
|
+
const key = typeof ref.extra?.key === "string" ? ref.extra.key : ref.cwd
|
|
81
|
+
if (!key) throw new Error("this Amazon Q session reference names no folder")
|
|
82
|
+
return withSnapshot(ref.path, (d) => readQ(d, ref.path, key, home))
|
|
83
|
+
},
|
|
84
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { join } from "node:path"
|
|
2
|
+
import { appSupport, versionOf } from "../../fsutil.ts"
|
|
3
|
+
import type { Session, ToolKind, Turn } from "../../session/types.ts"
|
|
4
|
+
import { asText, clip, iso, lossCounter, noteModel, toolKindOf } from "../../session/util.ts"
|
|
5
|
+
import { CONTINUED_NOTE } from "../claude-code/records.ts"
|
|
6
|
+
|
|
7
|
+
// What the Amazon Q Developer CLI keeps, shared by the reader and the writer. Checked against the source of
|
|
8
|
+
// aws/amazon-q-developer-cli at 1.19.7 (crates/chat-cli: database/mod.rs and sqlite_migrations/,
|
|
9
|
+
// cli/chat/conversation.rs, message.rs, parser.rs, context.rs, tools/, and the resume path in cli/chat/mod.rs).
|
|
10
|
+
// Catalogue: docs/sessions/2026-11-08-session-formats.md, "Amazon Q Developer CLI".
|
|
11
|
+
//
|
|
12
|
+
// Q keeps one conversation per folder: a row of the `conversations` table in
|
|
13
|
+
// ~/Library/Application Support/amazon-q/data.sqlite3 whose key is the folder's absolute path and whose value
|
|
14
|
+
// is the ConversationState as serde_json writes it. `q chat --resume` loads the row for the folder it runs
|
|
15
|
+
// in, and only when its history is not empty. The history is a list of entries, each one user message and
|
|
16
|
+
// the assistant's reply to it; tool calls are the reply's tool_uses, answered by the next entry's user
|
|
17
|
+
// message, which carries the results instead of a prompt.
|
|
18
|
+
|
|
19
|
+
export const TOOL = "amazon-q-cli"
|
|
20
|
+
export const LABEL = "Amazon Q CLI"
|
|
21
|
+
|
|
22
|
+
/** Q CLI versions whose conversation JSON the writer was checked against (from source). */
|
|
23
|
+
export const TESTED_VERSIONS = ["1.19.7"]
|
|
24
|
+
|
|
25
|
+
export const dbPath = (home: string) => join(appSupport(home), "amazon-q", "data.sqlite3")
|
|
26
|
+
|
|
27
|
+
/** "1.19.7" from `qchat --version` ("qchat 1.19.7") or `q --version`; undefined when neither runs. */
|
|
28
|
+
export const qVersion = (bins = ["qchat", "q"]) => {
|
|
29
|
+
for (const b of bins) {
|
|
30
|
+
const v = versionOf(b)?.match(/\d+\.\d+\.\d+/)?.[0]
|
|
31
|
+
if (v) return v
|
|
32
|
+
}
|
|
33
|
+
return undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The default agent, whose context manager a new conversation gets (Agent::default, ContextManager::from_agent). */
|
|
37
|
+
export const DEFAULT_AGENT = "q_cli_default"
|
|
38
|
+
export const DEFAULT_CONTEXT_PATHS = ["AmazonQ.md", "AGENTS.md", "README.md", ".amazonq/rules/**/*.md"]
|
|
39
|
+
/** calc_max_context_files_size with no model chosen: 75% of the default 200,000-token window. */
|
|
40
|
+
export const DEFAULT_CONTEXT_FILES_SIZE = 150_000
|
|
41
|
+
|
|
42
|
+
/** Q's reply placeholder for a user message that has none: every history entry needs one. */
|
|
43
|
+
export const NO_REPLY = "[No reply was recorded for this message before the session moved to Amazon Q.]"
|
|
44
|
+
|
|
45
|
+
export type ToolUseResultBlock = { Text: string } | { Json: unknown }
|
|
46
|
+
export interface ToolUseResult {
|
|
47
|
+
tool_use_id: string
|
|
48
|
+
content: ToolUseResultBlock[]
|
|
49
|
+
status: "Success" | "Error"
|
|
50
|
+
}
|
|
51
|
+
export type UserContent =
|
|
52
|
+
| { Prompt: { prompt: string } }
|
|
53
|
+
| { CancelledToolUses: { prompt: string | null; tool_use_results: ToolUseResult[] } }
|
|
54
|
+
| { ToolUseResults: { tool_use_results: ToolUseResult[] } }
|
|
55
|
+
export interface UserMessage {
|
|
56
|
+
additional_context: string
|
|
57
|
+
env_context: { env_state: { operating_system: string | null; current_working_directory: string | null; environment_variables: unknown[] } | null }
|
|
58
|
+
content: UserContent
|
|
59
|
+
timestamp: string | null
|
|
60
|
+
images: Array<{ format: string; source: { Bytes: number[] } }> | null
|
|
61
|
+
}
|
|
62
|
+
export interface AssistantToolUse {
|
|
63
|
+
id: string
|
|
64
|
+
name: string
|
|
65
|
+
orig_name: string
|
|
66
|
+
args: unknown
|
|
67
|
+
orig_args: unknown
|
|
68
|
+
}
|
|
69
|
+
export type AssistantMessage = { Response: { message_id: string | null; content: string } } | { ToolUse: { message_id: string | null; content: string; tool_uses: AssistantToolUse[] } }
|
|
70
|
+
export interface HistoryEntry {
|
|
71
|
+
user: UserMessage
|
|
72
|
+
assistant: AssistantMessage
|
|
73
|
+
request_metadata: unknown
|
|
74
|
+
}
|
|
75
|
+
/** ConversationState as serialized; fields this adapter does not use are left loose. */
|
|
76
|
+
export interface ConversationState {
|
|
77
|
+
conversation_id: string
|
|
78
|
+
next_message: UserMessage | null
|
|
79
|
+
history: HistoryEntry[]
|
|
80
|
+
valid_history_range: [number, number]
|
|
81
|
+
transcript: string[]
|
|
82
|
+
tools: Record<string, unknown>
|
|
83
|
+
context_manager: unknown
|
|
84
|
+
context_message_length: number | null
|
|
85
|
+
latest_summary: [string, unknown] | null
|
|
86
|
+
model?: string | null
|
|
87
|
+
model_info?: { model_id?: string } | null
|
|
88
|
+
file_line_tracker?: Record<string, unknown>
|
|
89
|
+
checkpoint_manager: unknown
|
|
90
|
+
mcp_enabled?: boolean
|
|
91
|
+
[k: string]: unknown
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** What a Q tool does. Its built-ins are fs_read (with operations), fs_write (with a command), execute_bash. */
|
|
95
|
+
export function qToolKind(name: string, args: unknown): ToolKind {
|
|
96
|
+
const a = (args && typeof args === "object" ? args : {}) as Record<string, any>
|
|
97
|
+
if (name === "fs_read") {
|
|
98
|
+
const mode = Array.isArray(a.operations) ? a.operations[0]?.mode : a.mode
|
|
99
|
+
return mode === "Directory" ? "list" : mode === "Search" ? "search" : "read"
|
|
100
|
+
}
|
|
101
|
+
if (name === "fs_write") return a.command === "create" ? "write" : "edit"
|
|
102
|
+
if (name === "execute_bash" || name === "execute_cmd") return "shell"
|
|
103
|
+
if (name === "use_aws") return "shell"
|
|
104
|
+
if (name === "todo_list") return "todo"
|
|
105
|
+
if (name === "delegate") return "task"
|
|
106
|
+
return toolKindOf(name)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const str = (v: unknown) => (typeof v === "string" ? v : undefined)
|
|
110
|
+
|
|
111
|
+
/** A call as Q's own built-in tool would have been called, when its arguments say what that tool needs. */
|
|
112
|
+
export function qCall(c: Extract<Turn, { kind: "tool_call" }>): { name: string; args: Record<string, unknown> } | undefined {
|
|
113
|
+
const i = (c.input && typeof c.input === "object" && !Array.isArray(c.input) ? c.input : {}) as Record<string, unknown>
|
|
114
|
+
const path = str(i.file_path) ?? str(i.path) ?? str(i.filePath) ?? str(i.target_file) ?? str(i.absolute_path)
|
|
115
|
+
switch (c.toolKind) {
|
|
116
|
+
case "shell": {
|
|
117
|
+
const command = str(i.command) ?? str(i.cmd) ?? (Array.isArray(i.command) ? (i.command as unknown[]).map(String).join(" ") : undefined)
|
|
118
|
+
return command ? { name: "execute_bash", args: { command } } : undefined
|
|
119
|
+
}
|
|
120
|
+
case "write": {
|
|
121
|
+
const text = str(i.content) ?? str(i.file_text) ?? str(i.contents) ?? str(i.text)
|
|
122
|
+
return path && text !== undefined ? { name: "fs_write", args: { command: "create", path, file_text: text } } : undefined
|
|
123
|
+
}
|
|
124
|
+
case "edit": {
|
|
125
|
+
const before = str(i.old_string) ?? str(i.old_str) ?? str(i.oldText) ?? str(i.oldStr)
|
|
126
|
+
const after = str(i.new_string) ?? str(i.new_str) ?? str(i.newText) ?? str(i.newStr)
|
|
127
|
+
return path && before !== undefined && after !== undefined ? { name: "fs_write", args: { command: "str_replace", path, old_str: before, new_str: after } } : undefined
|
|
128
|
+
}
|
|
129
|
+
case "read":
|
|
130
|
+
return path ? { name: "fs_read", args: { operations: [{ mode: "Line", path }] } } : undefined
|
|
131
|
+
case "list":
|
|
132
|
+
return path ? { name: "fs_read", args: { operations: [{ mode: "Directory", path }] } } : undefined
|
|
133
|
+
}
|
|
134
|
+
return undefined
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const blockText = (b: ToolUseResultBlock) => ("Text" in b ? b.Text : asText(b.Json))
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The portable turns of a Q conversation: the summary of a compaction first (Q keeps only the summary of the
|
|
141
|
+
* turns it replaced), then the history. Returns the id of the session a brain-written conversation continues,
|
|
142
|
+
* when its note is there.
|
|
143
|
+
*/
|
|
144
|
+
export function turnsOf(state: ConversationState, h: Session["header"]): { turns: Turn[]; continued?: string } {
|
|
145
|
+
const turns: Turn[] = []
|
|
146
|
+
const lost = lossCounter()
|
|
147
|
+
let continued: string | undefined
|
|
148
|
+
const model = state.model_info?.model_id ?? state.model ?? undefined
|
|
149
|
+
noteModel(h, model ?? undefined)
|
|
150
|
+
if (Array.isArray(state.latest_summary) && typeof state.latest_summary[0] === "string" && state.latest_summary[0].trim())
|
|
151
|
+
turns.push({ kind: "compaction", summary: state.latest_summary[0] })
|
|
152
|
+
const results = (rs: ToolUseResult[], at?: string) => {
|
|
153
|
+
for (const r of rs) {
|
|
154
|
+
const c = clip((r.content ?? []).map(blockText).join("\n"))
|
|
155
|
+
turns.push({ kind: "tool_result", ...(at ? { at } : {}), callId: String(r.tool_use_id), ok: r.status !== "Error", output: c.text, ...(c.truncated ? { truncated: true } : {}) })
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const prompt = (text: string | null | undefined, at?: string) => {
|
|
159
|
+
if (!text?.trim()) return
|
|
160
|
+
const note = text.match(CONTINUED_NOTE)
|
|
161
|
+
if (note) continued = note[1]
|
|
162
|
+
else turns.push({ kind: "user", ...(at ? { at } : {}), text })
|
|
163
|
+
}
|
|
164
|
+
for (const e of state.history ?? []) {
|
|
165
|
+
const u = e.user
|
|
166
|
+
const at = iso(u?.timestamp)
|
|
167
|
+
const note = u?.additional_context?.match(CONTINUED_NOTE)
|
|
168
|
+
if (note) continued = note[1]
|
|
169
|
+
if (u?.images?.length) lost.add("images", u.images.length)
|
|
170
|
+
const content = u?.content ?? ({} as UserContent)
|
|
171
|
+
if ("Prompt" in content) prompt(content.Prompt.prompt, at)
|
|
172
|
+
else if ("ToolUseResults" in content) results(content.ToolUseResults.tool_use_results ?? [], at)
|
|
173
|
+
else if ("CancelledToolUses" in content) {
|
|
174
|
+
results(content.CancelledToolUses.tool_use_results ?? [], at)
|
|
175
|
+
prompt(content.CancelledToolUses.prompt, at)
|
|
176
|
+
}
|
|
177
|
+
const a = e.assistant ?? ({} as AssistantMessage)
|
|
178
|
+
const reply = "Response" in a ? a.Response : "ToolUse" in a ? a.ToolUse : undefined
|
|
179
|
+
if (reply?.content?.trim() && reply.content !== NO_REPLY) turns.push({ kind: "assistant", text: reply.content, ...(model ? { model } : {}) })
|
|
180
|
+
if ("ToolUse" in a)
|
|
181
|
+
for (const t of a.ToolUse.tool_uses ?? []) {
|
|
182
|
+
// Q renames a call to a tool it no longer has to "dummy" when it resumes; orig_name is what was called.
|
|
183
|
+
const name = t.orig_name || t.name
|
|
184
|
+
const input = t.orig_args ?? t.args ?? {}
|
|
185
|
+
turns.push({ kind: "tool_call", id: String(t.id), name, toolKind: qToolKind(name, input), input })
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (state.next_message) lost.add("an unsent message")
|
|
189
|
+
lost.flush(h)
|
|
190
|
+
return { turns, ...(continued ? { continued } : {}) }
|
|
191
|
+
}
|