open-agents-ai 0.11.3 → 0.11.5

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.
Files changed (2) hide show
  1. package/dist/index.js +1143 -42
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6587,7 +6587,8 @@ Commands run non-interactively (CI=true). When running scaffolding tools:
6587
6587
  requestTimeoutMs: options?.requestTimeoutMs ?? 3e5,
6588
6588
  taskTimeoutMs: options?.taskTimeoutMs ?? 12e5,
6589
6589
  compactionThreshold: options?.compactionThreshold ?? 4e4,
6590
- dynamicContext: options?.dynamicContext ?? ""
6590
+ dynamicContext: options?.dynamicContext ?? "",
6591
+ streamEnabled: options?.streamEnabled ?? false
6591
6592
  };
6592
6593
  }
6593
6594
  /** Register a tool for the agent to use */
@@ -6683,13 +6684,14 @@ Integrate this guidance into your current approach. Continue working on the task
6683
6684
  });
6684
6685
  }
6685
6686
  const compacted = this.compactMessages(messages);
6686
- const response = await this.backend.chatCompletion({
6687
+ const chatRequest = {
6687
6688
  messages: compacted,
6688
6689
  tools: toolDefs,
6689
6690
  temperature: this.options.temperature,
6690
6691
  maxTokens: this.options.maxTokens,
6691
6692
  timeoutMs: this.options.requestTimeoutMs
6692
- });
6693
+ };
6694
+ const response = this.options.streamEnabled && this.hasStreamingSupport() ? await this.streamingRequest(chatRequest, turn) : await this.backend.chatCompletion(chatRequest);
6693
6695
  totalTokens += response.usage?.totalTokens ?? 0;
6694
6696
  const choice = response.choices[0];
6695
6697
  if (!choice)
@@ -6829,36 +6831,112 @@ ${marker}` : marker);
6829
6831
  const middle = messages.slice(2, -keepRecent);
6830
6832
  if (middle.length === 0)
6831
6833
  return messages;
6832
- const summary = this.summarizeCompactedMessages(middle);
6834
+ let previousSummary = "";
6835
+ const nonCompactionMiddle = [];
6836
+ for (const msg of middle) {
6837
+ if (msg.role === "system" && typeof msg.content === "string" && msg.content.startsWith("[Context compacted")) {
6838
+ previousSummary = msg.content.replace(/^\[Context compacted[^\]]*\]\s*/, "").replace(/\n\n\[Continue from[^\]]*\]\s*$/, "").trim();
6839
+ } else {
6840
+ nonCompactionMiddle.push(msg);
6841
+ }
6842
+ }
6843
+ const newSummary = this.summarizeCompactedMessages(nonCompactionMiddle);
6844
+ const combinedSummary = previousSummary ? this.progressiveSummarize(previousSummary, newSummary) : newSummary;
6833
6845
  this.emit({
6834
6846
  type: "compaction",
6835
- content: `Compacted ${middle.length} messages`,
6847
+ content: `Compacted ${middle.length} messages${previousSummary ? " (progressive)" : ""}`,
6836
6848
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
6837
6849
  });
6838
6850
  const compactionMsg = {
6839
6851
  role: "system",
6840
6852
  content: `[Context compacted \u2014 summary of earlier work]
6841
6853
 
6842
- ${summary}
6854
+ ${combinedSummary}
6843
6855
 
6844
6856
  [Continue from the recent context below. Do not repeat work already completed above.]`
6845
6857
  };
6846
6858
  return [...head, compactionMsg, ...recent];
6847
6859
  }
6848
6860
  /**
6849
- * Extract a structured summary from compacted messages, preserving:
6850
- * - Files read/written/edited
6851
- * - Shell commands run and their outcomes
6852
- * - Key decisions and findings
6861
+ * Progressive summarization: merge an older compacted summary with a newer one.
6862
+ * When the combined text exceeds the budget, condense the older summary.
6863
+ */
6864
+ progressiveSummarize(olderSummary, newerSummary) {
6865
+ const MAX_SUMMARY_CHARS = 4e3;
6866
+ const combined = `${olderSummary}
6867
+
6868
+ ---
6869
+
6870
+ ${newerSummary}`;
6871
+ if (combined.length <= MAX_SUMMARY_CHARS) {
6872
+ return combined;
6873
+ }
6874
+ const condensed = this.condenseSummary(olderSummary);
6875
+ const result = `${condensed}
6876
+
6877
+ ---
6878
+
6879
+ ${newerSummary}`;
6880
+ if (result.length > MAX_SUMMARY_CHARS) {
6881
+ const budget = MAX_SUMMARY_CHARS - newerSummary.length - 60;
6882
+ return budget > 200 ? `[Earlier work, condensed]
6883
+ ${olderSummary.slice(0, budget)}...
6884
+
6885
+ ---
6886
+
6887
+ ${newerSummary}` : newerSummary;
6888
+ }
6889
+ return result;
6890
+ }
6891
+ /**
6892
+ * Condense a summary by keeping section headings and only first 2 items under each.
6893
+ */
6894
+ condenseSummary(summary) {
6895
+ const lines = summary.split("\n");
6896
+ const condensed = [];
6897
+ let itemCount = 0;
6898
+ for (const line of lines) {
6899
+ if (line.startsWith("##") || line.startsWith("---") || line.trim() === "") {
6900
+ condensed.push(line);
6901
+ itemCount = 0;
6902
+ } else if (line.startsWith("- ") || line.startsWith(" - ")) {
6903
+ if (itemCount < 2) {
6904
+ condensed.push(line);
6905
+ } else if (itemCount === 2) {
6906
+ condensed.push(" - ...(condensed)");
6907
+ }
6908
+ itemCount++;
6909
+ } else {
6910
+ condensed.push(line);
6911
+ }
6912
+ }
6913
+ return condensed.join("\n");
6914
+ }
6915
+ /**
6916
+ * Extract a rich structured summary from compacted messages, preserving:
6917
+ * - Assistant analysis/reasoning text
6918
+ * - File contents examined (key snippets, not just names)
6919
+ * - Specific code changes (old_string → new_string)
6920
+ * - Shell command results (test pass/fail, build errors)
6921
+ * - Search findings (grep/find results)
6853
6922
  * - Errors encountered
6854
6923
  */
6855
6924
  summarizeCompactedMessages(messages) {
6856
- const filesRead = /* @__PURE__ */ new Set();
6857
- const filesModified = /* @__PURE__ */ new Set();
6858
- const commandsRun = [];
6925
+ const toolCallMap = /* @__PURE__ */ new Map();
6926
+ const assistantAnalysis = [];
6927
+ const filesRead = /* @__PURE__ */ new Map();
6928
+ const filesModified = /* @__PURE__ */ new Map();
6929
+ const commandResults = [];
6930
+ const searchFindings = [];
6859
6931
  const errors = [];
6860
6932
  let toolCallCount = 0;
6861
6933
  for (const msg of messages) {
6934
+ if (msg.role === "assistant" && typeof msg.content === "string" && msg.content.trim()) {
6935
+ const text = msg.content.trim();
6936
+ if (text.length > 20) {
6937
+ assistantAnalysis.push(text.length > 400 ? text.slice(0, 400) + "..." : text);
6938
+ }
6939
+ }
6862
6940
  if (msg.tool_calls) {
6863
6941
  for (const tc of msg.tool_calls) {
6864
6942
  toolCallCount++;
@@ -6869,37 +6947,134 @@ ${summary}
6869
6947
  return {};
6870
6948
  }
6871
6949
  })();
6950
+ toolCallMap.set(tc.id, { name: tc.function.name, args });
6872
6951
  const name = tc.function.name;
6873
- if (name === "file_read") {
6874
- filesRead.add(String(args.path || ""));
6875
- } else if (name === "file_write" || name === "file_edit" || name === "batch_edit") {
6876
- filesModified.add(String(args.path || ""));
6877
- } else if (name === "shell" || name === "background_run") {
6878
- const cmd = String(args.command || "").slice(0, 80);
6879
- if (cmd)
6880
- commandsRun.push(cmd);
6952
+ if (name === "file_edit") {
6953
+ const path = String(args.path || "");
6954
+ const oldStr = String(args.old_string || "").slice(0, 100);
6955
+ const newStr = String(args.new_string || "").slice(0, 100);
6956
+ if (path) {
6957
+ const changes = filesModified.get(path) || [];
6958
+ changes.push(`"${oldStr}" \u2192 "${newStr}"`);
6959
+ filesModified.set(path, changes);
6960
+ }
6961
+ } else if (name === "file_write") {
6962
+ const path = String(args.path || "");
6963
+ if (path) {
6964
+ if (!filesModified.has(path))
6965
+ filesModified.set(path, []);
6966
+ filesModified.get(path).push("(full file write)");
6967
+ }
6968
+ } else if (name === "batch_edit") {
6969
+ const path = String(args.path || "");
6970
+ if (path) {
6971
+ if (!filesModified.has(path))
6972
+ filesModified.set(path, []);
6973
+ filesModified.get(path).push("(batch edit)");
6974
+ }
6881
6975
  }
6882
6976
  }
6883
6977
  }
6884
- if (msg.role === "tool" && typeof msg.content === "string") {
6885
- if (msg.content.startsWith("Error:")) {
6886
- errors.push(msg.content.slice(0, 150));
6978
+ if (msg.role === "tool" && typeof msg.content === "string" && msg.tool_call_id) {
6979
+ const tc = toolCallMap.get(msg.tool_call_id);
6980
+ const content = msg.content;
6981
+ if (!tc) {
6982
+ if (content.startsWith("Error:"))
6983
+ errors.push(content.slice(0, 200));
6984
+ continue;
6985
+ }
6986
+ switch (tc.name) {
6987
+ case "file_read": {
6988
+ const path = String(tc.args.path || "");
6989
+ const lines = content.split("\n");
6990
+ const summary = lines.length > 5 ? `${lines.length} lines \u2014 ${lines.slice(0, 2).join("; ").slice(0, 120)}...` : content.slice(0, 150);
6991
+ if (path)
6992
+ filesRead.set(path, summary);
6993
+ break;
6994
+ }
6995
+ case "shell":
6996
+ case "background_run": {
6997
+ const cmd = String(tc.args.command || "").slice(0, 100);
6998
+ const hasError = content.startsWith("Error:") || /FAIL|ERR!/i.test(content);
6999
+ const hasPass = /PASS|passed|✓|success/i.test(content);
7000
+ let outcome;
7001
+ if (hasError) {
7002
+ const errorLines = content.split("\n").filter((l) => /error|FAIL|✗|×|ERR!/i.test(l)).slice(0, 3);
7003
+ outcome = errorLines.length > 0 ? errorLines.join("; ").slice(0, 200) : content.slice(0, 200);
7004
+ errors.push(`\`${cmd}\`: ${outcome.slice(0, 150)}`);
7005
+ } else if (hasPass) {
7006
+ outcome = "passed";
7007
+ } else {
7008
+ outcome = content.slice(0, 150);
7009
+ }
7010
+ commandResults.push({ cmd, outcome });
7011
+ break;
7012
+ }
7013
+ case "grep_search": {
7014
+ const pattern = String(tc.args.pattern || "");
7015
+ const matchCount = (content.match(/\n/g) || []).length;
7016
+ searchFindings.push(`grep "${pattern}": ${matchCount} matches \u2014 ${content.slice(0, 150)}`);
7017
+ break;
7018
+ }
7019
+ case "find_files": {
7020
+ const pattern = String(tc.args.pattern || "");
7021
+ const files = content.split("\n").filter(Boolean);
7022
+ searchFindings.push(`find "${pattern}": ${files.length} files \u2014 ${files.slice(0, 5).join(", ")}`);
7023
+ break;
7024
+ }
7025
+ default: {
7026
+ if (content.startsWith("Error:"))
7027
+ errors.push(`${tc.name}: ${content.slice(0, 200)}`);
7028
+ }
6887
7029
  }
