@thincoder/core 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/abort-provenance.mjs +116 -0
- package/advisor/citations.mjs +139 -0
- package/advisor/compaction.mjs +174 -0
- package/advisor/convergence.mjs +80 -0
- package/advisor/history.mjs +77 -0
- package/advisor/loop.mjs +288 -0
- package/advisor/messages.mjs +299 -0
- package/advisor/notice.mjs +141 -0
- package/advisor/project-context.mjs +197 -0
- package/advisor/repos.mjs +150 -0
- package/advisor/run.mjs +190 -0
- package/advisor/truncate.mjs +57 -0
- package/advisor.mjs +281 -0
- package/agent/child-marks.mjs +24 -0
- package/agent/completion.mjs +145 -0
- package/agent/dispatch.mjs +493 -0
- package/agent/family-tools.mjs +174 -0
- package/agent/helpers.mjs +412 -0
- package/agent/post-turn.mjs +70 -0
- package/agent/record-results.mjs +174 -0
- package/agent/relay-prefix.mjs +39 -0
- package/agent/run-stages.mjs +244 -0
- package/agent/setup-reminders.mjs +199 -0
- package/agent/setup.mjs +234 -0
- package/agent/spawn-child.mjs +258 -0
- package/agent/suspension.mjs +240 -0
- package/agent/write-gate.mjs +87 -0
- package/agent-tools/advisor-async.mjs +481 -0
- package/agent-tools/advisor-settle.mjs +240 -0
- package/agent-tools/advisor.mjs +280 -0
- package/agent-tools/async-discard.mjs +143 -0
- package/agent-tools/async-settle.mjs +299 -0
- package/agent-tools/batch-segment.mjs +265 -0
- package/agent-tools/child-permission.mjs +45 -0
- package/agent-tools/consult.mjs +471 -0
- package/agent-tools/design-token.mjs +117 -0
- package/agent-tools/digest-budget.mjs +76 -0
- package/agent-tools/eng.mjs +102 -0
- package/agent-tools/escalate-async.mjs +302 -0
- package/agent-tools/goal.mjs +119 -0
- package/agent-tools/panel-blocks.mjs +24 -0
- package/agent-tools/parent-channel.mjs +231 -0
- package/agent-tools/plan.mjs +86 -0
- package/agent-tools/read-history.mjs +309 -0
- package/agent-tools/recent-changes.mjs +24 -0
- package/agent-tools/review-facts.mjs +31 -0
- package/agent-tools/settings.mjs +268 -0
- package/agent-tools/skill.mjs +63 -0
- package/agent-tools/spawn-gates.mjs +109 -0
- package/agent-tools/subagent-actions.mjs +496 -0
- package/agent-tools/subagent-async.mjs +456 -0
- package/agent-tools/subagent-panel.mjs +160 -0
- package/agent-tools/subagent-run.mjs +208 -0
- package/agent-tools/subagent-scheduler.mjs +446 -0
- package/agent-tools/subagent-spawn.mjs +478 -0
- package/agent-tools/subagent.mjs +419 -0
- package/agent-tools/task.mjs +87 -0
- package/agent-tools/timer.mjs +46 -0
- package/agent-tools/verify.mjs +295 -0
- package/agent-tools.mjs +23 -0
- package/agent.mjs +430 -0
- package/auto-think.mjs +115 -0
- package/compress-form.mjs +24 -0
- package/config-io.mjs +277 -0
- package/config-migrate.mjs +178 -0
- package/config-presets.mjs +49 -0
- package/config.mjs +419 -0
- package/context.mjs +495 -0
- package/conventions.mjs +223 -0
- package/embedding.mjs +120 -0
- package/escape.mjs +152 -0
- package/expand-home.mjs +16 -0
- package/explore-distill.mjs +152 -0
- package/generate-title.mjs +123 -0
- package/git/checkpoint.mjs +448 -0
- package/git/gitmem.mjs +100 -0
- package/history-window.mjs +179 -0
- package/hooks.mjs +108 -0
- package/i18n.mjs +106 -0
- package/index-bin.mjs +48 -0
- package/index-discover.mjs +176 -0
- package/ledger-cmd.mjs +209 -0
- package/ledger-db.mjs +86 -0
- package/ledger-surface.mjs +76 -0
- package/ledger.mjs +202 -0
- package/log.mjs +195 -0
- package/manifest.mjs +338 -0
- package/markdown.mjs +106 -0
- package/mcp/helpers.mjs +51 -0
- package/mcp/transport-http.mjs +248 -0
- package/mcp/transport-stdio.mjs +140 -0
- package/mcp/transport-ws.mjs +122 -0
- package/mcp.mjs +295 -0
- package/memory/code-index.mjs +219 -0
- package/memory/code-sync.mjs +427 -0
- package/memory/core.mjs +318 -0
- package/memory/delete.mjs +242 -0
- package/memory/docs.mjs +431 -0
- package/memory/file-walk.mjs +109 -0
- package/memory/origin.mjs +24 -0
- package/memory/scan.mjs +177 -0
- package/memory/schema.mjs +460 -0
- package/memory.mjs +21 -0
- package/model-ref.mjs +66 -0
- package/model-specs.mjs +277 -0
- package/package.json +32 -0
- package/peer-domains.mjs +265 -0
- package/peer-instances.mjs +178 -0
- package/permission.mjs +79 -0
- package/process-probe.mjs +315 -0
- package/prompt-files.mjs +113 -0
- package/prompt-overlays.mjs +78 -0
- package/prompts/advisor-design.md +43 -0
- package/prompts/advisor-round1.md +41 -0
- package/prompts/advisor-round2.md +46 -0
- package/prompts/advisor-round3.md +42 -0
- package/prompts/common.md +158 -0
- package/prompts/consult-base.md +19 -0
- package/prompts/discipline-engineering.md +123 -0
- package/prompts/discipline-normal.md +206 -0
- package/prompts/persona-coder.md +21 -0
- package/prompts/persona-eng-coder.md +41 -0
- package/prompts/persona-eng-designer.md +80 -0
- package/prompts/persona-engineering.md +160 -0
- package/prompts/persona-explore.md +15 -0
- package/prompts/persona-normal.md +35 -0
- package/prompts/persona-plan.md +27 -0
- package/provider/anthropic.mjs +225 -0
- package/provider/core.mjs +491 -0
- package/provider/errors.mjs +101 -0
- package/provider/google.mjs +257 -0
- package/provider/index.mjs +7 -0
- package/provider/list-models.mjs +163 -0
- package/provider/normalize.mjs +81 -0
- package/provider/rate.mjs +168 -0
- package/provider/responses.mjs +495 -0
- package/provider/retry.mjs +88 -0
- package/provider/sse.mjs +264 -0
- package/provider/wait-status.mjs +59 -0
- package/proxy.mjs +274 -0
- package/rules.mjs +53 -0
- package/session-gc.mjs +248 -0
- package/session-guard.mjs +59 -0
- package/session-lifecycle.mjs +305 -0
- package/session-migrate.mjs +48 -0
- package/session-rename.mjs +38 -0
- package/session-segments.mjs +99 -0
- package/session-slot-write.mjs +168 -0
- package/session-slots-manifest.mjs +264 -0
- package/session-slots.mjs +298 -0
- package/session-store.mjs +441 -0
- package/session.mjs +244 -0
- package/skills.mjs +234 -0
- package/text-budget.mjs +79 -0
- package/token-ttl.mjs +285 -0
- package/tool-docs/apply_patch.md +15 -0
- package/tool-docs/bash.md +38 -0
- package/tool-docs/delete.md +13 -0
- package/tool-docs/edit.md +30 -0
- package/tool-docs/execute.md +21 -0
- package/tool-docs/fetch.md +12 -0
- package/tool-docs/file_ops.md +17 -0
- package/tool-docs/get_current_time.md +8 -0
- package/tool-docs/git.md +54 -0
- package/tool-docs/glob.md +11 -0
- package/tool-docs/grep.md +19 -0
- package/tool-docs/hashline_edit.md +14 -0
- package/tool-docs/insert_after.md +15 -0
- package/tool-docs/lint.md +10 -0
- package/tool-docs/ls.md +12 -0
- package/tool-docs/lsp.md +10 -0
- package/tool-docs/process.md +10 -0
- package/tool-docs/question.md +16 -0
- package/tool-docs/read.md +20 -0
- package/tool-docs/read_image.md +8 -0
- package/tool-docs/tree.md +14 -0
- package/tool-docs/wait_for.md +22 -0
- package/tool-docs/websearch.md +16 -0
- package/tool-docs/write.md +11 -0
- package/tools/bash.mjs +276 -0
- package/tools/edit-batch.mjs +204 -0
- package/tools/edit-diff.mjs +388 -0
- package/tools/exec-run.mjs +43 -0
- package/tools/execute.mjs +243 -0
- package/tools/file.mjs +464 -0
- package/tools/git-checkpoint.mjs +143 -0
- package/tools/git-ext.mjs +173 -0
- package/tools/git.mjs +415 -0
- package/tools/glob-dialect.mjs +130 -0
- package/tools/index.mjs +76 -0
- package/tools/linter.mjs +120 -0
- package/tools/lsp.mjs +335 -0
- package/tools/ops.mjs +293 -0
- package/tools/patch.mjs +290 -0
- package/tools/question.mjs +26 -0
- package/tools/repomap.mjs +314 -0
- package/tools/search.mjs +248 -0
- package/tools/shared.mjs +467 -0
- package/tools/tree.mjs +81 -0
- package/tools/web.mjs +224 -0
- package/tools/write-path.mjs +191 -0
- package/traces/trace-store.mjs +303 -0
- package/undo-stack.mjs +47 -0
package/context.mjs
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context.mjs — Context management and compaction
|
|
3
|
+
* When no measured token count is available, use estimation as fallback (ASCII/4 + non-ASCII/1, no tokenizer dependency).
|
|
4
|
+
* When a measured value exists (response usage.prompt_tokens), trust it — estimation underestimates CJK by 3-4x and relying solely on it may never trigger compaction.
|
|
5
|
+
* Compaction strategy: summarize everything before the tail into one LLM note, keep the latest N messages verbatim.
|
|
6
|
+
* NOTE: no dedicated head is kept (KEEP_HEAD = 0) — in multi-task sessions the earliest messages are
|
|
7
|
+
* typically a COMPLETED earlier task; preserving them verbatim anchored the model's attention on stale
|
|
8
|
+
* work after compaction. The earliest messages now go into the summary (which distinguishes completed
|
|
9
|
+
* vs in-progress work), so the post-compaction context anchors on the current task (recent tail) only.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { chat } from "./provider/index.mjs"
|
|
13
|
+
import { estimateText } from "./provider/rate.mjs"
|
|
14
|
+
import { providerSpec } from "./config.mjs"
|
|
15
|
+
import { buildCompressMessages } from "./compress-form.mjs"
|
|
16
|
+
|
|
17
|
+
const IMAGE_TOKEN_ESTIMATE = 2000 // rough estimate for image content tokens (CLI legacy 256 underestimated real image costs, delaying compaction)
|
|
18
|
+
|
|
19
|
+
/** Rough token count for a list of messages (body + reasoning + tool_calls params) */
|
|
20
|
+
export function estimateTokens(messages) {
|
|
21
|
+
let tokens = 0
|
|
22
|
+
for (const m of messages) {
|
|
23
|
+
if (typeof m.content === "string") tokens += estimateText(m.content)
|
|
24
|
+
else if (Array.isArray(m.content)) {
|
|
25
|
+
for (const part of m.content) {
|
|
26
|
+
if (part.type === "text") tokens += estimateText(part.text)
|
|
27
|
+
else if (part.type === "image_url") tokens += IMAGE_TOKEN_ESTIMATE
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (typeof m.reasoning_content === "string") tokens += estimateText(m.reasoning_content)
|
|
31
|
+
for (const tc of m.tool_calls ?? []) {
|
|
32
|
+
tokens += estimateText(tc.function?.name ?? "") + estimateText(tc.function?.arguments ?? "")
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return tokens
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const KEEP_HEAD = 0 // No dedicated head: earliest messages may be a COMPLETED earlier task in multi-task
|
|
39
|
+
// sessions — keeping them verbatim anchored attention on stale work. Everything before the tail is
|
|
40
|
+
// summarized (the summary itself distinguishes completed vs in-progress work; see SUMMARIZE_PROMPT).
|
|
41
|
+
// Tail count formula (D4): window-adaptive (~30 msgs per 100K — old fixed 10 too thin on 1M), capped
|
|
42
|
+
// at 40% of history; §6.4④ D-T1/D-T2 make the count only a CANDIDATE — a token budget (TAIL_BUDGET_FRACTION
|
|
43
|
+
// × window − SUMMARY_TOKEN_ESTIMATE ≈1K, §6.9) tightens it over pair-safe boundaries when compaction runs,
|
|
44
|
+
// never below TAIL_FLOOR_MESSAGES; ordinary sessions never reach it (D-T4: trigger 0.6 untouched).
|
|
45
|
+
const TAIL_BUDGET_FRACTION = 0.15
|
|
46
|
+
const SUMMARY_TOKEN_ESTIMATE = 1000 // §6.9: summary output target ~1K tokens — reserved from the 15%
|
|
47
|
+
const TAIL_FLOOR_MESSAGES = 10 // §6.4④ D-T2: the tail keeps ≥10 verbatim messages — floor beats budget
|
|
48
|
+
function keepTailSize(provider, historyLen) {
|
|
49
|
+
// provider is guaranteed at every call site (runAgent always builds one); providerSpec
|
|
50
|
+
// degrades to DEFAULT_SPEC (128K) only if provider is somehow absent — acceptable
|
|
51
|
+
// because the 40% history cap still bounds the tail. providers[].context override
|
|
52
|
+
// (K units) is honored here (PROVIDER.md §6.15 T-C2: tail formula follows the window).
|
|
53
|
+
const ctxWindow = providerSpec(provider).context
|
|
54
|
+
return Math.min(Math.max(10, Math.floor((ctxWindow / 100_000) * 30)), Math.floor(historyLen * 0.4))
|
|
55
|
+
}
|
|
56
|
+
// §6.4④ D-T1 tail token budget: window×15% − summary ~1K — the compressed history segment (summary + placeholder + tail) lands ≈ 15% (B 口径 §6.4④).
|
|
57
|
+
function tailBudgetTokens(provider) {
|
|
58
|
+
return Math.max(0, Math.floor(providerSpec(provider).context * TAIL_BUDGET_FRACTION) - SUMMARY_TOKEN_ESTIMATE)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const SUMMARIZE_PROMPT = `The conversation above is our work log so far — summarize it into a compact summary for use as context in the ongoing conversation.
|
|
62
|
+
Requirements:
|
|
63
|
+
- Write in first person, present tense — these are "my" handover notes, continuing my own train of thought
|
|
64
|
+
- Most important: preserve design decisions and their reasons — architecture choices, API contracts, naming conventions, trade-off rationale. These are the anchors the subsequent code must not deviate from
|
|
65
|
+
- Distinguish COMPLETED vs IN-PROGRESS work: completed tasks get a ONE-LINE recap each (what was done, key outcome); spend the detail budget on unresolved issues, next steps, and the CURRENT task
|
|
66
|
+
- The user's most recent request defines the current task — anchor on it. Earlier requests are likely already completed and only need the one-line recap; do NOT preserve them at full fidelity
|
|
67
|
+
- Explicitly list FILES CHANGED: every modified file path plus a one-line "why" — so post-compaction work can re-locate what was edited and where
|
|
68
|
+
- Explicitly list UNRESOLVED ISSUES / TODOs: anything still open plus the next steps — so post-compaction recovery knows where to resume
|
|
69
|
+
- Drop: pleasantries, repetition, fine-grained tool output details
|
|
70
|
+
- Honestly mark uncertain items: anything not actually verified must say "unverified"; do not present guesses as facts
|
|
71
|
+
- Use bullet-point output. Stay under ~1K tokens (≈1000 Chinese chars / 4000 ASCII chars) — a hard target. An oversized summary wastes window and dilutes the tail; the old unbounded-length guidance is deprecated. When over budget, trim in this order: completed recaps to one line; FILES CHANGED why-notes to bare paths; in-progress prose tightened. NEVER cut design anchors or UNRESOLVED ISSUES/TODOs — recovery depends on them.
|
|
72
|
+
`
|
|
73
|
+
|
|
74
|
+
/** Context prefix after compaction, informing the agent what happened */
|
|
75
|
+
const COMPACTION_PREFIX =
|
|
76
|
+
"[Context was automatically compacted. Below is a summary of earlier work. " +
|
|
77
|
+
"Treat it as notes, not proof — trust its conclusions (don't redo what it reports as done) " +
|
|
78
|
+
"but re-verify transient state with tools. Check memory search for any missing decisions.]\n\n"
|
|
79
|
+
|
|
80
|
+
/** Placeholder assistant reply committed right after compaction (D9); D-CC18 merges it into an adjacent tail assistant instead of emitting it as a separate message */
|
|
81
|
+
const COMPACTION_PLACEHOLDER = "Understood. I'll continue from these notes, re-verifying anything transient."
|
|
82
|
+
|
|
83
|
+
/** After this many consecutive compaction summary failures, degrade to deterministic truncation (losing info is better than task-killing 400 errors) */
|
|
84
|
+
export const COMPRESS_FAILURE_LIMIT = 3
|
|
85
|
+
|
|
86
|
+
/** Task re-injection reminder prefix (after compaction, clear old versions from history first for a single source of truth) */
|
|
87
|
+
const TASK_REINJECT_PREFIX = "[System reminder: your current task list after compaction:"
|
|
88
|
+
|
|
89
|
+
/** Truncation fallback note (used when the summary LLM fails repeatedly; no LLM call) */
|
|
90
|
+
const FALLBACK_NOTE =
|
|
91
|
+
"[Context was truncated after repeated summarization failures. " +
|
|
92
|
+
"The middle portion of earlier work was dropped WITHOUT a summary. " +
|
|
93
|
+
"Re-verify any state you need with tools before relying on it.]\n\n"
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Split history into head / middle (to be summarized) / tail; return null if no middle to compress.
|
|
97
|
+
* head is normally empty (KEEP_HEAD = 0 — earliest messages go into the summary); the tool_calls-extension logic below is defensive for future KEEP_HEAD > 0.
|
|
98
|
+
* The tail boundary must include any assistant whose tool results are in the tail — if the assistant is in the middle, the summary swallows it, leaving orphan tool results → protocol 400.
|
|
99
|
+
* `budgetTokens` (optional, §6.4④ D-T1): when the candidate's estimate exceeds it, the boundary moves
|
|
100
|
+
* forward until the tail fits — never below the D-T2 floor (10 msgs, or the candidate itself when
|
|
101
|
+
* the 40% cap made it < 10 — short history).
|
|
102
|
+
*/
|
|
103
|
+
function splitHistory(history, keepTail, budgetTokens = null) {
|
|
104
|
+
if (history.length <= KEEP_HEAD + keepTail + 1) return null
|
|
105
|
+
let headEnd = KEEP_HEAD
|
|
106
|
+
// head must not end with dangling tool_calls: when assistant declares tool_calls, all its tool results must stay in head.
|
|
107
|
+
// Parallel calls: one assistant followed by multiple tool messages — accepting only one still causes 400, must collect all
|
|
108
|
+
if (history[headEnd - 1]?.role === "assistant" && history[headEnd - 1].tool_calls?.length) {
|
|
109
|
+
while (headEnd < history.length && history[headEnd].role === "tool") headEnd++
|
|
110
|
+
}
|
|
111
|
+
const candidate = repairedTailStart(history, headEnd, history.length - keepTail)
|
|
112
|
+
if (candidate <= headEnd) return null
|
|
113
|
+
let tailStart = candidate
|
|
114
|
+
// §6.4④ D-T1: tighten only above the floor — a candidate ≤ 10 IS the floor (short history under the 40% cap must not tighten further, review #5); the floor is D5-repaired too.
|
|
115
|
+
if (budgetTokens > 0 && keepTail > TAIL_FLOOR_MESSAGES) {
|
|
116
|
+
const floor = repairedTailStart(history, headEnd, history.length - TAIL_FLOOR_MESSAGES)
|
|
117
|
+
if (floor > candidate) tailStart = tightenTailByBudget(history, candidate, floor, budgetTokens)
|
|
118
|
+
}
|
|
119
|
+
return { headEnd, tailStart }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* D5 tail-side pairing repair for a raw cut at history.length − tailCount: pull into the tail any
|
|
124
|
+
* assistant whose tool results are in the tail (the summary swallowing the owner leaves orphan tool
|
|
125
|
+
* results → protocol 400), then skip orphan tool messages at the new boundary. Single-assistant
|
|
126
|
+
* assumption (nearest owner only — a tail spans at most one assistant→tools cycle); bounds-guarded.
|
|
127
|
+
*/
|
|
128
|
+
function repairedTailStart(history, headEnd, tailStart) {
|
|
129
|
+
const tailToolIds = new Set()
|
|
130
|
+
for (let i = tailStart; i < history.length; i++) {
|
|
131
|
+
if (history[i].role === "tool") tailToolIds.add(history[i].tool_call_id)
|
|
132
|
+
}
|
|
133
|
+
for (let i = tailStart - 1; i > headEnd; i--) {
|
|
134
|
+
const m = history[i]
|
|
135
|
+
if (m.role === "assistant" && m.tool_calls?.some((tc) => tailToolIds.has(tc.id))) {
|
|
136
|
+
tailStart = i
|
|
137
|
+
break
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
while (tailStart < history.length && tailStart > headEnd && history[tailStart].role === "tool") {
|
|
141
|
+
tailStart++
|
|
142
|
+
}
|
|
143
|
+
return tailStart
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* §6.4④ D-T1 budget tightening (pair-safe, review #2): walk the boundary FORWARD (fewer tail messages —
|
|
148
|
+
* the rest joins the summary) while the tail's estimated tokens exceed the budget. Only pair-safe
|
|
149
|
+
* positions may stop the walk: a boundary ON a tool message would orphan its owner assistant into the
|
|
150
|
+
* middle (D5); pairing is contiguous in the machine line (§6.4③) — every non-tool boundary is safe.
|
|
151
|
+
* No fit before the floor → keep the floor, accept the overrun.
|
|
152
|
+
*/
|
|
153
|
+
function tightenTailByBudget(history, start, floorStart, budgetTokens) {
|
|
154
|
+
const suffixTokens = new Array(history.length + 1)
|
|
155
|
+
suffixTokens[history.length] = 0
|
|
156
|
+
for (let i = history.length - 1; i >= 0; i--) suffixTokens[i] = suffixTokens[i + 1] + estimateTokens([history[i]])
|
|
157
|
+
if (suffixTokens[start] <= budgetTokens) return start // already fits — ordinary sessions stay untouched (D-T2)
|
|
158
|
+
for (let p = start + 1; p <= floorStart; p++) { // first fit keeps the most recent verbatim context
|
|
159
|
+
if (history[p].role !== "tool" && suffixTokens[p] <= budgetTokens) return p
|
|
160
|
+
}
|
|
161
|
+
return floorStart
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* pushReal — the single entry point for REAL conversation messages.
|
|
166
|
+
* A real message (user input, assistant reply, tool result, multimodal image) is appended to BOTH:
|
|
167
|
+
* agent.history — the machine context (compaction shrinks this)
|
|
168
|
+
* agent._fullHistory — the human-readable record (persistence source)
|
|
169
|
+
* Machine-only messages ([System reminder:...], compaction notes, task/plan/checkpoint re-injections)
|
|
170
|
+
* are pushed directly to agent.history WITHOUT going through here, so they never enter _fullHistory.
|
|
171
|
+
* The two lines are written independently at the source — no after-the-fact delta sync.
|
|
172
|
+
* Message timestamps (SESSION.md §6.9): stamped HERE once at push time (epoch ms) — a single
|
|
173
|
+
* point covers every real message. Pre-existing ts (e.g. from another end writing the shared slot)
|
|
174
|
+
* is preserved; restored old messages keep no ts rather than getting a misleading backdate (D-S3).
|
|
175
|
+
* ts is a LOCAL-ONLY field — the send layer strips it before any provider request (T-S3).
|
|
176
|
+
*
|
|
177
|
+
* TUI-OOM-ROOTCAUSE 批(SESSION.md §6.14)——人读线内存有界 + 磁盘为准:
|
|
178
|
+
* ① `agent._recordStore?.append(msg)`:记录同步追加(磁盘为准——append-only sidecar);
|
|
179
|
+
* ② 窗口驱逐:绑定态(agent._historyWindow = 200)下 _fullHistory 只保最近窗口条——
|
|
180
|
+
* 更早内容仅存磁盘(翻页/检索/保存从盘按需读)。未绑定(模式 F)不驱逐(零回归)。
|
|
181
|
+
* 追加失败不阻断回合(独立 try/catch——尽力面 N-S6;store 内部另置 degraded 并停写)。
|
|
182
|
+
*/
|
|
183
|
+
export function pushReal(agent, msg) {
|
|
184
|
+
if (!Array.isArray(agent._fullHistory)) agent._fullHistory = []
|
|
185
|
+
if (msg && msg.ts === undefined) msg.ts = Date.now()
|
|
186
|
+
agent._fullHistory.push(msg)
|
|
187
|
+
try { agent._recordStore?.append(msg) } catch { /* 尽力面:落盘失败不阻断回合(N-S6) */ }
|
|
188
|
+
const win = agent._historyWindow
|
|
189
|
+
if (win > 0 && agent._fullHistory.length > win) {
|
|
190
|
+
agent._fullHistory.splice(0, agent._fullHistory.length - win)
|
|
191
|
+
}
|
|
192
|
+
agent.history.push(msg)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Content-shape-safe prefixing (D-CC18, generalized for D-CC19 merge reuse):
|
|
197
|
+
* string → text + blank line + original; multimodal array → text part prepended;
|
|
198
|
+
* empty string / null / undefined / other → text alone.
|
|
199
|
+
*/
|
|
200
|
+
function prefixContent(content, text) {
|
|
201
|
+
if (typeof content === "string" && content.length > 0) return `${text}\n\n${content}`
|
|
202
|
+
if (Array.isArray(content)) return [{ type: "text", text }, ...content]
|
|
203
|
+
return text
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* D-CC18 echo safety: prefix the placeholder onto an existing assistant message's content.
|
|
208
|
+
* Thin wrapper over prefixContent (behavioral semantics unchanged).
|
|
209
|
+
*/
|
|
210
|
+
function withPlaceholderPrefix(content) {
|
|
211
|
+
return prefixContent(content, COMPACTION_PLACEHOLDER)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* D-CC19 restore-path echo merge (2026-09-16 ENGINE-DEBT 批 ED-1): persisted `contextHistory`
|
|
216
|
+
* is loaded back VERBATIM on session restore, so the D-CC18 pathological shape (an assistant
|
|
217
|
+
* WITHOUT reasoning_content directly followed by another assistant — DeepSeek-family thinking
|
|
218
|
+
* mode rejects the first request with 400) can revive from disk. Pure in-core function: scans
|
|
219
|
+
* only at restore time, never prompts the user, never rewrites the session file. Merge direction
|
|
220
|
+
* matches D-CC18 (the reasoning-less message is absorbed INTO its follower — the follower's
|
|
221
|
+
* tool_calls / reasoning_content / other fields are kept verbatim; texts joined with a blank
|
|
222
|
+
* line). Iterates to a fixed point (chains collapse in full). Copy-on-write: messages may be
|
|
223
|
+
* shared with other lines, so the merged message is always a NEW object; clean input returns
|
|
224
|
+
* the SAME array reference (zero copy).
|
|
225
|
+
*/
|
|
226
|
+
|
|
227
|
+
/** Pair predicate: prev = assistant with no/empty reasoning_content and no tool_calls,
|
|
228
|
+
* directly followed by another assistant. (Prev WITH tool_calls is never merged — pairing
|
|
229
|
+
* safety, F-3.) */
|
|
230
|
+
export function isAssistantEchoPair(prev, next) {
|
|
231
|
+
return prev?.role === "assistant"
|
|
232
|
+
&& next?.role === "assistant"
|
|
233
|
+
&& !prev.reasoning_content
|
|
234
|
+
&& !(Array.isArray(prev.tool_calls) && prev.tool_calls.length > 0)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Text of a message content in any supported shape (string / parts array / null-ish). */
|
|
238
|
+
function contentTextOf(content) {
|
|
239
|
+
if (typeof content === "string") return content
|
|
240
|
+
if (Array.isArray(content)) {
|
|
241
|
+
return content.filter((p) => p?.type === "text").map((p) => p.text ?? "").join("\n\n")
|
|
242
|
+
}
|
|
243
|
+
return ""
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Absorb prev's text into next's content (blank-line join; empty prev text → next unchanged). */
|
|
247
|
+
function absorbEchoContent(prevContent, nextContent) {
|
|
248
|
+
const text = contentTextOf(prevContent)
|
|
249
|
+
if (text.length === 0) return nextContent
|
|
250
|
+
return prefixContent(nextContent, text)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function mergeAdjacentAssistantEchoes(history) {
|
|
254
|
+
if (!Array.isArray(history)) return history
|
|
255
|
+
let src = history
|
|
256
|
+
for (;;) {
|
|
257
|
+
let changed = false
|
|
258
|
+
const next = []
|
|
259
|
+
for (let i = 0; i < src.length; i++) {
|
|
260
|
+
if (i + 1 < src.length && isAssistantEchoPair(src[i], src[i + 1])) {
|
|
261
|
+
next.push({ ...src[i + 1], content: absorbEchoContent(src[i].content, src[i + 1].content) })
|
|
262
|
+
i += 1
|
|
263
|
+
changed = true
|
|
264
|
+
} else {
|
|
265
|
+
next.push(src[i])
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (!changed) return src === history ? history : src
|
|
269
|
+
src = next
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Replace middle with a note, then re-inject task/plan state (shared by LLM summary and truncation fallback) */
|
|
274
|
+
function applyCompression(agent, headEnd, tailStart, note) {
|
|
275
|
+
// _fullHistory already holds every real message (written at the source via pushReal),
|
|
276
|
+
// so compaction only shrinks the machine line — nothing to preserve here.
|
|
277
|
+
// head is normally empty (KEEP_HEAD = 0) — the summary note becomes the first message,
|
|
278
|
+
// which is exactly the intent: post-compaction context anchors on the current task, not on
|
|
279
|
+
// possibly-completed earlier requests.
|
|
280
|
+
const head = agent.history.slice(0, headEnd)
|
|
281
|
+
const tail = agent.history.slice(tailStart)
|
|
282
|
+
// SESSION.md §6.9: compaction-injected messages carry a ts — Date.now() at the compaction
|
|
283
|
+
// moment (the note below; and the separate "Understood" placeholder in the non-merge branch).
|
|
284
|
+
// They are machine-only (never in _fullHistory), but the machine-line timeline stays consistent
|
|
285
|
+
// for any audit use. D-CC18 exception: in the merge branch the placeholder rides inside a REAL
|
|
286
|
+
// tail message, which keeps its OWN ts — merging must not rewrite that message's time semantics
|
|
287
|
+
// (and must not backfill ts-less restored messages either, D-S3).
|
|
288
|
+
const now = Date.now()
|
|
289
|
+
// D-CC18 echo safety: an assistant WITHOUT reasoning_content directly followed by another
|
|
290
|
+
// assistant is rejected 400 by DeepSeek-family thinking mode ("The `reasoning_content` in the
|
|
291
|
+
// thinking mode must be passed back to the API." — traced from a subagent's first post-compaction
|
|
292
|
+
// request, 2026-09-16). The synthetic placeholder has no reasoning to echo, so when the tail
|
|
293
|
+
// starts with an assistant the placeholder is merged INTO that message — copy-on-write, because
|
|
294
|
+
// pushReal shares message objects with _fullHistory (never mutate in place) — instead of being
|
|
295
|
+
// emitted as a separate message.
|
|
296
|
+
//
|
|
297
|
+
// Compaction REBUILDS the machine line (head + note + placeholder + tail), so the pre-compaction
|
|
298
|
+
// _runStartHistoryLen index is stale — a longer array shrank beneath it, and end-of-run exploration
|
|
299
|
+
// distillation would then silently skip or slice from the wrong offset. Reset the boundary to the
|
|
300
|
+
// first verbatim tail message: head.length + 1 in the merge branch (the placeholder rides inside
|
|
301
|
+
// the rewritten tail[0], which keeps its index), head.length + 2 otherwise (the note and the
|
|
302
|
+
// separate "Understood" placeholder sit between head and tail). Exploration before the tail was
|
|
303
|
+
// already covered by the compaction summary, so only the still-raw tail needs distilling. `head`
|
|
304
|
+
// is empty today (KEEP_HEAD = 0) — the formula stays correct if KEEP_HEAD ever grows.
|
|
305
|
+
// (shrinkOversized only truncates message bodies in place and leaves the array length unchanged,
|
|
306
|
+
// so this boundary stays valid there — no reset needed.)
|
|
307
|
+
if (tail[0]?.role === "assistant") {
|
|
308
|
+
const merged = { ...tail[0], content: withPlaceholderPrefix(tail[0].content) }
|
|
309
|
+
agent.history = [
|
|
310
|
+
...head,
|
|
311
|
+
{ role: "user", content: note, ts: now },
|
|
312
|
+
merged,
|
|
313
|
+
...tail.slice(1),
|
|
314
|
+
]
|
|
315
|
+
agent._runStartHistoryLen = head.length + 1
|
|
316
|
+
} else {
|
|
317
|
+
agent.history = [
|
|
318
|
+
...head,
|
|
319
|
+
{ role: "user", content: note, ts: now },
|
|
320
|
+
{ role: "assistant", content: COMPACTION_PLACEHOLDER, ts: now },
|
|
321
|
+
...tail,
|
|
322
|
+
]
|
|
323
|
+
agent._runStartHistoryLen = head.length + 2
|
|
324
|
+
}
|
|
325
|
+
// Measured token baseline is invalidated along with old history (prompt_tokens were for pre-compaction context), fall back to estimation until next response
|
|
326
|
+
agent._lastPromptTokens = null
|
|
327
|
+
agent._usageAtLen = null
|
|
328
|
+
|
|
329
|
+
// After compaction, re-inject the task list (the agent needs to know what it was doing).
|
|
330
|
+
// Single source of truth: first remove any stale re-injections from the tail, then inject the latest version —
|
|
331
|
+
// no longer embedded in the summary body (would duplicate and grow stale)
|
|
332
|
+
agent.history = agent.history.filter(
|
|
333
|
+
(m) => !(m.role === "user" && typeof m.content === "string" && m.content.startsWith(TASK_REINJECT_PREFIX))
|
|
334
|
+
)
|
|
335
|
+
if (agent.tasks.length > 0) {
|
|
336
|
+
const taskSummary = agent.tasks.map((t) => `- [${t.status}] ${t.title}`).join("\n")
|
|
337
|
+
agent.history.push({
|
|
338
|
+
role: "user",
|
|
339
|
+
content: `${TASK_REINJECT_PREFIX}\n${taskSummary}\nContinue from where you left off.]`,
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Plan mode compaction: re-inject plan mode guidance
|
|
344
|
+
if (agent.planMode) {
|
|
345
|
+
agent.history.push({
|
|
346
|
+
role: "user",
|
|
347
|
+
content: "[System reminder: plan mode is active. Explore the codebase read-only, design your solution, then call plan with action='exit' to present it for user approval.]",
|
|
348
|
+
})
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* If history exceeds threshold, compact it. Returns whether compaction happened.
|
|
354
|
+
* Only called at safe points in the loop (history ends with user or tool message — a complete exchange boundary).
|
|
355
|
+
* Automatically re-injects task list state after compaction.
|
|
356
|
+
* @param {object} agent
|
|
357
|
+
* @param {number} threshold - compaction threshold in tokens
|
|
358
|
+
* @param {object} callbacks - { onToken, onReasoning, onCompress, onCompressStart } — summary
|
|
359
|
+
* generation is SILENT (never forwards onToken/onReasoning: the compaction process is an
|
|
360
|
+
* internal mechanism, not a model reply); onCompressStart fires right before the summary call
|
|
361
|
+
* (§6.8 D-C1, compression lifecycle visibility — panel start state)
|
|
362
|
+
* @param {object} extras - { systemPrompt?, tools? } — estimated overhead for the pure-estimation
|
|
363
|
+
* path (no measured baseline); the measured path already includes system+tools in prompt_tokens.
|
|
364
|
+
*/
|
|
365
|
+
export async function compressIfNeeded(agent, threshold, callbacks, extras = {}, signal) {
|
|
366
|
+
const history = agent.history
|
|
367
|
+
// Prefer the real baseline: the last response's prompt_tokens is the measured value for the full context (system+tools+history).
|
|
368
|
+
// Subsequent appended messages use estimation as increment; when no measured value exists (first turn / after restore / right after compaction), fall back to pure estimation
|
|
369
|
+
const overhead =
|
|
370
|
+
(extras.systemPrompt ? estimateText(extras.systemPrompt) : 0) +
|
|
371
|
+
(extras.tools ? estimateText(JSON.stringify(extras.tools)) : 0)
|
|
372
|
+
const tokens =
|
|
373
|
+
agent._lastPromptTokens != null
|
|
374
|
+
? agent._lastPromptTokens + estimateTokens(history.slice(agent._usageAtLen ?? history.length))
|
|
375
|
+
: estimateTokens(history) + overhead
|
|
376
|
+
if (tokens <= threshold) return false
|
|
377
|
+
|
|
378
|
+
const keepTail = keepTailSize(agent.provider, history.length)
|
|
379
|
+
const split = splitHistory(history, keepTail, tailBudgetTokens(agent.provider))
|
|
380
|
+
if (!split) {
|
|
381
|
+
// History is too short (≤KEEP_HEAD+keepTail+1 messages) to find a middle section, but tokens exceed threshold — typically a single giant message
|
|
382
|
+
// (large paste / huge injection). When summarization has no room, degrade to deterministic shrinking to ensure context always reduces
|
|
383
|
+
return shrinkOversized(agent)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const middle = history.slice(split.headEnd, split.tailStart)
|
|
387
|
+
|
|
388
|
+
// Compression request form v2/v3 (§6.14 / D-CC20): continuation — same system + same tools declaration (the turn's own
|
|
389
|
+
// array) + the middle's verbatim messages + one tail instruction. 首现即复用回合所建前缀 ⟺ 带 tools 声明(无 tool_choice)
|
|
390
|
+
// 且 `reasoning_effort` 与回合侧同值——v2 探针复测:8 格 84–98%(读数详表见批次档 §5);不带 tools / 带 tool_choice:"none" / effort 异值 ⇒ 首现 0%(自建项复跑例外);无 extras.tools ⇒ 不发 tools。
|
|
391
|
+
// ⚠️ **不带 `tool_choice`**(设计 §6.14 备选② · 预注册判定规则「S3 <0.9 且 S4 ≥0.9 ⇒ 采纳 v2 减 tool_choice」启用——实测该参数使服务端丢弃 tools 区 ⇒ 首现命中 0%)。
|
|
392
|
+
// ⚠️ v3(父侧 2026-09-18 裁定):**随带与回合请求同源的 `reasoning_effort`**——下行不再覆盖 `agent.provider.reasoningEffort`(回合调用 `chat(agent.provider, …)` 同字段;
|
|
393
|
+
// 不硬编码、未配置 ⇒ 缺省同修前。实证:deepseek 同值 95.69% / 异值首现 0%;族差/窗差:百炼 qwen 另有 `enable_thinking` 派生差、autoThink 的 turn 0 有改写窗口——登记见批次档 §5 上抛)。
|
|
394
|
+
// Silent by design (D11): no onToken/onReasoning — the compaction process must not stream to the frontend.
|
|
395
|
+
// signal propagates user cancellation (Ctrl+C) to the in-flight summary call.
|
|
396
|
+
// Compression visibility (CONTEXT-COMPACTION.md §6.8 D-C1/D-C2): the frontend learns the compression
|
|
397
|
+
// STARTED right before the summary LLM call ("Compressing context… / summarizing N messages" panel) — only
|
|
398
|
+
// the lifecycle is surfaced, never the summary body. N = the number of history messages being summarized.
|
|
399
|
+
callbacks?.onCompressStart?.({ messages: middle.length })
|
|
400
|
+
const startedAt = performance.now()
|
|
401
|
+
const summary = await chat({ ...agent.provider, thinking: null }, {
|
|
402
|
+
messages: buildCompressMessages(history, split.tailStart, extras?.systemPrompt, SUMMARIZE_PROMPT),
|
|
403
|
+
tools: extras?.tools,
|
|
404
|
+
signal,
|
|
405
|
+
// §18.6 D-TR4:轨迹元数据增补——kind=compress(上下文构建面——agent 元数据透出;
|
|
406
|
+
// depth 经 extras.traceDepth——agent.mjs 主作用域传入——compress 调用点补齐)
|
|
407
|
+
logCtx: {
|
|
408
|
+
stage: "compress", child: agent._logId, kind: "compress",
|
|
409
|
+
role: agent._role ?? null, depth: extras?.traceDepth ?? null,
|
|
410
|
+
session: agent._sessionStart ?? null, cwd: agent.cwd,
|
|
411
|
+
traces: agent.config?.traces?.enabled !== false,
|
|
412
|
+
},
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
// Blank-summary guard (§6.14 退化面 3): a blank summary would land in applyCompression as
|
|
416
|
+
// "middle dropped + empty note" and still count as success — throw into the failure chain instead.
|
|
417
|
+
if (!summary.content?.trim()) throw new Error("compaction summary is empty")
|
|
418
|
+
|
|
419
|
+
applyCompression(agent, split.headEnd, split.tailStart, COMPACTION_PREFIX + summary.content)
|
|
420
|
+
|
|
421
|
+
// Completion info for the compression panel (D-C2): tokens freed = the pre-compression prompt
|
|
422
|
+
// estimate (`tokens` — the value that tripped the threshold, incl. system/tools overhead on the
|
|
423
|
+
// pure-estimation path) minus the post-compression estimate on the same basis. Elapsed = the
|
|
424
|
+
// summary call + splice duration. agent.mjs forwards this to onCompress unchanged.
|
|
425
|
+
agent._lastCompressInfo = {
|
|
426
|
+
mode: "summary",
|
|
427
|
+
tokensFreed: Math.max(0, Math.round(tokens - (estimateTokens(agent.history) + overhead))),
|
|
428
|
+
elapsedMs: performance.now() - startedAt,
|
|
429
|
+
}
|
|
430
|
+
return true
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Deterministic truncation fallback: called when the summary LLM fails repeatedly, no network call.
|
|
435
|
+
* Drops the middle so the task can continue. Returns whether truncation happened.
|
|
436
|
+
*/
|
|
437
|
+
export function compressFallback(agent) {
|
|
438
|
+
const keepTail = keepTailSize(agent.provider, agent.history.length)
|
|
439
|
+
const split = splitHistory(agent.history, keepTail, tailBudgetTokens(agent.provider))
|
|
440
|
+
if (!split) return false
|
|
441
|
+
const tailMessages = agent.history.length - split.tailStart
|
|
442
|
+
applyCompression(agent, split.headEnd, split.tailStart, FALLBACK_NOTE)
|
|
443
|
+
// Fallback completion info (D-C2): mode marks the deterministic-truncation path — the panel
|
|
444
|
+
// shows the degradation note ("truncated to N messages") ONLY after 3 consecutive failures.
|
|
445
|
+
agent._lastCompressInfo = { mode: "fallback", tailMessages }
|
|
446
|
+
return true
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Hard truncation limit for a single message body: when exceeded and the splitter can't find a middle section, truncate to a stub (prevents one giant message from blocking compaction) */
|
|
450
|
+
const OVERSIZE_CONTENT_LIMIT = 8_000
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Deterministic shrinking: last resort when splitHistory can't find a middle section (history too short) but threshold is exceeded. No LLM call.
|
|
454
|
+
* Truncates user/tool message bodies exceeding OVERSIZE_CONTENT_LIMIT to a stub (keeps head + tail);
|
|
455
|
+
* does not touch reasoning_content (DeepSeek/Kimi echo protocol) or tool_calls pairing structure — no protocol 400
|
|
456
|
+
* risk from this path (this module's echo-safety face is `applyCompression`: the compaction placeholder is never
|
|
457
|
+
* committed as a separate reasoning-less assistant next to another assistant — D-CC18, 2026-09-16 batch).
|
|
458
|
+
* Only called after compressIfNeeded determines threshold is exceeded. Returns whether any message was truncated.
|
|
459
|
+
*/
|
|
460
|
+
function shrinkOversized(agent, limit = OVERSIZE_CONTENT_LIMIT) {
|
|
461
|
+
let shrunk = false
|
|
462
|
+
// Copy-on-write: build a NEW array and replace only truncated entries. pushReal stores the SAME
|
|
463
|
+
// message object in both `agent.history` (machine line) and `agent._fullHistory` (human/persistence
|
|
464
|
+
// line), so in-place `m.content = ...` would ALSO truncate the never-compacted human line and lose
|
|
465
|
+
// the original pasted content on session persist (session.mjs persists _fullHistory). VS Code port
|
|
466
|
+
// already copies (`history.map(m => ({ ...m }))`); this brings CLI to parity.
|
|
467
|
+
const next = agent.history.map((m) => {
|
|
468
|
+
if ((m.role !== "user" && m.role !== "tool") || typeof m.content !== "string") return m
|
|
469
|
+
if (m.content.length <= limit) return m
|
|
470
|
+
// Truncate keeping head + tail, insert stub in between; keepHead/keepTail proportional but not exceeding 50%/25% of limit
|
|
471
|
+
const keepHead = Math.min(Math.floor(limit * 0.5), 4000)
|
|
472
|
+
const keepTail = Math.min(Math.floor(limit * 0.25), 2000)
|
|
473
|
+
shrunk = true
|
|
474
|
+
return {
|
|
475
|
+
...m,
|
|
476
|
+
content:
|
|
477
|
+
m.content.slice(0, keepHead) +
|
|
478
|
+
`\n[... ${m.content.length - keepHead - keepTail} chars truncated — single message too large for context window ...]\n` +
|
|
479
|
+
m.content.slice(-keepTail),
|
|
480
|
+
}
|
|
481
|
+
})
|
|
482
|
+
if (shrunk) {
|
|
483
|
+
agent.history = next
|
|
484
|
+
// Same as compaction: measured token baseline is invalidated by the changed history, fall back to estimation until next response
|
|
485
|
+
agent._lastPromptTokens = null
|
|
486
|
+
agent._usageAtLen = null
|
|
487
|
+
}
|
|
488
|
+
return shrunk
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// ─── End-of-run exploration distillation(2026-09-05 module-split:524 > 500 硬限——verbatim
|
|
492
|
+
// 迁至 explore-distill.mjs,语义零变——VS Code compact.mjs 同款联动;cross-repo parity 锚改指
|
|
493
|
+
// explore-distill.mjs——消费方 import 面不变(re-export))───────────────────────
|
|
494
|
+
|
|
495
|
+
export { summarizeRunExplorations, EXPLORE_TOOLS, EXPLORE_SUMMARY_PROMPT } from "./explore-distill.mjs"
|