6888
7030
  }
6889
7031
  }
6890
7032
  const parts = [];
6891
- parts.push(`${toolCallCount} tool calls were made in the compacted section.`);
7033
+ parts.push(`## Compacted Work Summary (${toolCallCount} tool calls)
7034
+ `);
7035
+ if (assistantAnalysis.length > 0) {
7036
+ parts.push("### Agent Analysis");
7037
+ for (const analysis of assistantAnalysis.slice(-5)) {
7038
+ parts.push(`- ${analysis}`);
7039
+ }
7040
+ parts.push("");
7041
+ }
6892
7042
  if (filesRead.size > 0) {
6893
- parts.push(`Files read: ${Array.from(filesRead).slice(0, 15).join(", ")}`);
7043
+ parts.push("### Files Examined");
7044
+ for (const [path, summary] of Array.from(filesRead).slice(0, 15)) {
7045
+ parts.push(`- \`${path}\`: ${summary}`);
7046
+ }
7047
+ parts.push("");
6894
7048
  }
6895
7049
  if (filesModified.size > 0) {
6896
- parts.push(`Files modified: ${Array.from(filesModified).slice(0, 15).join(", ")}`);
7050
+ parts.push("### Code Changes Made");
7051
+ for (const [path, changes] of Array.from(filesModified).slice(0, 10)) {
7052
+ parts.push(`- **${path}**:`);
7053
+ for (const change of changes.slice(0, 3)) {
7054
+ parts.push(` - ${change}`);
7055
+ }
7056
+ }
7057
+ parts.push("");
6897
7058
  }
6898
- if (commandsRun.length > 0) {
6899
- parts.push(`Commands run: ${commandsRun.slice(0, 8).join("; ")}`);
7059
+ if (commandResults.length > 0) {
7060
+ parts.push("### Commands Executed");
7061
+ for (const { cmd, outcome } of commandResults.slice(0, 8)) {
7062
+ parts.push(`- \`${cmd}\` \u2192 ${outcome}`);
7063
+ }
7064
+ parts.push("");
7065
+ }
7066
+ if (searchFindings.length > 0) {
7067
+ parts.push("### Search Findings");
7068
+ for (const finding of searchFindings.slice(0, 5)) {
7069
+ parts.push(`- ${finding}`);
7070
+ }
7071
+ parts.push("");
6900
7072
  }
6901
7073
  if (errors.length > 0) {
6902
- parts.push(`Errors encountered: ${errors.slice(0, 3).join("; ")}`);
7074
+ parts.push("### Errors Encountered");
7075
+ for (const error of errors.slice(0, 5)) {
7076
+ parts.push(`- ${error}`);
7077
+ }
6903
7078
  }
6904
7079
  return parts.join("\n");
6905
7080
  }
@@ -6916,6 +7091,90 @@ ${summary}
6916
7091
  }
6917
7092
  }));
6918
7093
  }
7094
+ // -------------------------------------------------------------------------
7095
+ // Streaming support — parallel path that emits token events
7096
+ // -------------------------------------------------------------------------
7097
+ /** Check whether the backend supports SSE streaming */
7098
+ hasStreamingSupport() {
7099
+ return typeof this.backend.chatCompletionStream === "function";
7100
+ }
7101
+ /**
7102
+ * Streaming request: calls the SSE endpoint, emits stream events,
7103
+ * assembles and returns the same response format as chatCompletion().
7104
+ * The non-streaming chatCompletion path is NEVER touched by this code.
7105
+ */
7106
+ async streamingRequest(request, turn) {
7107
+ const backend = this.backend;
7108
+ let content = "";
7109
+ let inThinkTag = false;
7110
+ const toolCallAccumulators = /* @__PURE__ */ new Map();
7111
+ this.emit({ type: "stream_start", turn, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
7112
+ for await (const chunk of backend.chatCompletionStream(request)) {
7113
+ if (this.aborted)
7114
+ break;
7115
+ if (chunk.type === "content" && chunk.content) {
7116
+ content += chunk.content;
7117
+ let kind = inThinkTag ? "thinking" : "content";
7118
+ const fragment = chunk.content;
7119
+ if (fragment.includes("<think>")) {
7120
+ inThinkTag = true;
7121
+ kind = "thinking";
7122
+ }
7123
+ if (fragment.includes("</think>")) {
7124
+ inThinkTag = false;
7125
+ kind = "content";
7126
+ }
7127
+ this.emit({
7128
+ type: "stream_token",
7129
+ content: fragment,
7130
+ streamKind: kind,
7131
+ turn,
7132
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
7133
+ });
7134
+ }
7135
+ if (chunk.type === "tool_call_delta") {
7136
+ const idx = chunk.toolCallIndex ?? 0;
7137
+ if (!toolCallAccumulators.has(idx)) {
7138
+ toolCallAccumulators.set(idx, {
7139
+ id: chunk.toolCallId ?? crypto.randomUUID(),
7140
+ name: chunk.toolCallName ?? "",
7141
+ args: ""
7142
+ });
7143
+ }
7144
+ const acc = toolCallAccumulators.get(idx);
7145
+ if (chunk.toolCallName)
7146
+ acc.name = chunk.toolCallName;
7147
+ if (chunk.toolCallId)
7148
+ acc.id = chunk.toolCallId;
7149
+ if (chunk.toolCallArgs) {
7150
+ acc.args += chunk.toolCallArgs;
7151
+ this.emit({
7152
+ type: "stream_token",
7153
+ content: chunk.toolCallArgs,
7154
+ streamKind: "tool_args",
7155
+ turn,
7156
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
7157
+ });
7158
+ }
7159
+ }
7160
+ }
7161
+ this.emit({ type: "stream_end", content, turn, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
7162
+ const cleanContent = content.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
7163
+ const toolCalls = toolCallAccumulators.size > 0 ? Array.from(toolCallAccumulators.values()).map((tc) => {
7164
+ let args;
7165
+ try {
7166
+ args = JSON.parse(tc.args);
7167
+ } catch {
7168
+ args = { _raw: tc.args };
7169
+ }
7170
+ return { id: tc.id, name: tc.name, arguments: args };
7171
+ }) : void 0;
7172
+ return {
7173
+ choices: [{ message: { content: cleanContent || null, toolCalls } }],
7174
+ usage: void 0
7175
+ // SSE responses typically don't include usage in chunks
7176
+ };
7177
+ }
6919
7178
  };
6920
7179
  OllamaAgenticBackend = class {
6921
7180
  baseUrl;
@@ -6975,6 +7234,79 @@ ${summary}
6975
7234
  usage: usage ? { totalTokens: usage.total_tokens ?? 0 } : void 0
6976
7235
  };
6977
7236
  }
7237
+ /**
7238
+ * SSE streaming variant — yields StreamChunks as tokens arrive.
7239
+ * Uses `stream: true` and `think: true` so thinking tokens are visible.
7240
+ * The existing chatCompletion() method is completely unmodified.
7241
+ */
7242
+ async *chatCompletionStream(request) {
7243
+ const body = {
7244
+ model: this.model,
7245
+ messages: request.messages,
7246
+ tools: request.tools,
7247
+ temperature: request.temperature,
7248
+ max_tokens: request.maxTokens,
7249
+ stream: true,
7250
+ think: true
7251
+ };
7252
+ const resp = await fetch(`${this.baseUrl}/v1/chat/completions`, {
7253
+ method: "POST",
7254
+ headers: { "Content-Type": "application/json" },
7255
+ body: JSON.stringify(body),
7256
+ signal: AbortSignal.timeout(request.timeoutMs)
7257
+ });
7258
+ if (!resp.ok) {
7259
+ const text = await resp.text().catch(() => "");
7260
+ const isHtml = text.trimStart().startsWith("<!") || text.trimStart().startsWith("<html");
7261
+ const detail = isHtml ? `(received HTML error page \u2014 backend may be behind a proxy/CDN that is timing out)` : text.slice(0, 200);
7262
+ throw new Error(`Backend HTTP ${resp.status}: ${detail}`);
7263
+ }
7264
+ let sseBuffer = "";
7265
+ const decoder = new TextDecoder();
7266
+ for await (const rawChunk of resp.body) {
7267
+ sseBuffer += decoder.decode(rawChunk, { stream: true });
7268
+ const parts = sseBuffer.split("\n\n");
7269
+ sseBuffer = parts.pop();
7270
+ for (const part of parts) {
7271
+ const line = part.trim();
7272
+ if (!line)
7273
+ continue;
7274
+ if (line === "data: [DONE]")
7275
+ return;
7276
+ if (!line.startsWith("data: "))
7277
+ continue;
7278
+ try {
7279
+ const data = JSON.parse(line.slice(6));
7280
+ const choices = data.choices ?? [];
7281
+ const choice = choices[0];
7282
+ if (!choice)
7283
+ continue;
7284
+ const delta = choice.delta;
7285
+ const finishReason = choice.finish_reason;
7286
+ if (delta?.content) {
7287
+ yield { type: "content", content: delta.content };
7288
+ }
7289
+ const tcDeltas = delta?.tool_calls;
7290
+ if (tcDeltas) {
7291
+ for (const tcd of tcDeltas) {
7292
+ const fn = tcd.function;
7293
+ yield {
7294
+ type: "tool_call_delta",
7295
+ toolCallIndex: tcd.index ?? 0,
7296
+ toolCallId: tcd.id || void 0,
7297
+ toolCallName: fn?.name || void 0,
7298
+ toolCallArgs: fn?.arguments || void 0
7299
+ };
7300
+ }
7301
+ }
7302
+ if (finishReason) {
7303
+ yield { type: "finish", finishReason };
7304
+ }
7305
+ } catch {
7306
+ }
7307
+ }
7308
+ }
7309
+ }
6978
7310
  };
6979
7311
  }
6980
7312
  });
@@ -7232,6 +7564,7 @@ function renderSlashHelp() {
7232
7564
  ["/update", "Check for updates and auto-install"],
7233
7565
  ["/voice", "Toggle TTS voice feedback (GLaDOS)"],
7234
7566
  ["/voice <model>", "Set voice: glados, overwatch"],
7567
+ ["/stream", "Toggle real-time token streaming (pastel syntax highlighting)"],
7235
7568
  ["/verbose", "Toggle verbose mode"],
7236
7569
  ["/clear", "Clear the screen"],
7237
7570
  ["/help", "Show this help"],
@@ -7509,6 +7842,7 @@ var init_render = __esm({
7509
7842
  "/config",
7510
7843
  "/update",
7511
7844
  "/voice",
7845
+ "/stream",
7512
7846
  "/verbose",
7513
7847
  "/clear",
7514
7848
  "/help",
@@ -7595,6 +7929,13 @@ async function handleSlashCommand(input, ctx) {
7595
7929
  }
7596
7930
  return "handled";
7597
7931
  }
7932
+ case "stream": {
7933
+ const isOn = ctx.streamToggle();
7934
+ const save = hasLocal ? ctx.saveLocalSettings.bind(ctx) : ctx.saveSettings.bind(ctx);
7935
+ save({ stream: isOn });
7936
+ renderInfo(`Token streaming: ${isOn ? "on" : "off"}${hasLocal ? " (project-local)" : ""}` + (isOn ? " \u2014 thinking tokens in grey italics, responses with pastel syntax highlighting" : ""));
7937
+ return "handled";
7938
+ }
7598
7939
  default:
7599
7940
  renderWarning(`Unknown command: /${cmd}. Type /help for available commands.`);
7600
7941
  return "handled";
@@ -8647,14 +8988,62 @@ function getEnvironment(repoRoot) {
8647
8988
  ];
8648
8989
  return lines.join("\n");
8649
8990
  }
8650
- function buildProjectContext(repoRoot) {
8991
+ function loadTaskMemories(repoRoot, store) {
8992
+ try {
8993
+ let tasks = store.listByRepo(repoRoot);
8994
+ if (tasks.length === 0) {
8995
+ tasks = store.recent(10);
8996
+ }
8997
+ if (tasks.length === 0)
8998
+ return "";
8999
+ const lines = ["Recent agent tasks (cross-session memory):"];
9000
+ for (const t of tasks.slice(0, 10)) {
9001
+ const date = t.createdAt.split("T")[0];
9002
+ const files = t.filesTouched.slice(0, 5).join(", ");
9003
+ lines.push(`- [${date}] ${t.goal.slice(0, 100)} \u2192 ${t.outcome}${files ? ` (files: ${files})` : ""}`);
9004
+ if (t.notes) {
9005
+ lines.push(` Notes: ${t.notes.slice(0, 150)}`);
9006
+ }
9007
+ }
9008
+ return lines.join("\n");
9009
+ } catch {
9010
+ return "";
9011
+ }
9012
+ }
9013
+ function loadFailurePatterns(store) {
9014
+ try {
9015
+ const unresolved = store.listUnresolved();
9016
+ if (unresolved.length === 0)
9017
+ return "";
9018
+ const seen = /* @__PURE__ */ new Set();
9019
+ const unique = unresolved.filter((f) => {
9020
+ if (seen.has(f.fingerprint))
9021
+ return false;
9022
+ seen.add(f.fingerprint);
9023
+ return true;
9024
+ });
9025
+ if (unique.length === 0)
9026
+ return "";
9027
+ const lines = ["Known failure patterns (avoid repeating these):"];
9028
+ for (const f of unique.slice(0, 8)) {
9029
+ const file = f.filePath ? ` in ${f.filePath}` : "";
9030
+ lines.push(`- [${f.failureType}]${file}: ${f.errorMessage.slice(0, 150)}`);
9031
+ }
9032
+ return lines.join("\n");
9033
+ } catch {
9034
+ return "";
9035
+ }
9036
+ }
9037
+ function buildProjectContext(repoRoot, stores) {
8651
9038
  return {
8652
9039
  projectInstructions: loadProjectFiles(repoRoot),
8653
9040
  projectMap: loadProjectMap(repoRoot),
8654
9041
  gitInfo: getGitInfo(repoRoot),
8655
9042
  memoryContext: loadMemoryContext(repoRoot),
8656
9043
  sessionHistory: loadSessionHistory(repoRoot),
8657
- environment: getEnvironment(repoRoot)
9044
+ environment: getEnvironment(repoRoot),
9045
+ taskMemories: stores?.taskMemoryStore ? loadTaskMemories(repoRoot, stores.taskMemoryStore) : "",
9046
+ failurePatterns: stores?.failureStore ? loadFailurePatterns(stores.failureStore) : ""
8658
9047
  };
8659
9048
  }
8660
9049
  function formatContextForPrompt(ctx) {
@@ -8688,6 +9077,20 @@ Use this context to avoid re-learning known patterns. Update with memory_write i
8688
9077
  sections.push(`## Session History
8689
9078
 
8690
9079
  ${ctx.sessionHistory}`);
9080
+ }
9081
+ if (ctx.taskMemories) {
9082
+ sections.push(`## Cross-Session Task Memory
9083
+
9084
+ ${ctx.taskMemories}
9085
+
9086
+ Use this history to avoid re-doing completed work and to learn from past approaches.`);
9087
+ }
9088
+ if (ctx.failurePatterns) {
9089
+ sections.push(`## Known Failure Patterns
9090
+
9091
+ ${ctx.failurePatterns}
9092
+
9093
+ Avoid approaches that led to these failures. If you encounter these errors, try a different strategy.`);
8691
9094
  }
8692
9095
  return sections.join("\n\n");
8693
9096
  }
@@ -8698,6 +9101,354 @@ var init_project_context = __esm({
8698
9101
  }
8699
9102
  });
8700
9103
 
9104
+ // packages/memory/dist/db.js
9105
+ import Database from "better-sqlite3";
9106
+ function initDb(dbPath) {
9107
+ const db = new Database(dbPath);
9108
+ db.pragma("journal_mode = WAL");
9109
+ db.pragma("foreign_keys = ON");
9110
+ runMigrations(db);
9111
+ return db;
9112
+ }
9113
+ function closeDb(db) {
9114
+ db.close();
9115
+ }
9116
+ function runMigrations(db) {
9117
+ db.exec(`
9118
+ -- repo_profiles: one row per repository root.
9119
+ CREATE TABLE IF NOT EXISTS repo_profiles (
9120
+ repo_root TEXT PRIMARY KEY,
9121
+ languages TEXT NOT NULL DEFAULT '[]', -- JSON array
9122
+ frameworks TEXT NOT NULL DEFAULT '[]', -- JSON array
9123
+ build_system TEXT NOT NULL DEFAULT '',
9124
+ test_commands TEXT NOT NULL DEFAULT '[]', -- JSON array
9125
+ lint_commands TEXT NOT NULL DEFAULT '[]', -- JSON array
9126
+ package_manager TEXT NOT NULL DEFAULT '',
9127
+ notes TEXT,
9128
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
9129
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
9130
+ );
9131
+
9132
+ -- file_summaries: one row per (repo_root, file_path) pair.
9133
+ CREATE TABLE IF NOT EXISTS file_summaries (
9134
+ file_path TEXT NOT NULL,
9135
+ repo_root TEXT NOT NULL,
9136
+ purpose TEXT NOT NULL DEFAULT '',
9137
+ exports TEXT NOT NULL DEFAULT '[]', -- JSON array
9138
+ imports TEXT NOT NULL DEFAULT '[]', -- JSON array
9139
+ domain TEXT NOT NULL DEFAULT '',
9140
+ risk_level TEXT NOT NULL DEFAULT 'low', -- low | medium | high
9141
+ related_tests TEXT NOT NULL DEFAULT '[]', -- JSON array
9142
+ notes TEXT,
9143
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
9144
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
9145
+ PRIMARY KEY (file_path)
9146
+ );
9147
+
9148
+ CREATE INDEX IF NOT EXISTS idx_file_summaries_repo
9149
+ ON file_summaries (repo_root);
9150
+
9151
+ CREATE INDEX IF NOT EXISTS idx_file_summaries_domain
9152
+ ON file_summaries (domain);
9153
+
9154
+ CREATE INDEX IF NOT EXISTS idx_file_summaries_risk
9155
+ ON file_summaries (risk_level);
9156
+
9157
+ -- task_memory: historical record of agent tasks.
9158
+ CREATE TABLE IF NOT EXISTS task_memory (
9159
+ id TEXT PRIMARY KEY,
9160
+ session_id TEXT NOT NULL,
9161
+ repo_root TEXT NOT NULL,
9162
+ goal TEXT NOT NULL DEFAULT '',
9163
+ constraints TEXT NOT NULL DEFAULT '[]', -- JSON array
9164
+ files_touched TEXT NOT NULL DEFAULT '[]', -- JSON array
9165
+ patches TEXT NOT NULL DEFAULT '[]', -- JSON array
9166
+ outcome TEXT NOT NULL DEFAULT 'unknown', -- success | failure | partial | unknown
9167
+ quality_score REAL,
9168
+ notes TEXT,
9169
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
9170
+ );
9171
+
9172
+ CREATE INDEX IF NOT EXISTS idx_task_memory_session
9173
+ ON task_memory (session_id);
9174
+
9175
+ CREATE INDEX IF NOT EXISTS idx_task_memory_repo
9176
+ ON task_memory (repo_root);
9177
+
9178
+ CREATE INDEX IF NOT EXISTS idx_task_memory_outcome
9179
+ ON task_memory (outcome);
9180
+
9181
+ -- patch_history: individual file diffs applied by the agent.
9182
+ CREATE TABLE IF NOT EXISTS patch_history (
9183
+ id TEXT PRIMARY KEY,
9184
+ task_id TEXT NOT NULL,
9185
+ session_id TEXT NOT NULL,
9186
+ repo_root TEXT NOT NULL,
9187
+ file_path TEXT NOT NULL,
9188
+ diff TEXT NOT NULL DEFAULT '',
9189
+ status TEXT NOT NULL DEFAULT 'applied', -- applied | reverted | pending | failed
9190
+ applied_at TEXT,
9191
+ reverted_at TEXT,
9192
+ notes TEXT,
9193
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
9194
+ );
9195
+
9196
+ CREATE INDEX IF NOT EXISTS idx_patch_history_task
9197
+ ON patch_history (task_id);
9198
+
9199
+ CREATE INDEX IF NOT EXISTS idx_patch_history_file
9200
+ ON patch_history (file_path);
9201
+
9202
+ CREATE INDEX IF NOT EXISTS idx_patch_history_status
9203
+ ON patch_history (status);
9204
+
9205
+ -- failures: fingerprinted failure events for pattern detection.
9206
+ CREATE TABLE IF NOT EXISTS failures (
9207
+ id TEXT PRIMARY KEY,
9208
+ task_id TEXT NOT NULL,
9209
+ session_id TEXT NOT NULL,
9210
+ repo_root TEXT NOT NULL,
9211
+ failure_type TEXT NOT NULL DEFAULT '',
9212
+ fingerprint TEXT NOT NULL DEFAULT '',
9213
+ file_path TEXT,
9214
+ error_message TEXT NOT NULL DEFAULT '',
9215
+ context TEXT, -- JSON object or null
9216
+ resolved INTEGER NOT NULL DEFAULT 0, -- 0 = false, 1 = true
9217
+ resolved_at TEXT,
9218
+ notes TEXT,
9219
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
9220
+ );
9221
+
9222
+ CREATE INDEX IF NOT EXISTS idx_failures_type
9223
+ ON failures (failure_type);
9224
+
9225
+ CREATE INDEX IF NOT EXISTS idx_failures_fingerprint
9226
+ ON failures (fingerprint);
9227
+
9228
+ CREATE INDEX IF NOT EXISTS idx_failures_resolved
9229
+ ON failures (resolved);
9230
+
9231
+ -- validation_runs: results of lint / test / build / typecheck runs.
9232
+ CREATE TABLE IF NOT EXISTS validation_runs (
9233
+ id TEXT PRIMARY KEY,
9234
+ task_id TEXT NOT NULL,
9235
+ session_id TEXT NOT NULL,
9236
+ repo_root TEXT NOT NULL,
9237
+ run_type TEXT NOT NULL DEFAULT '', -- test | lint | build | typecheck
9238
+ command TEXT NOT NULL DEFAULT '',
9239
+ passed INTEGER NOT NULL DEFAULT 0, -- 0 = false, 1 = true
9240
+ duration_ms INTEGER,
9241
+ output TEXT,
9242
+ error_output TEXT,
9243
+ coverage_percent REAL,
9244
+ notes TEXT,
9245
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
9246
+ );
9247
+
9248
+ CREATE INDEX IF NOT EXISTS idx_validation_runs_task
9249
+ ON validation_runs (task_id);
9250
+
9251
+ CREATE INDEX IF NOT EXISTS idx_validation_runs_run_type
9252
+ ON validation_runs (run_type);
9253
+
9254
+ CREATE INDEX IF NOT EXISTS idx_validation_runs_passed
9255
+ ON validation_runs (passed);
9256
+ `);
9257
+ }
9258
+ var init_db = __esm({
9259
+ "packages/memory/dist/db.js"() {
9260
+ "use strict";
9261
+ }
9262
+ });
9263
+
9264
+ // packages/memory/dist/repoProfileStore.js
9265
+ var init_repoProfileStore = __esm({
9266
+ "packages/memory/dist/repoProfileStore.js"() {
9267
+ "use strict";
9268
+ }
9269
+ });
9270
+
9271
+ // packages/memory/dist/fileSummaryStore.js
9272
+ var init_fileSummaryStore = __esm({
9273
+ "packages/memory/dist/fileSummaryStore.js"() {
9274
+ "use strict";
9275
+ }
9276
+ });
9277
+
9278
+ // packages/memory/dist/taskMemoryStore.js
9279
+ import { randomUUID } from "node:crypto";
9280
+ function rowToTask(row) {
9281
+ return {
9282
+ id: row.id,
9283
+ sessionId: row.session_id,
9284
+ repoRoot: row.repo_root,
9285
+ goal: row.goal,
9286
+ constraints: JSON.parse(row.constraints),
9287
+ filesTouched: JSON.parse(row.files_touched),
9288
+ patches: JSON.parse(row.patches),
9289
+ outcome: row.outcome,
9290
+ qualityScore: row.quality_score ?? null,
9291
+ notes: row.notes ?? null,
9292
+ createdAt: row.created_at
9293
+ };
9294
+ }
9295
+ var TaskMemoryStore;
9296
+ var init_taskMemoryStore = __esm({
9297
+ "packages/memory/dist/taskMemoryStore.js"() {
9298
+ "use strict";
9299
+ TaskMemoryStore = class {
9300
+ db;
9301
+ constructor(db) {
9302
+ this.db = db;
9303
+ }
9304
+ /**
9305
+ * Insert a new task record and return its auto-generated id.
9306
+ */
9307
+ insert(input) {
9308
+ const id = randomUUID();
9309
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9310
+ this.db.prepare(`INSERT INTO task_memory
9311
+ (id, session_id, repo_root, goal, constraints,
9312
+ files_touched, patches, outcome, quality_score, notes, created_at)
9313
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, input.sessionId, input.repoRoot, input.goal, JSON.stringify(input.constraints), JSON.stringify(input.filesTouched), JSON.stringify(input.patches), input.outcome, input.qualityScore ?? null, input.notes ?? null, now);
9314
+ return id;
9315
+ }
9316
+ /** Retrieve a task record by id, or `null` if not found. */
9317
+ getById(id) {
9318
+ const row = this.db.prepare("SELECT * FROM task_memory WHERE id = ?").get(id);
9319
+ return row ? rowToTask(row) : null;
9320
+ }
9321
+ /** List all tasks associated with a session. */
9322
+ listBySession(sessionId) {
9323
+ const rows = this.db.prepare("SELECT * FROM task_memory WHERE session_id = ? ORDER BY created_at DESC").all(sessionId);
9324
+ return rows.map(rowToTask);
9325
+ }
9326
+ /** List all tasks for a repository root. */
9327
+ listByRepo(repoRoot) {
9328
+ const rows = this.db.prepare("SELECT * FROM task_memory WHERE repo_root = ? ORDER BY created_at DESC").all(repoRoot);
9329
+ return rows.map(rowToTask);
9330
+ }
9331
+ /** List tasks filtered by outcome. */
9332
+ listByOutcome(outcome) {
9333
+ const rows = this.db.prepare("SELECT * FROM task_memory WHERE outcome = ? ORDER BY created_at DESC").all(outcome);
9334
+ return rows.map(rowToTask);
9335
+ }
9336
+ /**
9337
+ * Return the N most recently created tasks across all sessions and repos.
9338
+ * Useful for building a rolling context window.
9339
+ */
9340
+ recent(limit) {
9341
+ const rows = this.db.prepare("SELECT * FROM task_memory ORDER BY created_at DESC LIMIT ?").all(limit);
9342
+ return rows.map(rowToTask);
9343
+ }
9344
+ };
9345
+ }
9346
+ });
9347
+
9348
+ // packages/memory/dist/patchHistoryStore.js
9349
+ var init_patchHistoryStore = __esm({
9350
+ "packages/memory/dist/patchHistoryStore.js"() {
9351
+ "use strict";
9352
+ }
9353
+ });
9354
+
9355
+ // packages/memory/dist/failureStore.js
9356
+ import { randomUUID as randomUUID2 } from "node:crypto";
9357
+ function rowToFailure(row) {
9358
+ return {
9359
+ id: row.id,
9360
+ taskId: row.task_id,
9361
+ sessionId: row.session_id,
9362
+ repoRoot: row.repo_root,
9363
+ failureType: row.failure_type,
9364
+ fingerprint: row.fingerprint,
9365
+ filePath: row.file_path ?? null,
9366
+ errorMessage: row.error_message,
9367
+ context: row.context !== null ? JSON.parse(row.context) : null,
9368
+ resolved: row.resolved === 1,
9369
+ resolvedAt: row.resolved_at ?? null,
9370
+ notes: row.notes ?? null,
9371
+ createdAt: row.created_at
9372
+ };
9373
+ }
9374
+ var FailureStore;
9375
+ var init_failureStore = __esm({
9376
+ "packages/memory/dist/failureStore.js"() {
9377
+ "use strict";
9378
+ FailureStore = class {
9379
+ db;
9380
+ constructor(db) {
9381
+ this.db = db;
9382
+ }
9383
+ /**
9384
+ * Record a new failure event and return its auto-generated id.
9385
+ */
9386
+ insert(input) {
9387
+ const id = randomUUID2();
9388
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9389
+ this.db.prepare(`INSERT INTO failures
9390
+ (id, task_id, session_id, repo_root, failure_type,
9391
+ fingerprint, file_path, error_message, context,
9392
+ resolved, resolved_at, notes, created_at)
9393
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, input.taskId, input.sessionId, input.repoRoot, input.failureType, input.fingerprint, input.filePath ?? null, input.errorMessage, input.context !== null ? JSON.stringify(input.context) : null, input.resolved ? 1 : 0, input.resolvedAt ?? null, input.notes ?? null, now);
9394
+ return id;
9395
+ }
9396
+ /** Retrieve a failure by id, or `null` if not found. */
9397
+ getById(id) {
9398
+ const row = this.db.prepare("SELECT * FROM failures WHERE id = ?").get(id);
9399
+ return row ? rowToFailure(row) : null;
9400
+ }
9401
+ /** List failures filtered by type. */
9402
+ listByType(failureType) {
9403
+ const rows = this.db.prepare("SELECT * FROM failures WHERE failure_type = ? ORDER BY created_at DESC").all(failureType);
9404
+ return rows.map(rowToFailure);
9405
+ }
9406
+ /** List all failures sharing the same content fingerprint. */
9407
+ listByFingerprint(fingerprint) {
9408
+ const rows = this.db.prepare("SELECT * FROM failures WHERE fingerprint = ? ORDER BY created_at DESC").all(fingerprint);
9409
+ return rows.map(rowToFailure);
9410
+ }
9411
+ /** List all failures that have not yet been marked as resolved. */
9412
+ listUnresolved() {
9413
+ const rows = this.db.prepare("SELECT * FROM failures WHERE resolved = 0 ORDER BY created_at DESC").all();
9414
+ return rows.map(rowToFailure);
9415
+ }
9416
+ /**
9417
+ * Mark a failure as resolved.
9418
+ *
9419
+ * @param id - The failure record id.
9420
+ * @param resolvedAt - ISO timestamp string of resolution time.
9421
+ */
9422
+ markResolved(id, resolvedAt) {
9423
+ this.db.prepare(`UPDATE failures
9424
+ SET resolved = 1, resolved_at = ?
9425
+ WHERE id = ?`).run(resolvedAt, id);
9426
+ }
9427
+ };
9428
+ }
9429
+ });
9430
+
9431
+ // packages/memory/dist/validationStore.js
9432
+ var init_validationStore = __esm({
9433
+ "packages/memory/dist/validationStore.js"() {
9434
+ "use strict";
9435
+ }
9436
+ });
9437
+
9438
+ // packages/memory/dist/index.js
9439
+ var init_dist6 = __esm({
9440
+ "packages/memory/dist/index.js"() {
9441
+ "use strict";
9442
+ init_db();
9443
+ init_repoProfileStore();
9444
+ init_fileSummaryStore();
9445
+ init_taskMemoryStore();
9446
+ init_patchHistoryStore();
9447
+ init_failureStore();
9448
+ init_validationStore();
9449
+ }
9450
+ });
9451
+
8701
9452
  // packages/cli/dist/tui/carousel.js
8702
9453
  function fg(code, text) {
8703
9454
  return isTTY3 ? `\x1B[38;5;${code}m${text}\x1B[0m` : text;
@@ -9502,6 +10253,266 @@ Error: ${err instanceof Error ? err.message : String(err)}`);
9502
10253
  }
9503
10254
  });
9504
10255
 
10256
+ // packages/cli/dist/tui/stream-renderer.js
10257
+ function fg256(code, text) {
10258
+ return isTTY4 ? `\x1B[38;5;${code}m${text}\x1B[0m` : text;
10259
+ }
10260
+ function dimText(text) {
10261
+ return isTTY4 ? `\x1B[2m${text}\x1B[0m` : text;
10262
+ }
10263
+ function dimItalic(text) {
10264
+ return isTTY4 ? `\x1B[2;3m${text}\x1B[0m` : text;
10265
+ }
10266
+ var isTTY4, PASTEL, StreamRenderer;
10267
+ var init_stream_renderer = __esm({
10268
+ "packages/cli/dist/tui/stream-renderer.js"() {
10269
+ "use strict";
10270
+ isTTY4 = process.stdout.isTTY ?? false;
10271
+ PASTEL = {
10272
+ key: 222,
10273
+ // light gold — JSON keys
10274
+ string: 183,
10275
+ // light lavender — "string values"
10276
+ number: 156,
10277
+ // soft green — 42, 3.14
10278
+ boolean: 114,
10279
+ // mint green — true, false
10280
+ null: 109,
10281
+ // grey-blue — null
10282
+ bracket: 75,
10283
+ // soft blue — { } [ ]
10284
+ colon: 245,
10285
+ // neutral grey — : ,
10286
+ keyword: 117,
10287
+ // sky blue — function, return, if, else
10288
+ comment: 243,
10289
+ // dim grey — // comments
10290
+ thinking: 245,
10291
+ // neutral grey for thinking tokens
10292
+ toolArg: 111
10293
+ // dim periwinkle for tool arg tokens
10294
+ };
10295
+ StreamRenderer = class {
10296
+ lineBuffer = "";
10297
+ inThinkBlock = false;
10298
+ inCodeBlock = false;
10299
+ codeLang = "";
10300
+ lineStarted = false;
10301
+ flushTimer = null;
10302
+ enabled = false;
10303
+ tokenCount = 0;
10304
+ startTime = 0;
10305
+ /** Track if we're mid-tool-arg display */
10306
+ inToolArgs = false;
10307
+ /** Called when a new model response starts streaming */
10308
+ onStreamStart() {
10309
+ this.lineBuffer = "";
10310
+ this.inThinkBlock = false;
10311
+ this.inCodeBlock = false;
10312
+ this.codeLang = "";
10313
+ this.lineStarted = false;
10314
+ this.inToolArgs = false;
10315
+ this.enabled = true;
10316
+ this.tokenCount = 0;
10317
+ this.startTime = Date.now();
10318
+ this.cancelFlush();
10319
+ }
10320
+ /**
10321
+ * Feed a streamed token into the renderer.
10322
+ * Tokens are buffered per-line and flushed with syntax highlighting.
10323
+ */
10324
+ write(token, kind) {
10325
+ if (!this.enabled)
10326
+ return;
10327
+ this.tokenCount++;
10328
+ if (kind === "tool_args" && !this.inToolArgs) {
10329
+ this.flushPartial(kind);
10330
+ this.inToolArgs = true;
10331
+ } else if (kind !== "tool_args" && this.inToolArgs) {
10332
+ this.flushPartial(kind);
10333
+ this.inToolArgs = false;
10334
+ }
10335
+ for (const char of token) {
10336
+ this.lineBuffer += char;
10337
+ if (char === "\n") {
10338
+ this.flushLine(kind);
10339
+ }
10340
+ }
10341
+ this.scheduleFlush(kind);
10342
+ }
10343
+ /** Called when streaming ends for this response */
10344
+ onStreamEnd() {
10345
+ if (!this.enabled)
10346
+ return;
10347
+ this.cancelFlush();
10348
+ if (this.lineBuffer.length > 0) {
10349
+ const kind = this.inThinkBlock ? "thinking" : this.inToolArgs ? "tool_args" : "content";
10350
+ this.writeHighlighted(this.lineBuffer, kind);
10351
+ this.lineBuffer = "";
10352
+ }
10353
+ if (this.lineStarted) {
10354
+ process.stdout.write("\n");
10355
+ this.lineStarted = false;
10356
+ }
10357
+ this.enabled = false;
10358
+ }
10359
+ /** Get streaming stats */
10360
+ getStats() {
10361
+ return {
10362
+ tokens: this.tokenCount,
10363
+ durationMs: Date.now() - this.startTime
10364
+ };
10365
+ }
10366
+ // -------------------------------------------------------------------------
10367
+ // Internal rendering
10368
+ // -------------------------------------------------------------------------
10369
+ /**
10370
+ * Flush a complete line (ending with \n) with full syntax highlighting.
10371
+ */
10372
+ flushLine(kind) {
10373
+ const line = this.lineBuffer;
10374
+ this.lineBuffer = "";
10375
+ if (!line || line === "\n") {
10376
+ if (this.lineStarted) {
10377
+ process.stdout.write("\n");
10378
+ this.lineStarted = false;
10379
+ } else {
10380
+ process.stdout.write("\n");
10381
+ }
10382
+ return;
10383
+ }
10384
+ if (line.includes("<think>")) {
10385
+ this.inThinkBlock = true;
10386
+ const after = line.replace(/<think>/g, "");
10387
+ if (after.trim()) {
10388
+ this.writeHighlighted(after, "thinking");
10389
+ }
10390
+ return;
10391
+ }
10392
+ if (line.includes("</think>")) {
10393
+ this.inThinkBlock = false;
10394
+ const after = line.replace(/<\/think>/g, "");
10395
+ if (after.trim()) {
10396
+ this.writeHighlighted(after, "content");
10397
+ }
10398
+ return;
10399
+ }
10400
+ const trimmedLine = line.replace(/\n$/, "");
10401
+ if (trimmedLine.trimStart().startsWith("```")) {
10402
+ if (this.inCodeBlock) {
10403
+ this.writeRaw(dimText(" \u23BF ") + dimText("```") + "\n");
10404
+ this.inCodeBlock = false;
10405
+ this.codeLang = "";
10406
+ this.lineStarted = false;
10407
+ } else {
10408
+ this.codeLang = trimmedLine.replace(/```/g, "").trim();
10409
+ this.writeRaw(dimText(" \u23BF ") + dimText("```" + this.codeLang) + "\n");
10410
+ this.inCodeBlock = true;
10411
+ this.lineStarted = false;
10412
+ }
10413
+ return;
10414
+ }
10415
+ const effectiveKind = this.inThinkBlock ? "thinking" : kind;
10416
+ this.writeHighlighted(line, effectiveKind);
10417
+ }
10418
+ /**
10419
+ * Write a highlighted line/fragment to stdout.
10420
+ */
10421
+ writeHighlighted(text, kind) {
10422
+ const raw = text.replace(/\n$/, "");
10423
+ if (!raw)
10424
+ return;
10425
+ const prefix = this.lineStarted ? "" : " \u23BF ";
10426
+ let rendered;
10427
+ switch (kind) {
10428
+ case "thinking":
10429
+ rendered = dimItalic(raw);
10430
+ break;
10431
+ case "tool_args":
10432
+ rendered = this.highlightJson(raw, true);
10433
+ break;
10434
+ case "content":
10435
+ if (this.inCodeBlock) {
10436
+ rendered = this.highlightCode(raw);
10437
+ } else if (this.looksLikeJson(raw)) {
10438
+ rendered = this.highlightJson(raw, false);
10439
+ } else {
10440
+ rendered = raw;
10441
+ }
10442
+ break;
10443
+ }
10444
+ const hasNewline = text.endsWith("\n");
10445
+ this.writeRaw(dimText(prefix) + rendered + (hasNewline ? "\n" : ""));
10446
+ this.lineStarted = !hasNewline;
10447
+ }
10448
+ /** Write raw ANSI text to stdout */
10449
+ writeRaw(text) {
10450
+ process.stdout.write(text);
10451
+ }
10452
+ /** Flush partial buffer (non-newline-terminated tokens) */
10453
+ flushPartial(kind) {
10454
+ if (this.lineBuffer.length === 0)
10455
+ return;
10456
+ const effectiveKind = this.inThinkBlock ? "thinking" : kind;
10457
+ this.writeHighlighted(this.lineBuffer, effectiveKind);
10458
+ this.lineBuffer = "";
10459
+ }
10460
+ /** Schedule a timer to flush partial buffer (for streaming smoothness) */
10461
+ scheduleFlush(kind) {
10462
+ this.cancelFlush();
10463
+ this.flushTimer = setTimeout(() => {
10464
+ if (this.lineBuffer.length > 0) {
10465
+ this.flushPartial(kind);
10466
+ }
10467
+ }, 80);
10468
+ }
10469
+ cancelFlush() {
10470
+ if (this.flushTimer) {
10471
+ clearTimeout(this.flushTimer);
10472
+ this.flushTimer = null;
10473
+ }
10474
+ }
10475
+ // -------------------------------------------------------------------------
10476
+ // Syntax highlighting — pastel palette
10477
+ // -------------------------------------------------------------------------
10478
+ /** Check if a string looks like JSON (starts with { [ " or has key: patterns) */
10479
+ looksLikeJson(text) {
10480
+ const trimmed = text.trimStart();
10481
+ return trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith("}") || trimmed.startsWith("]") || /^\s*"[^"]+"\s*:/.test(trimmed);
10482
+ }
10483
+ /**
10484
+ * Highlight a JSON line with pastel colors.
10485
+ * @param dim If true, apply dimmer colors (for tool args)
10486
+ */
10487
+ highlightJson(line, dim) {
10488
+ const colorKey = dim ? PASTEL.toolArg : PASTEL.key;
10489
+ const colorStr = dim ? PASTEL.toolArg : PASTEL.string;
10490
+ let result = line;
10491
+ result = result.replace(/"([^"]*)"(\s*:)/g, (_m, key, colon) => fg256(colorKey, `"${key}"`) + fg256(PASTEL.colon, colon));
10492
+ result = result.replace(/(:\s*)"([^"]*)"/g, (_m, prefix, val) => fg256(PASTEL.colon, prefix) + fg256(colorStr, `"${val}"`));
10493
+ result = result.replace(/(:\s*)(\d+\.?\d*)/g, (_m, prefix, num) => fg256(PASTEL.colon, prefix) + fg256(PASTEL.number, num));
10494
+ result = result.replace(/(:\s*)(true|false)/g, (_m, prefix, bool) => fg256(PASTEL.colon, prefix) + fg256(PASTEL.boolean, bool));
10495
+ result = result.replace(/(:\s*)(null)/g, (_m, prefix, n) => fg256(PASTEL.colon, prefix) + fg256(PASTEL.null, n));
10496
+ result = result.replace(/([{}[\]])/g, (_m, b) => fg256(PASTEL.bracket, b));
10497
+ return dim ? dimText(result) : result;
10498
+ }
10499
+ /**
10500
+ * Highlight a code line with basic pastel syntax coloring.
10501
+ */
10502
+ highlightCode(line) {
10503
+ let result = line;
10504
+ result = result.replace(/"([^"]*)"/g, (_m, s) => fg256(PASTEL.string, `"${s}"`));
10505
+ result = result.replace(/'([^']*)'/g, (_m, s) => fg256(PASTEL.string, `'${s}'`));
10506
+ result = result.replace(/\b(\d+\.?\d*)\b/g, (_m, n) => fg256(PASTEL.number, n));
10507
+ result = result.replace(/\b(true|false|null|undefined|None|True|False)\b/g, (_m, kw) => fg256(PASTEL.boolean, kw));
10508
+ result = result.replace(/\b(function|const|let|var|return|if|else|for|while|import|export|from|class|async|await|def|self|try|catch|finally|throw|new|typeof|instanceof)\b/g, (_m, kw) => fg256(PASTEL.keyword, kw));
10509
+ result = result.replace(/(\/\/.*$|#.*$)/gm, (_m, c3) => fg256(PASTEL.comment, c3));
10510
+ return result;
10511
+ }
10512
+ };
10513
+ }
10514
+ });
10515
+
9505
10516
  // packages/cli/dist/tui/interactive.js
9506
10517
  import * as readline2 from "node:readline";
9507
10518
  import { cwd } from "node:process";
@@ -9669,8 +10680,8 @@ Use task_status("${taskId}") or task_output("${taskId}") to check progress.`
9669
10680
  }
9670
10681
  };
9671
10682
  }
9672
- function startTask(task, config, repoRoot, voice) {
9673
- const projectCtx = buildProjectContext(repoRoot);
10683
+ function startTask(task, config, repoRoot, voice, stream, taskStores) {
10684
+ const projectCtx = buildProjectContext(repoRoot, taskStores?.contextStores);
9674
10685
  const dynamicContext = formatContextForPrompt(projectCtx);
9675
10686
  const backend = new OllamaAgenticBackend(config.backendUrl.replace(/\/$/, ""), config.model);
9676
10687
  const runner = new AgenticRunner(backend, {
@@ -9680,12 +10691,20 @@ function startTask(task, config, repoRoot, voice) {
9680
10691
  requestTimeoutMs: config.timeoutMs,
9681
10692
  taskTimeoutMs: config.timeoutMs * 4,
9682
10693
  compactionThreshold: 4e4,
9683
- dynamicContext
10694
+ dynamicContext,
10695
+ streamEnabled: stream?.enabled ?? false
9684
10696
  });
9685
10697
  runner.registerTools(buildTools(repoRoot, config));
10698
+ const filesTouched = /* @__PURE__ */ new Set();
9686
10699
  runner.onEvent((event) => {
9687
10700
  switch (event.type) {
9688
10701
  case "tool_call":
10702
+ if (event.toolArgs?.path && typeof event.toolArgs.path === "string") {
10703
+ const name = event.toolName ?? "";
10704
+ if (name === "file_write" || name === "file_edit" || name === "batch_edit") {
10705
+ filesTouched.add(event.toolArgs.path);
10706
+ }
10707
+ }
9689
10708
  if (voice?.enabled) {
9690
10709
  const desc = describeToolCall(event.toolName ?? "unknown", event.toolArgs ?? {});
9691
10710
  renderVoiceText(desc);
@@ -9704,10 +10723,23 @@ function startTask(task, config, repoRoot, voice) {
9704
10723
  }
9705
10724
  break;
9706
10725
  case "model_response":
9707
- if (config.verbose && event.content) {
10726
+ if (config.verbose && !stream?.enabled && event.content) {
9708
10727
  renderAssistantText(event.content);
9709
10728
  }
9710
10729
  break;
10730
+ case "stream_start":
10731
+ if (stream?.enabled)
10732
+ stream.renderer.onStreamStart();
10733
+ break;
10734
+ case "stream_token":
10735
+ if (stream?.enabled) {
10736
+ stream.renderer.write(event.content ?? "", event.streamKind ?? "content");
10737
+ }
10738
+ break;
10739
+ case "stream_end":
10740
+ if (stream?.enabled)
10741
+ stream.renderer.onStreamEnd();
10742
+ break;
9711
10743
  case "user_interrupt":
9712
10744
  break;
9713
10745
  case "compaction":
@@ -9747,6 +10779,22 @@ function startTask(task, config, repoRoot, voice) {
9747
10779
  });
9748
10780
  } catch {
9749
10781
  }
10782
+ if (taskStores?.taskMemoryStore) {
10783
+ try {
10784
+ taskStores.taskMemoryStore.insert({
10785
+ sessionId,
10786
+ repoRoot,
10787
+ goal: task.slice(0, 500),
10788
+ constraints: [],
10789
+ filesTouched: Array.from(filesTouched).slice(0, 50),
10790
+ patches: [],
10791
+ outcome: result.completed ? "success" : "partial",
10792
+ qualityScore: null,
10793
+ notes: result.summary.slice(0, 500)
10794
+ });
10795
+ } catch {
10796
+ }
10797
+ }
9750
10798
  });
9751
10799
  return { runner, promise };
9752
10800
  }
@@ -9758,6 +10806,17 @@ async function startInteractive(config, repoPath) {
9758
10806
  }
9759
10807
  initOaDirectory(repoRoot);
9760
10808
  const savedSettings = resolveSettings(repoRoot);
10809
+ let memoryDb = null;
10810
+ let taskMemoryStore = null;
10811
+ let failureStore = null;
10812
+ let contextStores;
10813
+ try {
10814
+ memoryDb = initDb(config.dbPath);
10815
+ taskMemoryStore = new TaskMemoryStore(memoryDb);
10816
+ failureStore = new FailureStore(memoryDb);
10817
+ contextStores = { taskMemoryStore, failureStore };
10818
+ } catch {
10819
+ }
9761
10820
  if (savedSettings.model)
9762
10821
  config = { ...config, model: savedSettings.model };
9763
10822
  if (savedSettings.backendUrl)
@@ -9777,6 +10836,7 @@ async function startInteractive(config, repoPath) {
9777
10836
  config = { ...config, dryRun: savedSettings.dryRun };
9778
10837
  if (savedSettings.dbPath)
9779
10838
  config = { ...config, dbPath: savedSettings.dbPath };
10839
+ let streamEnabled = savedSettings.stream ?? false;
9780
10840
  if (!isResumed) {
9781
10841
  const needsSetup = isFirstRun() || !await isModelAvailable(config);
9782
10842
  if (needsSetup && config.backendType === "ollama") {
@@ -9820,6 +10880,7 @@ async function startInteractive(config, repoPath) {
9820
10880
  `);
9821
10881
  }
9822
10882
  const voiceEngine = new VoiceEngine();
10883
+ const streamRenderer = new StreamRenderer();
9823
10884
  if (savedSettings.voice) {
9824
10885
  voiceEngine.toggle().catch(() => {
9825
10886
  });
@@ -9886,6 +10947,12 @@ async function startInteractive(config, repoPath) {
9886
10947
  if (carousel.isRunning)
9887
10948
  carousel.stop();
9888
10949
  voiceEngine.dispose();
10950
+ if (memoryDb) {
10951
+ try {
10952
+ closeDb(memoryDb);
10953
+ } catch {
10954
+ }
10955
+ }
9889
10956
  rl.close();
9890
10957
  },
9891
10958
  async voiceToggle() {
@@ -9894,6 +10961,10 @@ async function startInteractive(config, repoPath) {
9894
10961
  async voiceSetModel(id) {
9895
10962
  return voiceEngine.setModel(id);
9896
10963
  },
10964
+ streamToggle() {
10965
+ streamEnabled = !streamEnabled;
10966
+ return streamEnabled;
10967
+ },
9897
10968
  saveSettings(settings) {
9898
10969
  try {
9899
10970
  saveProjectSettings(repoRoot, settings);
@@ -9970,12 +11041,39 @@ ${c2.dim("Goodbye!")}
9970
11041
  }
9971
11042
  renderUserMessage(isImage ? `[Image: ${cleanPath}]` : fullInput);
9972
11043
  try {
9973
- const task = startTask(fullInput, currentConfig, repoRoot, voiceEngine);
11044
+ const task = startTask(fullInput, currentConfig, repoRoot, voiceEngine, {
11045
+ enabled: streamEnabled,
11046
+ renderer: streamRenderer
11047
+ }, {
11048
+ contextStores,
11049
+ taskMemoryStore: taskMemoryStore ?? void 0,
11050
+ failureStore: failureStore ?? void 0
11051
+ });
9974
11052
  activeTask = task;
9975
11053
  showPrompt();
9976
11054
  await task.promise;
9977
11055
  } catch (err) {
9978
- renderError(err instanceof Error ? err.message : String(err));
11056
+ const errMsg = err instanceof Error ? err.message : String(err);
11057
+ renderError(errMsg);
11058
+ if (failureStore) {
11059
+ try {
11060
+ const { createHash: createHash2 } = await import("node:crypto");
11061
+ failureStore.insert({
11062
+ taskId: "",
11063
+ sessionId: `${Date.now()}`,
11064
+ repoRoot,
11065
+ failureType: "runtime-error",
11066
+ fingerprint: createHash2("sha256").update(errMsg.slice(0, 200)).digest("hex").slice(0, 16),
11067
+ filePath: null,
11068
+ errorMessage: errMsg.slice(0, 500),
11069
+ context: null,
11070
+ resolved: false,
11071
+ resolvedAt: null,
11072
+ notes: `Task: ${fullInput.slice(0, 200)}`
11073
+ });
11074
+ } catch {
11075
+ }
11076
+ }
9979
11077
  } finally {
9980
11078
  activeTask = null;
9981
11079
  }
@@ -10062,10 +11160,12 @@ var init_interactive = __esm({
10062
11160
  init_commands();
10063
11161
  init_setup();
10064
11162
  init_project_context();
11163
+ init_dist6();
10065
11164
  init_oa_directory();
10066
11165
  init_render();
10067
11166
  init_carousel();
10068
11167
  init_voice();
11168
+ init_stream_renderer();
10069
11169
  taskManager = new BackgroundTaskManager();
10070
11170
  }
10071
11171
  });
@@ -10263,7 +11363,7 @@ var init_embeddings = __esm({
10263
11363
  });
10264
11364
 
10265
11365
  // packages/indexer/dist/index.js
10266
- var init_dist6 = __esm({
11366
+ var init_dist7 = __esm({
10267
11367
  "packages/indexer/dist/index.js"() {
10268
11368
  "use strict";
10269
11369
  init_codebase_indexer();
@@ -10370,7 +11470,7 @@ async function indexRepoCommand(opts, _config) {
10370
11470
  var init_index_repo = __esm({
10371
11471
  "packages/cli/dist/commands/index-repo.js"() {
10372
11472
  "use strict";
10373
- init_dist6();
11473
+ init_dist7();
10374
11474
  init_spinner();
10375
11475
  init_output();
10376
11476
  }
@@ -10671,10 +11771,11 @@ var init_config3 = __esm({
10671
11771
  verbose: "Verbose output (true/false)",
10672
11772
  dbPath: "Path to SQLite memory database",
10673
11773
  voice: "Enable TTS voice feedback (true/false)",
10674
- voiceModel: "TTS voice model: glados, overwatch"
11774
+ voiceModel: "TTS voice model: glados, overwatch",
11775
+ stream: "Enable real-time token streaming with pastel syntax highlighting (true/false)"
10675
11776
  };
10676
11777
  INT_KEYS = /* @__PURE__ */ new Set(["maxRetries", "timeoutMs"]);
10677
- BOOL_KEYS = /* @__PURE__ */ new Set(["dryRun", "verbose", "voice"]);
11778
+ BOOL_KEYS = /* @__PURE__ */ new Set(["dryRun", "verbose", "voice", "stream"]);
10678
11779
  }
10679
11780
  });
10680
11781