@kolisachint/hoocode-agent 0.4.146 → 0.4.148
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/CHANGELOG.md +12 -0
- package/dist/core/context-gc.d.ts +7 -4
- package/dist/core/context-gc.d.ts.map +1 -1
- package/dist/core/context-gc.js +45 -11
- package/dist/core/context-gc.js.map +1 -1
- package/dist/core/sdk.d.ts +5 -6
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/system-prompt.d.ts +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +6 -3
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +1 -1
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/search.d.ts.map +1 -1
- package/dist/core/tools/search.js +3 -3
- package/dist/core/tools/search.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +3 -4
- package/dist/main.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.148] - 2026-07-21
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Context GC no longer evicts a `read` result just because the same file was read again over a **different** line range. Eviction is now range-aware: a read is superseded only by a later read whose `offset`/`limit` window overlaps it (or by any later edit/write). This fixes a read loop where alternating reads of two disjoint regions of one large file would repeatedly stub each other, and the "Re-read the file if you need its current contents" hint would drive the model to re-read indefinitely.
|
|
8
|
+
|
|
9
|
+
## [0.4.147] - 2026-07-20
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Surfaced the `search` tool more prominently in the default system prompt: it is now listed first in file-exploration guidance, its description no longer frames it as "grep-backed", and its guidelines encourage starting with search for discovery while reserving `grep` for exact-line/regex enumeration.
|
|
14
|
+
|
|
3
15
|
## [0.4.146] - 2026-07-19
|
|
4
16
|
|
|
5
17
|
### Changed
|
|
@@ -13,10 +13,13 @@
|
|
|
13
13
|
*
|
|
14
14
|
* A `read` result for a path P is stale once, later in the transcript, the
|
|
15
15
|
* same path is edited/written (its on-disk content changed) or read again
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* over an overlapping line range (the newer read reflects that region's newer
|
|
17
|
+
* state). Reads of disjoint regions of the same file coexist — a later read of
|
|
18
|
+
* lines 200-260 does not evict an earlier read of lines 1-40 — so paginating
|
|
19
|
+
* through a large file never makes the model re-fetch a region it already has.
|
|
20
|
+
* A read is only evicted when a *successful* later event supersedes it — so a
|
|
21
|
+
* read whose edit failed (and which the model still needs to retry) is never
|
|
22
|
+
* touched.
|
|
20
23
|
*
|
|
21
24
|
* Deliberately conservative: paths are matched after `path.resolve`, so two
|
|
22
25
|
* different files never collide, and any ambiguity results in NOT evicting.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-gc.d.ts","sourceRoot":"","sources":["../../src/core/context-gc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAoBpE,MAAM,WAAW,gBAAgB;IAChC,sEAAsE;IACtE,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAuBD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,gBAAgB,GAAG,YAAY,EAAE,CA8FxG","sourcesContent":["/**\n * Context garbage collection.\n *\n * The whole transcript is re-sent on every turn, so a tool result that has\n * become useless keeps costing tokens for the rest of the session. This pass\n * runs on the OUTGOING message copy (via the agent's `transformContext` hook),\n * never on the persisted session, and replaces provably-dead `read` results\n * with a short stub. Nothing is lost: the persisted history is untouched and\n * the file is re-readable on demand.\n *\n * Current rule — superseded reads only (the read-then-edit / re-read pattern,\n * which dominates coding sessions):\n *\n * A `read` result for a path P is stale once, later in the transcript, the\n * same path is edited/written (its on-disk content changed) or read again\n * (the newer read reflects newer state). The most recent read of each path is\n * always kept, and a read is only evicted when a *successful* later event\n * supersedes it — so a read whose edit failed (and which the model still needs\n * to retry) is never touched.\n *\n * Deliberately conservative: paths are matched after `path.resolve`, so two\n * different files never collide, and any ambiguity results in NOT evicting.\n *\n * Bash-output eviction is additionally gated on token-budget pressure\n * (`options.budgetPressure`, the fraction of the model's context window in\n * use). At 0 pressure — the default — behaviour is identical to read-only GC.\n * Bash output is not always recoverable, so it carries its own safeguards: a\n * command that looks side-effecting is never elided (re-running it, as the\n * stub invites, could repeat a destructive action), and below 80% pressure\n * only large outputs are elided.\n */\n\nimport { resolve } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\n\n/** Tool names whose result injects file contents into the transcript. */\nconst READ_TOOLS = new Set([\"read\"]);\n/** Tool names that change a file, making a prior read of that path stale. */\nconst MUTATE_TOOLS = new Set([\"edit\", \"write\"]);\n\n/**\n * Commands whose bash output must never be elided, because the stub invites\n * the model to re-run the command and re-running could repeat a destructive or\n * state-changing action. Tested against the whole command string, and\n * deliberately over-broad: a false positive merely keeps an output (safe),\n * while the common verbose *read* commands (cat/grep/ls/find/git log/diff,\n * test runners) intentionally do NOT match, so they stay evictable.\n */\nconst BASH_SIDE_EFFECT_PATTERN =\n\t/(\\b(write|insert|delete|update|curl|wget|psql|mysql|sqlite3|migrate|drop|truncate|rm|rmdir|mv|cp|dd|kill|tee|chmod|chown|ln|mkdir|touch)\\b|\\bgit\\s+(commit|push|reset|checkout|rebase|clean|apply|merge)\\b|\\bsed\\b[^|]*-i|>>?)/i;\n/** Below 80% pressure, only bash outputs larger than this (chars) are elided. */\nconst BASH_EVICTION_CHAR_THRESHOLD = 2000;\n\nexport interface ContextGcOptions {\n\t/** Working directory used to resolve relative tool path arguments. */\n\tcwd: string;\n\t/**\n\t * Token-budget pressure in [0, 1] — the fraction of the model's context\n\t * window currently in use. Absent or 0 reproduces read-only GC behaviour.\n\t * Bash-output eviction begins at 0.6 and becomes unconditional at 0.8.\n\t */\n\tbudgetPressure?: number;\n}\n\ninterface AssistantLike {\n\trole: \"assistant\";\n\tcontent: Array<{ type: string; id?: string; name?: string; arguments?: Record<string, unknown> }>;\n}\n\ninterface ToolResultLike {\n\trole: \"toolResult\";\n\ttoolCallId: string;\n\ttoolName: string;\n\tcontent: Array<{ type: string; text?: string }>;\n\tisError: boolean;\n}\n\nfunction isAssistant(m: AgentMessage): m is AgentMessage & AssistantLike {\n\treturn (m as { role?: string }).role === \"assistant\" && Array.isArray((m as AssistantLike).content);\n}\n\nfunction isToolResult(m: AgentMessage): m is AgentMessage & ToolResultLike {\n\treturn (m as { role?: string }).role === \"toolResult\";\n}\n\n/**\n * Return a message array with superseded `read` results stubbed out. Returns the\n * original array reference unchanged when there is nothing to evict, so a\n * no-op turn does not needlessly perturb the outgoing context.\n */\nexport function evictSupersededReads(messages: AgentMessage[], options: ContextGcOptions): AgentMessage[] {\n\t// Map each tool call id -> the resolved path it operated on, plus a friendly\n\t// display path (the original argument) for the stub text.\n\tconst resolvedPathByCallId = new Map<string, string>();\n\tconst displayPathByResolved = new Map<string, string>();\n\t// Bash calls carry a `command`, not a `path`; capture it so the eviction\n\t// pass can consult the side-effect guard by tool call id.\n\tconst bashCommandByCallId = new Map<string, string>();\n\tfor (const m of messages) {\n\t\tif (!isAssistant(m)) continue;\n\t\tfor (const block of m.content) {\n\t\t\tif (block.type !== \"toolCall\" || !block.id) continue;\n\t\t\tif (block.name === \"bash\") {\n\t\t\t\tconst cmd = block.arguments?.command;\n\t\t\t\tif (typeof cmd === \"string\" && cmd.length > 0) bashCommandByCallId.set(block.id, cmd);\n\t\t\t}\n\t\t\tconst rawPath = block.arguments?.path;\n\t\t\tif (typeof rawPath !== \"string\" || rawPath.length === 0) continue;\n\t\t\tconst resolved = resolve(options.cwd, rawPath);\n\t\t\tresolvedPathByCallId.set(block.id, resolved);\n\t\t\tif (!displayPathByResolved.has(resolved)) displayPathByResolved.set(resolved, rawPath);\n\t\t}\n\t}\n\n\t// First pass: per path, the index of the last read and the last successful\n\t// mutate. A read at index i is superseded when a later read (index > i) or a\n\t// later successful mutate (index > i) exists for the same path.\n\tconst lastReadIndex = new Map<string, number>();\n\tconst lastMutateIndex = new Map<string, number>();\n\tmessages.forEach((m, i) => {\n\t\tif (!isToolResult(m) || m.isError) return;\n\t\tconst path = resolvedPathByCallId.get(m.toolCallId);\n\t\tif (!path) return;\n\t\tif (READ_TOOLS.has(m.toolName)) {\n\t\t\tlastReadIndex.set(path, i);\n\t\t} else if (MUTATE_TOOLS.has(m.toolName)) {\n\t\t\tlastMutateIndex.set(path, i);\n\t\t}\n\t});\n\n\t// Second pass: build the output, stubbing evicted reads. Keep every other\n\t// message by reference; clone only the ones we rewrite so the persisted\n\t// history (which may share these objects) is never mutated.\n\tconst pressure = options.budgetPressure ?? 0;\n\tlet changed = false;\n\tconst out = messages.map((m, i) => {\n\t\tif (!isToolResult(m) || m.isError) return m;\n\n\t\t// Superseded-read eviction — always on, pressure-independent.\n\t\tif (READ_TOOLS.has(m.toolName)) {\n\t\t\tconst path = resolvedPathByCallId.get(m.toolCallId);\n\t\t\tif (!path) return m;\n\t\t\tconst laterRead = (lastReadIndex.get(path) ?? -1) > i;\n\t\t\tconst laterMutate = (lastMutateIndex.get(path) ?? -1) > i;\n\t\t\tif (!laterRead && !laterMutate) return m;\n\t\t\tchanged = true;\n\t\t\tconst display = displayPathByResolved.get(path) ?? path;\n\t\t\tconst reason = laterMutate ? \"the file was modified after this read\" : \"the file was read again later\";\n\t\t\treturn {\n\t\t\t\t...m,\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\ttext: `[Superseded read of ${display} elided to save context — ${reason}. Re-read the file if you need its current contents.]`,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t};\n\t\t}\n\n\t\t// Bash-output eviction — only under token-budget pressure (>= 0.6).\n\t\tif (pressure >= 0.6 && m.toolName === \"bash\") {\n\t\t\tconst cmd = bashCommandByCallId.get(m.toolCallId) ?? \"\";\n\t\t\tif (BASH_SIDE_EFFECT_PATTERN.test(cmd)) return m;\n\t\t\tconst textLen = m.content.filter((c) => c.type === \"text\").reduce((sum, c) => sum + (c.text?.length ?? 0), 0);\n\t\t\t// 60–79%: elide only large outputs. 80%+: elide unconditionally.\n\t\t\tconst shouldEvict = pressure >= 0.8 || textLen > BASH_EVICTION_CHAR_THRESHOLD;\n\t\t\tif (shouldEvict) {\n\t\t\t\tchanged = true;\n\t\t\t\treturn {\n\t\t\t\t\t...m,\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `[Bash output elided at ${Math.round(pressure * 100)}% token budget — re-run if needed.]`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\treturn m;\n\t});\n\n\treturn changed ? out : messages;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"context-gc.d.ts","sourceRoot":"","sources":["../../src/core/context-gc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAoBpE,MAAM,WAAW,gBAAgB;IAChC,sEAAsE;IACtE,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAoDD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,gBAAgB,GAAG,YAAY,EAAE,CAyGxG","sourcesContent":["/**\n * Context garbage collection.\n *\n * The whole transcript is re-sent on every turn, so a tool result that has\n * become useless keeps costing tokens for the rest of the session. This pass\n * runs on the OUTGOING message copy (via the agent's `transformContext` hook),\n * never on the persisted session, and replaces provably-dead `read` results\n * with a short stub. Nothing is lost: the persisted history is untouched and\n * the file is re-readable on demand.\n *\n * Current rule — superseded reads only (the read-then-edit / re-read pattern,\n * which dominates coding sessions):\n *\n * A `read` result for a path P is stale once, later in the transcript, the\n * same path is edited/written (its on-disk content changed) or read again\n * over an overlapping line range (the newer read reflects that region's newer\n * state). Reads of disjoint regions of the same file coexist — a later read of\n * lines 200-260 does not evict an earlier read of lines 1-40 — so paginating\n * through a large file never makes the model re-fetch a region it already has.\n * A read is only evicted when a *successful* later event supersedes it — so a\n * read whose edit failed (and which the model still needs to retry) is never\n * touched.\n *\n * Deliberately conservative: paths are matched after `path.resolve`, so two\n * different files never collide, and any ambiguity results in NOT evicting.\n *\n * Bash-output eviction is additionally gated on token-budget pressure\n * (`options.budgetPressure`, the fraction of the model's context window in\n * use). At 0 pressure — the default — behaviour is identical to read-only GC.\n * Bash output is not always recoverable, so it carries its own safeguards: a\n * command that looks side-effecting is never elided (re-running it, as the\n * stub invites, could repeat a destructive action), and below 80% pressure\n * only large outputs are elided.\n */\n\nimport { resolve } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\n\n/** Tool names whose result injects file contents into the transcript. */\nconst READ_TOOLS = new Set([\"read\"]);\n/** Tool names that change a file, making a prior read of that path stale. */\nconst MUTATE_TOOLS = new Set([\"edit\", \"write\"]);\n\n/**\n * Commands whose bash output must never be elided, because the stub invites\n * the model to re-run the command and re-running could repeat a destructive or\n * state-changing action. Tested against the whole command string, and\n * deliberately over-broad: a false positive merely keeps an output (safe),\n * while the common verbose *read* commands (cat/grep/ls/find/git log/diff,\n * test runners) intentionally do NOT match, so they stay evictable.\n */\nconst BASH_SIDE_EFFECT_PATTERN =\n\t/(\\b(write|insert|delete|update|curl|wget|psql|mysql|sqlite3|migrate|drop|truncate|rm|rmdir|mv|cp|dd|kill|tee|chmod|chown|ln|mkdir|touch)\\b|\\bgit\\s+(commit|push|reset|checkout|rebase|clean|apply|merge)\\b|\\bsed\\b[^|]*-i|>>?)/i;\n/** Below 80% pressure, only bash outputs larger than this (chars) are elided. */\nconst BASH_EVICTION_CHAR_THRESHOLD = 2000;\n\nexport interface ContextGcOptions {\n\t/** Working directory used to resolve relative tool path arguments. */\n\tcwd: string;\n\t/**\n\t * Token-budget pressure in [0, 1] — the fraction of the model's context\n\t * window currently in use. Absent or 0 reproduces read-only GC behaviour.\n\t * Bash-output eviction begins at 0.6 and becomes unconditional at 0.8.\n\t */\n\tbudgetPressure?: number;\n}\n\ninterface AssistantLike {\n\trole: \"assistant\";\n\tcontent: Array<{ type: string; id?: string; name?: string; arguments?: Record<string, unknown> }>;\n}\n\ninterface ToolResultLike {\n\trole: \"toolResult\";\n\ttoolCallId: string;\n\ttoolName: string;\n\tcontent: Array<{ type: string; text?: string }>;\n\tisError: boolean;\n}\n\nfunction isAssistant(m: AgentMessage): m is AgentMessage & AssistantLike {\n\treturn (m as { role?: string }).role === \"assistant\" && Array.isArray((m as AssistantLike).content);\n}\n\nfunction isToolResult(m: AgentMessage): m is AgentMessage & ToolResultLike {\n\treturn (m as { role?: string }).role === \"toolResult\";\n}\n\n/** Half-open line interval [start, end) a read call covers; end is exclusive. */\ninterface ReadRange {\n\tstart: number;\n\tend: number;\n}\n\n/**\n * Derive the line range a read call covers from its `offset`/`limit` args.\n * Missing offset means \"from line 1\"; missing limit means \"to end of file\"\n * (open-ended, so it overlaps any later read of the same file).\n */\nfunction readRangeFromArgs(args: Record<string, unknown> | undefined): ReadRange {\n\tconst offsetRaw = args?.offset;\n\tconst limitRaw = args?.limit;\n\tconst offset = typeof offsetRaw === \"number\" && Number.isFinite(offsetRaw) && offsetRaw > 0 ? offsetRaw : 1;\n\tconst end =\n\t\ttypeof limitRaw === \"number\" && Number.isFinite(limitRaw)\n\t\t\t? offset + Math.max(0, limitRaw)\n\t\t\t: Number.POSITIVE_INFINITY;\n\treturn { start: offset, end };\n}\n\n/** Whether two half-open line ranges intersect. */\nfunction rangesOverlap(a: ReadRange, b: ReadRange): boolean {\n\treturn a.start < b.end && b.start < a.end;\n}\n\nconst WHOLE_FILE_RANGE: ReadRange = { start: 1, end: Number.POSITIVE_INFINITY };\n\n/**\n * Return a message array with superseded `read` results stubbed out. Returns the\n * original array reference unchanged when there is nothing to evict, so a\n * no-op turn does not needlessly perturb the outgoing context.\n */\nexport function evictSupersededReads(messages: AgentMessage[], options: ContextGcOptions): AgentMessage[] {\n\t// Map each tool call id -> the resolved path it operated on, plus a friendly\n\t// display path (the original argument) for the stub text.\n\tconst resolvedPathByCallId = new Map<string, string>();\n\tconst displayPathByResolved = new Map<string, string>();\n\t// Bash calls carry a `command`, not a `path`; capture it so the eviction\n\t// pass can consult the side-effect guard by tool call id.\n\tconst bashCommandByCallId = new Map<string, string>();\n\t// Read calls carry `offset`/`limit`; capture the line range each covers so a\n\t// later read only supersedes an earlier one when their ranges overlap.\n\tconst rangeByCallId = new Map<string, ReadRange>();\n\tfor (const m of messages) {\n\t\tif (!isAssistant(m)) continue;\n\t\tfor (const block of m.content) {\n\t\t\tif (block.type !== \"toolCall\" || !block.id) continue;\n\t\t\tif (block.name === \"bash\") {\n\t\t\t\tconst cmd = block.arguments?.command;\n\t\t\t\tif (typeof cmd === \"string\" && cmd.length > 0) bashCommandByCallId.set(block.id, cmd);\n\t\t\t}\n\t\t\tconst rawPath = block.arguments?.path;\n\t\t\tif (typeof rawPath !== \"string\" || rawPath.length === 0) continue;\n\t\t\tconst resolved = resolve(options.cwd, rawPath);\n\t\t\tresolvedPathByCallId.set(block.id, resolved);\n\t\t\tif (block.name && READ_TOOLS.has(block.name)) rangeByCallId.set(block.id, readRangeFromArgs(block.arguments));\n\t\t\tif (!displayPathByResolved.has(resolved)) displayPathByResolved.set(resolved, rawPath);\n\t\t}\n\t}\n\n\t// First pass: per path, collect every successful read (index + line range)\n\t// and the last successful mutate. A read at index i is superseded when a later\n\t// successful mutate exists (index > i, whole file changed) or a later read of\n\t// an overlapping range exists (index > i) for the same path.\n\tconst readsByPath = new Map<string, Array<{ index: number; range: ReadRange }>>();\n\tconst lastMutateIndex = new Map<string, number>();\n\tmessages.forEach((m, i) => {\n\t\tif (!isToolResult(m) || m.isError) return;\n\t\tconst path = resolvedPathByCallId.get(m.toolCallId);\n\t\tif (!path) return;\n\t\tif (READ_TOOLS.has(m.toolName)) {\n\t\t\tconst range = rangeByCallId.get(m.toolCallId) ?? WHOLE_FILE_RANGE;\n\t\t\tconst list = readsByPath.get(path);\n\t\t\tif (list) list.push({ index: i, range });\n\t\t\telse readsByPath.set(path, [{ index: i, range }]);\n\t\t} else if (MUTATE_TOOLS.has(m.toolName)) {\n\t\t\tlastMutateIndex.set(path, i);\n\t\t}\n\t});\n\n\t// Second pass: build the output, stubbing evicted reads. Keep every other\n\t// message by reference; clone only the ones we rewrite so the persisted\n\t// history (which may share these objects) is never mutated.\n\tconst pressure = options.budgetPressure ?? 0;\n\tlet changed = false;\n\tconst out = messages.map((m, i) => {\n\t\tif (!isToolResult(m) || m.isError) return m;\n\n\t\t// Superseded-read eviction — always on, pressure-independent.\n\t\tif (READ_TOOLS.has(m.toolName)) {\n\t\t\tconst path = resolvedPathByCallId.get(m.toolCallId);\n\t\t\tif (!path) return m;\n\t\t\tconst range = rangeByCallId.get(m.toolCallId) ?? WHOLE_FILE_RANGE;\n\t\t\tconst laterMutate = (lastMutateIndex.get(path) ?? -1) > i;\n\t\t\tconst laterOverlappingRead = (readsByPath.get(path) ?? []).some(\n\t\t\t\t(r) => r.index > i && rangesOverlap(r.range, range),\n\t\t\t);\n\t\t\tif (!laterOverlappingRead && !laterMutate) return m;\n\t\t\tchanged = true;\n\t\t\tconst display = displayPathByResolved.get(path) ?? path;\n\t\t\tconst reason = laterMutate ? \"the file was modified after this read\" : \"the file was read again later\";\n\t\t\treturn {\n\t\t\t\t...m,\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\ttext: `[Superseded read of ${display} elided to save context — ${reason}. Re-read the file if you need its current contents.]`,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t};\n\t\t}\n\n\t\t// Bash-output eviction — only under token-budget pressure (>= 0.6).\n\t\tif (pressure >= 0.6 && m.toolName === \"bash\") {\n\t\t\tconst cmd = bashCommandByCallId.get(m.toolCallId) ?? \"\";\n\t\t\tif (BASH_SIDE_EFFECT_PATTERN.test(cmd)) return m;\n\t\t\tconst textLen = m.content.filter((c) => c.type === \"text\").reduce((sum, c) => sum + (c.text?.length ?? 0), 0);\n\t\t\t// 60–79%: elide only large outputs. 80%+: elide unconditionally.\n\t\t\tconst shouldEvict = pressure >= 0.8 || textLen > BASH_EVICTION_CHAR_THRESHOLD;\n\t\t\tif (shouldEvict) {\n\t\t\t\tchanged = true;\n\t\t\t\treturn {\n\t\t\t\t\t...m,\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `[Bash output elided at ${Math.round(pressure * 100)}% token budget — re-run if needed.]`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\treturn m;\n\t});\n\n\treturn changed ? out : messages;\n}\n"]}
|
package/dist/core/context-gc.js
CHANGED
|
@@ -13,10 +13,13 @@
|
|
|
13
13
|
*
|
|
14
14
|
* A `read` result for a path P is stale once, later in the transcript, the
|
|
15
15
|
* same path is edited/written (its on-disk content changed) or read again
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* over an overlapping line range (the newer read reflects that region's newer
|
|
17
|
+
* state). Reads of disjoint regions of the same file coexist — a later read of
|
|
18
|
+
* lines 200-260 does not evict an earlier read of lines 1-40 — so paginating
|
|
19
|
+
* through a large file never makes the model re-fetch a region it already has.
|
|
20
|
+
* A read is only evicted when a *successful* later event supersedes it — so a
|
|
21
|
+
* read whose edit failed (and which the model still needs to retry) is never
|
|
22
|
+
* touched.
|
|
20
23
|
*
|
|
21
24
|
* Deliberately conservative: paths are matched after `path.resolve`, so two
|
|
22
25
|
* different files never collide, and any ambiguity results in NOT evicting.
|
|
@@ -51,6 +54,25 @@ function isAssistant(m) {
|
|
|
51
54
|
function isToolResult(m) {
|
|
52
55
|
return m.role === "toolResult";
|
|
53
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Derive the line range a read call covers from its `offset`/`limit` args.
|
|
59
|
+
* Missing offset means "from line 1"; missing limit means "to end of file"
|
|
60
|
+
* (open-ended, so it overlaps any later read of the same file).
|
|
61
|
+
*/
|
|
62
|
+
function readRangeFromArgs(args) {
|
|
63
|
+
const offsetRaw = args?.offset;
|
|
64
|
+
const limitRaw = args?.limit;
|
|
65
|
+
const offset = typeof offsetRaw === "number" && Number.isFinite(offsetRaw) && offsetRaw > 0 ? offsetRaw : 1;
|
|
66
|
+
const end = typeof limitRaw === "number" && Number.isFinite(limitRaw)
|
|
67
|
+
? offset + Math.max(0, limitRaw)
|
|
68
|
+
: Number.POSITIVE_INFINITY;
|
|
69
|
+
return { start: offset, end };
|
|
70
|
+
}
|
|
71
|
+
/** Whether two half-open line ranges intersect. */
|
|
72
|
+
function rangesOverlap(a, b) {
|
|
73
|
+
return a.start < b.end && b.start < a.end;
|
|
74
|
+
}
|
|
75
|
+
const WHOLE_FILE_RANGE = { start: 1, end: Number.POSITIVE_INFINITY };
|
|
54
76
|
/**
|
|
55
77
|
* Return a message array with superseded `read` results stubbed out. Returns the
|
|
56
78
|
* original array reference unchanged when there is nothing to evict, so a
|
|
@@ -64,6 +86,9 @@ export function evictSupersededReads(messages, options) {
|
|
|
64
86
|
// Bash calls carry a `command`, not a `path`; capture it so the eviction
|
|
65
87
|
// pass can consult the side-effect guard by tool call id.
|
|
66
88
|
const bashCommandByCallId = new Map();
|
|
89
|
+
// Read calls carry `offset`/`limit`; capture the line range each covers so a
|
|
90
|
+
// later read only supersedes an earlier one when their ranges overlap.
|
|
91
|
+
const rangeByCallId = new Map();
|
|
67
92
|
for (const m of messages) {
|
|
68
93
|
if (!isAssistant(m))
|
|
69
94
|
continue;
|
|
@@ -80,14 +105,17 @@ export function evictSupersededReads(messages, options) {
|
|
|
80
105
|
continue;
|
|
81
106
|
const resolved = resolve(options.cwd, rawPath);
|
|
82
107
|
resolvedPathByCallId.set(block.id, resolved);
|
|
108
|
+
if (block.name && READ_TOOLS.has(block.name))
|
|
109
|
+
rangeByCallId.set(block.id, readRangeFromArgs(block.arguments));
|
|
83
110
|
if (!displayPathByResolved.has(resolved))
|
|
84
111
|
displayPathByResolved.set(resolved, rawPath);
|
|
85
112
|
}
|
|
86
113
|
}
|
|
87
|
-
// First pass: per path,
|
|
88
|
-
// mutate. A read at index i is superseded when a later
|
|
89
|
-
//
|
|
90
|
-
|
|
114
|
+
// First pass: per path, collect every successful read (index + line range)
|
|
115
|
+
// and the last successful mutate. A read at index i is superseded when a later
|
|
116
|
+
// successful mutate exists (index > i, whole file changed) or a later read of
|
|
117
|
+
// an overlapping range exists (index > i) for the same path.
|
|
118
|
+
const readsByPath = new Map();
|
|
91
119
|
const lastMutateIndex = new Map();
|
|
92
120
|
messages.forEach((m, i) => {
|
|
93
121
|
if (!isToolResult(m) || m.isError)
|
|
@@ -96,7 +124,12 @@ export function evictSupersededReads(messages, options) {
|
|
|
96
124
|
if (!path)
|
|
97
125
|
return;
|
|
98
126
|
if (READ_TOOLS.has(m.toolName)) {
|
|
99
|
-
|
|
127
|
+
const range = rangeByCallId.get(m.toolCallId) ?? WHOLE_FILE_RANGE;
|
|
128
|
+
const list = readsByPath.get(path);
|
|
129
|
+
if (list)
|
|
130
|
+
list.push({ index: i, range });
|
|
131
|
+
else
|
|
132
|
+
readsByPath.set(path, [{ index: i, range }]);
|
|
100
133
|
}
|
|
101
134
|
else if (MUTATE_TOOLS.has(m.toolName)) {
|
|
102
135
|
lastMutateIndex.set(path, i);
|
|
@@ -115,9 +148,10 @@ export function evictSupersededReads(messages, options) {
|
|
|
115
148
|
const path = resolvedPathByCallId.get(m.toolCallId);
|
|
116
149
|
if (!path)
|
|
117
150
|
return m;
|
|
118
|
-
const
|
|
151
|
+
const range = rangeByCallId.get(m.toolCallId) ?? WHOLE_FILE_RANGE;
|
|
119
152
|
const laterMutate = (lastMutateIndex.get(path) ?? -1) > i;
|
|
120
|
-
|
|
153
|
+
const laterOverlappingRead = (readsByPath.get(path) ?? []).some((r) => r.index > i && rangesOverlap(r.range, range));
|
|
154
|
+
if (!laterOverlappingRead && !laterMutate)
|
|
121
155
|
return m;
|
|
122
156
|
changed = true;
|
|
123
157
|
const display = displayPathByResolved.get(path) ?? path;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-gc.js","sourceRoot":"","sources":["../../src/core/context-gc.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Context garbage collection.\n *\n * The whole transcript is re-sent on every turn, so a tool result that has\n * become useless keeps costing tokens for the rest of the session. This pass\n * runs on the OUTGOING message copy (via the agent's `transformContext` hook),\n * never on the persisted session, and replaces provably-dead `read` results\n * with a short stub. Nothing is lost: the persisted history is untouched and\n * the file is re-readable on demand.\n *\n * Current rule — superseded reads only (the read-then-edit / re-read pattern,\n * which dominates coding sessions):\n *\n * A `read` result for a path P is stale once, later in the transcript, the\n * same path is edited/written (its on-disk content changed) or read again\n * (the newer read reflects newer state). The most recent read of each path is\n * always kept, and a read is only evicted when a *successful* later event\n * supersedes it — so a read whose edit failed (and which the model still needs\n * to retry) is never touched.\n *\n * Deliberately conservative: paths are matched after `path.resolve`, so two\n * different files never collide, and any ambiguity results in NOT evicting.\n *\n * Bash-output eviction is additionally gated on token-budget pressure\n * (`options.budgetPressure`, the fraction of the model's context window in\n * use). At 0 pressure — the default — behaviour is identical to read-only GC.\n * Bash output is not always recoverable, so it carries its own safeguards: a\n * command that looks side-effecting is never elided (re-running it, as the\n * stub invites, could repeat a destructive action), and below 80% pressure\n * only large outputs are elided.\n */\n\nimport { resolve } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\n\n/** Tool names whose result injects file contents into the transcript. */\nconst READ_TOOLS = new Set([\"read\"]);\n/** Tool names that change a file, making a prior read of that path stale. */\nconst MUTATE_TOOLS = new Set([\"edit\", \"write\"]);\n\n/**\n * Commands whose bash output must never be elided, because the stub invites\n * the model to re-run the command and re-running could repeat a destructive or\n * state-changing action. Tested against the whole command string, and\n * deliberately over-broad: a false positive merely keeps an output (safe),\n * while the common verbose *read* commands (cat/grep/ls/find/git log/diff,\n * test runners) intentionally do NOT match, so they stay evictable.\n */\nconst BASH_SIDE_EFFECT_PATTERN =\n\t/(\\b(write|insert|delete|update|curl|wget|psql|mysql|sqlite3|migrate|drop|truncate|rm|rmdir|mv|cp|dd|kill|tee|chmod|chown|ln|mkdir|touch)\\b|\\bgit\\s+(commit|push|reset|checkout|rebase|clean|apply|merge)\\b|\\bsed\\b[^|]*-i|>>?)/i;\n/** Below 80% pressure, only bash outputs larger than this (chars) are elided. */\nconst BASH_EVICTION_CHAR_THRESHOLD = 2000;\n\nexport interface ContextGcOptions {\n\t/** Working directory used to resolve relative tool path arguments. */\n\tcwd: string;\n\t/**\n\t * Token-budget pressure in [0, 1] — the fraction of the model's context\n\t * window currently in use. Absent or 0 reproduces read-only GC behaviour.\n\t * Bash-output eviction begins at 0.6 and becomes unconditional at 0.8.\n\t */\n\tbudgetPressure?: number;\n}\n\ninterface AssistantLike {\n\trole: \"assistant\";\n\tcontent: Array<{ type: string; id?: string; name?: string; arguments?: Record<string, unknown> }>;\n}\n\ninterface ToolResultLike {\n\trole: \"toolResult\";\n\ttoolCallId: string;\n\ttoolName: string;\n\tcontent: Array<{ type: string; text?: string }>;\n\tisError: boolean;\n}\n\nfunction isAssistant(m: AgentMessage): m is AgentMessage & AssistantLike {\n\treturn (m as { role?: string }).role === \"assistant\" && Array.isArray((m as AssistantLike).content);\n}\n\nfunction isToolResult(m: AgentMessage): m is AgentMessage & ToolResultLike {\n\treturn (m as { role?: string }).role === \"toolResult\";\n}\n\n/**\n * Return a message array with superseded `read` results stubbed out. Returns the\n * original array reference unchanged when there is nothing to evict, so a\n * no-op turn does not needlessly perturb the outgoing context.\n */\nexport function evictSupersededReads(messages: AgentMessage[], options: ContextGcOptions): AgentMessage[] {\n\t// Map each tool call id -> the resolved path it operated on, plus a friendly\n\t// display path (the original argument) for the stub text.\n\tconst resolvedPathByCallId = new Map<string, string>();\n\tconst displayPathByResolved = new Map<string, string>();\n\t// Bash calls carry a `command`, not a `path`; capture it so the eviction\n\t// pass can consult the side-effect guard by tool call id.\n\tconst bashCommandByCallId = new Map<string, string>();\n\tfor (const m of messages) {\n\t\tif (!isAssistant(m)) continue;\n\t\tfor (const block of m.content) {\n\t\t\tif (block.type !== \"toolCall\" || !block.id) continue;\n\t\t\tif (block.name === \"bash\") {\n\t\t\t\tconst cmd = block.arguments?.command;\n\t\t\t\tif (typeof cmd === \"string\" && cmd.length > 0) bashCommandByCallId.set(block.id, cmd);\n\t\t\t}\n\t\t\tconst rawPath = block.arguments?.path;\n\t\t\tif (typeof rawPath !== \"string\" || rawPath.length === 0) continue;\n\t\t\tconst resolved = resolve(options.cwd, rawPath);\n\t\t\tresolvedPathByCallId.set(block.id, resolved);\n\t\t\tif (!displayPathByResolved.has(resolved)) displayPathByResolved.set(resolved, rawPath);\n\t\t}\n\t}\n\n\t// First pass: per path, the index of the last read and the last successful\n\t// mutate. A read at index i is superseded when a later read (index > i) or a\n\t// later successful mutate (index > i) exists for the same path.\n\tconst lastReadIndex = new Map<string, number>();\n\tconst lastMutateIndex = new Map<string, number>();\n\tmessages.forEach((m, i) => {\n\t\tif (!isToolResult(m) || m.isError) return;\n\t\tconst path = resolvedPathByCallId.get(m.toolCallId);\n\t\tif (!path) return;\n\t\tif (READ_TOOLS.has(m.toolName)) {\n\t\t\tlastReadIndex.set(path, i);\n\t\t} else if (MUTATE_TOOLS.has(m.toolName)) {\n\t\t\tlastMutateIndex.set(path, i);\n\t\t}\n\t});\n\n\t// Second pass: build the output, stubbing evicted reads. Keep every other\n\t// message by reference; clone only the ones we rewrite so the persisted\n\t// history (which may share these objects) is never mutated.\n\tconst pressure = options.budgetPressure ?? 0;\n\tlet changed = false;\n\tconst out = messages.map((m, i) => {\n\t\tif (!isToolResult(m) || m.isError) return m;\n\n\t\t// Superseded-read eviction — always on, pressure-independent.\n\t\tif (READ_TOOLS.has(m.toolName)) {\n\t\t\tconst path = resolvedPathByCallId.get(m.toolCallId);\n\t\t\tif (!path) return m;\n\t\t\tconst laterRead = (lastReadIndex.get(path) ?? -1) > i;\n\t\t\tconst laterMutate = (lastMutateIndex.get(path) ?? -1) > i;\n\t\t\tif (!laterRead && !laterMutate) return m;\n\t\t\tchanged = true;\n\t\t\tconst display = displayPathByResolved.get(path) ?? path;\n\t\t\tconst reason = laterMutate ? \"the file was modified after this read\" : \"the file was read again later\";\n\t\t\treturn {\n\t\t\t\t...m,\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\ttext: `[Superseded read of ${display} elided to save context — ${reason}. Re-read the file if you need its current contents.]`,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t};\n\t\t}\n\n\t\t// Bash-output eviction — only under token-budget pressure (>= 0.6).\n\t\tif (pressure >= 0.6 && m.toolName === \"bash\") {\n\t\t\tconst cmd = bashCommandByCallId.get(m.toolCallId) ?? \"\";\n\t\t\tif (BASH_SIDE_EFFECT_PATTERN.test(cmd)) return m;\n\t\t\tconst textLen = m.content.filter((c) => c.type === \"text\").reduce((sum, c) => sum + (c.text?.length ?? 0), 0);\n\t\t\t// 60–79%: elide only large outputs. 80%+: elide unconditionally.\n\t\t\tconst shouldEvict = pressure >= 0.8 || textLen > BASH_EVICTION_CHAR_THRESHOLD;\n\t\t\tif (shouldEvict) {\n\t\t\t\tchanged = true;\n\t\t\t\treturn {\n\t\t\t\t\t...m,\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `[Bash output elided at ${Math.round(pressure * 100)}% token budget — re-run if needed.]`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\treturn m;\n\t});\n\n\treturn changed ? out : messages;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"context-gc.js","sourceRoot":"","sources":["../../src/core/context-gc.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Context garbage collection.\n *\n * The whole transcript is re-sent on every turn, so a tool result that has\n * become useless keeps costing tokens for the rest of the session. This pass\n * runs on the OUTGOING message copy (via the agent's `transformContext` hook),\n * never on the persisted session, and replaces provably-dead `read` results\n * with a short stub. Nothing is lost: the persisted history is untouched and\n * the file is re-readable on demand.\n *\n * Current rule — superseded reads only (the read-then-edit / re-read pattern,\n * which dominates coding sessions):\n *\n * A `read` result for a path P is stale once, later in the transcript, the\n * same path is edited/written (its on-disk content changed) or read again\n * over an overlapping line range (the newer read reflects that region's newer\n * state). Reads of disjoint regions of the same file coexist — a later read of\n * lines 200-260 does not evict an earlier read of lines 1-40 — so paginating\n * through a large file never makes the model re-fetch a region it already has.\n * A read is only evicted when a *successful* later event supersedes it — so a\n * read whose edit failed (and which the model still needs to retry) is never\n * touched.\n *\n * Deliberately conservative: paths are matched after `path.resolve`, so two\n * different files never collide, and any ambiguity results in NOT evicting.\n *\n * Bash-output eviction is additionally gated on token-budget pressure\n * (`options.budgetPressure`, the fraction of the model's context window in\n * use). At 0 pressure — the default — behaviour is identical to read-only GC.\n * Bash output is not always recoverable, so it carries its own safeguards: a\n * command that looks side-effecting is never elided (re-running it, as the\n * stub invites, could repeat a destructive action), and below 80% pressure\n * only large outputs are elided.\n */\n\nimport { resolve } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\n\n/** Tool names whose result injects file contents into the transcript. */\nconst READ_TOOLS = new Set([\"read\"]);\n/** Tool names that change a file, making a prior read of that path stale. */\nconst MUTATE_TOOLS = new Set([\"edit\", \"write\"]);\n\n/**\n * Commands whose bash output must never be elided, because the stub invites\n * the model to re-run the command and re-running could repeat a destructive or\n * state-changing action. Tested against the whole command string, and\n * deliberately over-broad: a false positive merely keeps an output (safe),\n * while the common verbose *read* commands (cat/grep/ls/find/git log/diff,\n * test runners) intentionally do NOT match, so they stay evictable.\n */\nconst BASH_SIDE_EFFECT_PATTERN =\n\t/(\\b(write|insert|delete|update|curl|wget|psql|mysql|sqlite3|migrate|drop|truncate|rm|rmdir|mv|cp|dd|kill|tee|chmod|chown|ln|mkdir|touch)\\b|\\bgit\\s+(commit|push|reset|checkout|rebase|clean|apply|merge)\\b|\\bsed\\b[^|]*-i|>>?)/i;\n/** Below 80% pressure, only bash outputs larger than this (chars) are elided. */\nconst BASH_EVICTION_CHAR_THRESHOLD = 2000;\n\nexport interface ContextGcOptions {\n\t/** Working directory used to resolve relative tool path arguments. */\n\tcwd: string;\n\t/**\n\t * Token-budget pressure in [0, 1] — the fraction of the model's context\n\t * window currently in use. Absent or 0 reproduces read-only GC behaviour.\n\t * Bash-output eviction begins at 0.6 and becomes unconditional at 0.8.\n\t */\n\tbudgetPressure?: number;\n}\n\ninterface AssistantLike {\n\trole: \"assistant\";\n\tcontent: Array<{ type: string; id?: string; name?: string; arguments?: Record<string, unknown> }>;\n}\n\ninterface ToolResultLike {\n\trole: \"toolResult\";\n\ttoolCallId: string;\n\ttoolName: string;\n\tcontent: Array<{ type: string; text?: string }>;\n\tisError: boolean;\n}\n\nfunction isAssistant(m: AgentMessage): m is AgentMessage & AssistantLike {\n\treturn (m as { role?: string }).role === \"assistant\" && Array.isArray((m as AssistantLike).content);\n}\n\nfunction isToolResult(m: AgentMessage): m is AgentMessage & ToolResultLike {\n\treturn (m as { role?: string }).role === \"toolResult\";\n}\n\n/** Half-open line interval [start, end) a read call covers; end is exclusive. */\ninterface ReadRange {\n\tstart: number;\n\tend: number;\n}\n\n/**\n * Derive the line range a read call covers from its `offset`/`limit` args.\n * Missing offset means \"from line 1\"; missing limit means \"to end of file\"\n * (open-ended, so it overlaps any later read of the same file).\n */\nfunction readRangeFromArgs(args: Record<string, unknown> | undefined): ReadRange {\n\tconst offsetRaw = args?.offset;\n\tconst limitRaw = args?.limit;\n\tconst offset = typeof offsetRaw === \"number\" && Number.isFinite(offsetRaw) && offsetRaw > 0 ? offsetRaw : 1;\n\tconst end =\n\t\ttypeof limitRaw === \"number\" && Number.isFinite(limitRaw)\n\t\t\t? offset + Math.max(0, limitRaw)\n\t\t\t: Number.POSITIVE_INFINITY;\n\treturn { start: offset, end };\n}\n\n/** Whether two half-open line ranges intersect. */\nfunction rangesOverlap(a: ReadRange, b: ReadRange): boolean {\n\treturn a.start < b.end && b.start < a.end;\n}\n\nconst WHOLE_FILE_RANGE: ReadRange = { start: 1, end: Number.POSITIVE_INFINITY };\n\n/**\n * Return a message array with superseded `read` results stubbed out. Returns the\n * original array reference unchanged when there is nothing to evict, so a\n * no-op turn does not needlessly perturb the outgoing context.\n */\nexport function evictSupersededReads(messages: AgentMessage[], options: ContextGcOptions): AgentMessage[] {\n\t// Map each tool call id -> the resolved path it operated on, plus a friendly\n\t// display path (the original argument) for the stub text.\n\tconst resolvedPathByCallId = new Map<string, string>();\n\tconst displayPathByResolved = new Map<string, string>();\n\t// Bash calls carry a `command`, not a `path`; capture it so the eviction\n\t// pass can consult the side-effect guard by tool call id.\n\tconst bashCommandByCallId = new Map<string, string>();\n\t// Read calls carry `offset`/`limit`; capture the line range each covers so a\n\t// later read only supersedes an earlier one when their ranges overlap.\n\tconst rangeByCallId = new Map<string, ReadRange>();\n\tfor (const m of messages) {\n\t\tif (!isAssistant(m)) continue;\n\t\tfor (const block of m.content) {\n\t\t\tif (block.type !== \"toolCall\" || !block.id) continue;\n\t\t\tif (block.name === \"bash\") {\n\t\t\t\tconst cmd = block.arguments?.command;\n\t\t\t\tif (typeof cmd === \"string\" && cmd.length > 0) bashCommandByCallId.set(block.id, cmd);\n\t\t\t}\n\t\t\tconst rawPath = block.arguments?.path;\n\t\t\tif (typeof rawPath !== \"string\" || rawPath.length === 0) continue;\n\t\t\tconst resolved = resolve(options.cwd, rawPath);\n\t\t\tresolvedPathByCallId.set(block.id, resolved);\n\t\t\tif (block.name && READ_TOOLS.has(block.name)) rangeByCallId.set(block.id, readRangeFromArgs(block.arguments));\n\t\t\tif (!displayPathByResolved.has(resolved)) displayPathByResolved.set(resolved, rawPath);\n\t\t}\n\t}\n\n\t// First pass: per path, collect every successful read (index + line range)\n\t// and the last successful mutate. A read at index i is superseded when a later\n\t// successful mutate exists (index > i, whole file changed) or a later read of\n\t// an overlapping range exists (index > i) for the same path.\n\tconst readsByPath = new Map<string, Array<{ index: number; range: ReadRange }>>();\n\tconst lastMutateIndex = new Map<string, number>();\n\tmessages.forEach((m, i) => {\n\t\tif (!isToolResult(m) || m.isError) return;\n\t\tconst path = resolvedPathByCallId.get(m.toolCallId);\n\t\tif (!path) return;\n\t\tif (READ_TOOLS.has(m.toolName)) {\n\t\t\tconst range = rangeByCallId.get(m.toolCallId) ?? WHOLE_FILE_RANGE;\n\t\t\tconst list = readsByPath.get(path);\n\t\t\tif (list) list.push({ index: i, range });\n\t\t\telse readsByPath.set(path, [{ index: i, range }]);\n\t\t} else if (MUTATE_TOOLS.has(m.toolName)) {\n\t\t\tlastMutateIndex.set(path, i);\n\t\t}\n\t});\n\n\t// Second pass: build the output, stubbing evicted reads. Keep every other\n\t// message by reference; clone only the ones we rewrite so the persisted\n\t// history (which may share these objects) is never mutated.\n\tconst pressure = options.budgetPressure ?? 0;\n\tlet changed = false;\n\tconst out = messages.map((m, i) => {\n\t\tif (!isToolResult(m) || m.isError) return m;\n\n\t\t// Superseded-read eviction — always on, pressure-independent.\n\t\tif (READ_TOOLS.has(m.toolName)) {\n\t\t\tconst path = resolvedPathByCallId.get(m.toolCallId);\n\t\t\tif (!path) return m;\n\t\t\tconst range = rangeByCallId.get(m.toolCallId) ?? WHOLE_FILE_RANGE;\n\t\t\tconst laterMutate = (lastMutateIndex.get(path) ?? -1) > i;\n\t\t\tconst laterOverlappingRead = (readsByPath.get(path) ?? []).some(\n\t\t\t\t(r) => r.index > i && rangesOverlap(r.range, range),\n\t\t\t);\n\t\t\tif (!laterOverlappingRead && !laterMutate) return m;\n\t\t\tchanged = true;\n\t\t\tconst display = displayPathByResolved.get(path) ?? path;\n\t\t\tconst reason = laterMutate ? \"the file was modified after this read\" : \"the file was read again later\";\n\t\t\treturn {\n\t\t\t\t...m,\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\ttext: `[Superseded read of ${display} elided to save context — ${reason}. Re-read the file if you need its current contents.]`,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t};\n\t\t}\n\n\t\t// Bash-output eviction — only under token-budget pressure (>= 0.6).\n\t\tif (pressure >= 0.6 && m.toolName === \"bash\") {\n\t\t\tconst cmd = bashCommandByCallId.get(m.toolCallId) ?? \"\";\n\t\t\tif (BASH_SIDE_EFFECT_PATTERN.test(cmd)) return m;\n\t\t\tconst textLen = m.content.filter((c) => c.type === \"text\").reduce((sum, c) => sum + (c.text?.length ?? 0), 0);\n\t\t\t// 60–79%: elide only large outputs. 80%+: elide unconditionally.\n\t\t\tconst shouldEvict = pressure >= 0.8 || textLen > BASH_EVICTION_CHAR_THRESHOLD;\n\t\t\tif (shouldEvict) {\n\t\t\t\tchanged = true;\n\t\t\t\treturn {\n\t\t\t\t\t...m,\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `[Bash output elided at ${Math.round(pressure * 100)}% token budget — re-run if needed.]`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\treturn m;\n\t});\n\n\treturn changed ? out : messages;\n}\n"]}
|
package/dist/core/sdk.d.ts
CHANGED
|
@@ -71,12 +71,11 @@ export interface CreateAgentSessionOptions {
|
|
|
71
71
|
*/
|
|
72
72
|
enableFileTools?: boolean;
|
|
73
73
|
/**
|
|
74
|
-
* Enable the built-in `search` tool (ranked
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* retrieval instead of erroring.
|
|
74
|
+
* Enable the semantic index layer for the built-in `search` tool (ranked
|
|
75
|
+
* lexical + semantic retrieval, rank-fused). The search tool is active by
|
|
76
|
+
* default; this flag only toggles the optional embedding index. When false,
|
|
77
|
+
* search degrades to lexical-only. Ignored when an explicit `tools` allowlist
|
|
78
|
+
* is provided (list it there instead).
|
|
80
79
|
*/
|
|
81
80
|
enableEmbsearchTools?: boolean;
|
|
82
81
|
/** Custom tools to register (in addition to built-in tools). */
|
package/dist/core/sdk.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../src/core/sdk.ts"],"names":[],"mappings":"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","sourcesContent":["import { join } from \"node:path\";\nimport {\n\tAgent,\n\ttype AgentMessage,\n\ttype AgentTool,\n\tconvertToLlm,\n\tcreateBackgroundPlaceholderText,\n\tcreateBackgroundTaskMessage,\n\testimateContextTokens,\n\ttype ThinkingLevel,\n} from \"@kolisachint/hoocode-agent-core\";\nimport { clampThinkingLevel, type Message, type Model, streamSimple } from \"@kolisachint/hoocode-ai\";\nimport { getAgentDir } from \"../config.js\";\nimport { readConfig as readHooConfig } from \"../extensions/core/config.js\";\nimport { AgentSession } from \"./agent-session.js\";\nimport { formatNoModelsAvailableMessage } from \"./auth-guidance.js\";\nimport { AuthStorage } from \"./auth-storage.js\";\nimport { evictSupersededReads } from \"./context-gc.js\";\nimport { DEFAULT_THINKING_LEVEL } from \"./defaults.js\";\nimport type { ExtensionRunner, LoadExtensionsResult, SessionStartEvent, ToolDefinition } from \"./extensions/index.js\";\nimport { ModelRegistry } from \"./model-registry.js\";\nimport { defaultModelPerProvider, findInitialModel } from \"./model-resolver.js\";\nimport type { ResourceLoader } from \"./resource-loader.js\";\nimport { DefaultResourceLoader } from \"./resource-loader.js\";\nimport { getDefaultSessionDir, SessionManager } from \"./session-manager.js\";\nimport { SettingsManager } from \"./settings-manager.js\";\nimport { peekSubagentPool } from \"./subagent-pool-instance.js\";\nimport { isInstallTelemetryEnabled } from \"./telemetry.js\";\nimport { time } from \"./timings.js\";\nimport {\n\tcreateBashTool,\n\tcreateCodingTools,\n\tcreateEditTool,\n\tcreateFindTool,\n\tcreateGrepTool,\n\tcreateLsTool,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateWriteTool,\n\ttype ToolName,\n\twithFileMutationQueue,\n} from \"./tools/index.js\";\n\nexport interface CreateAgentSessionOptions {\n\t/** Working directory for project-local discovery. Default: process.cwd() */\n\tcwd?: string;\n\t/** Global config directory. Default: ~/.hoocode/agent */\n\tagentDir?: string;\n\n\t/** Auth storage for credentials. Default: AuthStorage.create(agentDir/auth.json) */\n\tauthStorage?: AuthStorage;\n\t/** Model registry. Default: ModelRegistry.create(authStorage, agentDir/models.json) */\n\tmodelRegistry?: ModelRegistry;\n\n\t/** Model to use. Default: from settings, else first available */\n\tmodel?: Model<any>;\n\t/** Thinking level. Default: from settings, else 'medium' (clamped to model capabilities) */\n\tthinkingLevel?: ThinkingLevel;\n\t/** Models available for cycling (Ctrl+P in interactive mode) */\n\tscopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;\n\n\t/**\n\t * Optional default tool suppression mode when no explicit allowlist is provided.\n\t *\n\t * - \"all\": start with no tools enabled\n\t * - \"builtin\": disable the default built-in tools (read, bash, edit, write)\n\t * but keep extension/custom tools enabled\n\t */\n\tnoTools?: \"all\" | \"builtin\";\n\t/**\n\t * Optional allowlist of tool names.\n\t *\n\t * When omitted, hoocode enables the default built-in tools (read, bash, edit, write)\n\t * and leaves extension/custom tools enabled unless `noTools` changes that default.\n\t * When provided, only the listed tool names are enabled.\n\t */\n\ttools?: string[];\n\t/**\n\t * Optional denylist of tool names, subtracted from whatever set is otherwise\n\t * enabled (allowlist or default). Applied to built-in, extension, and custom tools.\n\t */\n\tdisallowedTools?: string[];\n\t/**\n\t * Enable the built-in `webfetch` + `websearch` tools, which are defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list `webfetch`/`websearch` there instead). Network access is still gated\n\t * per call and filtered by `.webtoolsignore`.\n\t */\n\tenableWebTools?: boolean;\n\t/**\n\t * Enable the built-in `browser_run` + `browser_continue` tools, which drive the\n\t * `browsertools` deterministic browser engine (parent-in-the-loop). Defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list them there instead).\n\t */\n\tenableBrowserTools?: boolean;\n\t/**\n\t * Enable the built-in document tools — `DocRead`/`DocEdit`/`DocWrite` (extract\n\t * and lossless id-based editing) plus `DocScan`/`DocGrep`/`DocPeek` (the\n\t * token-sensitive discovery loop: outline, search, partial read). Defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list them there instead). They shell out to the `filetools` binary to\n\t * losslessly extract/edit structured/binary documents.\n\t */\n\tenableFileTools?: boolean;\n\t/**\n\t * Enable the built-in `search` tool (ranked lexical + semantic retrieval,\n\t * rank-fused; the semantic side uses a local embedding index via the\n\t * `embsearch` binary). Defined but inactive by default. Ignored when an\n\t * explicit `tools` allowlist is provided (list it there instead). While the\n\t * per-session index service is unavailable the tool degrades to lexical\n\t * retrieval instead of erroring.\n\t */\n\tenableEmbsearchTools?: boolean;\n\t/** Custom tools to register (in addition to built-in tools). */\n\tcustomTools?: ToolDefinition[];\n\t/**\n\t * Replace the built-in base tools entirely (the light preset uses this to\n\t * swap in short-schema read/write/edit/bash variants). Keys become the\n\t * default active tool set when no explicit `tools` allowlist is provided.\n\t */\n\tbaseToolsOverride?: Record<string, AgentTool>;\n\n\t/** Resource loader. When omitted, DefaultResourceLoader is used. */\n\tresourceLoader?: ResourceLoader;\n\n\t/** Session manager. Default: SessionManager.create(cwd) */\n\tsessionManager?: SessionManager;\n\n\t/** Settings manager. Default: SettingsManager.create(cwd, agentDir) */\n\tsettingsManager?: SettingsManager;\n\t/** Session start event metadata for extension runtime startup. */\n\tsessionStartEvent?: SessionStartEvent;\n}\n\n/** Result from createAgentSession */\nexport interface CreateAgentSessionResult {\n\t/** The created session */\n\tsession: AgentSession;\n\t/** Extensions result (for UI context setup in interactive mode) */\n\textensionsResult: LoadExtensionsResult;\n\t/** Warning if session was restored with a different model than saved */\n\tmodelFallbackMessage?: string;\n}\n\n// Re-exports\n\nexport type { AgentDefinition, AgentSource } from \"./agent-frontmatter.js\";\nexport { AgentRegistry, formatAgentsForPrompt, loadAgentRegistry } from \"./agent-registry.js\";\nexport * from \"./agent-session-runtime.js\";\nexport type {\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tExtensionFactory,\n\tSlashCommandInfo,\n\tSlashCommandSource,\n\tToolDefinition,\n} from \"./extensions/index.js\";\nexport type { PromptTemplate } from \"./prompt-templates.js\";\nexport type { Skill } from \"./skills.js\";\nexport type { Tool } from \"./tools/index.js\";\n\nexport {\n\twithFileMutationQueue,\n\t// Tool factories (for custom cwd)\n\tcreateCodingTools,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateBashTool,\n\tcreateEditTool,\n\tcreateWriteTool,\n\tcreateGrepTool,\n\tcreateFindTool,\n\tcreateLsTool,\n};\n\n// Helper Functions\n\nfunction getDefaultAgentDir(): string {\n\treturn getAgentDir();\n}\n\nfunction getAttributionHeaders(\n\tmodel: Model<any>,\n\tsettingsManager: SettingsManager,\n): Record<string, string> | undefined {\n\tif (!isInstallTelemetryEnabled(settingsManager)) {\n\t\treturn undefined;\n\t}\n\n\tif (model.provider === \"openrouter\" || model.baseUrl.includes(\"openrouter.ai\")) {\n\t\treturn {\n\t\t\t\"HTTP-Referer\": \"https://github.com/kolisachint/hoocode\",\n\t\t\t\"X-OpenRouter-Title\": \"hoocode\",\n\t\t\t\"X-OpenRouter-Categories\": \"cli-agent\",\n\t\t};\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Create an AgentSession with the specified options.\n *\n * @example\n * ```typescript\n * // Minimal - uses defaults\n * const { session } = await createAgentSession();\n *\n * // With explicit model\n * import { getModel } from '@kolisachint/hoocode-ai';\n * const { session } = await createAgentSession({\n * model: getModel('anthropic', 'claude-opus-4-5'),\n * thinkingLevel: 'high',\n * });\n *\n * // Continue previous session\n * const { session, modelFallbackMessage } = await createAgentSession({\n * continueSession: true,\n * });\n *\n * // Full control\n * const loader = new DefaultResourceLoader({\n * cwd: process.cwd(),\n * agentDir: getAgentDir(),\n * settingsManager: SettingsManager.create(),\n * });\n * await loader.reload();\n * const { session } = await createAgentSession({\n * model: myModel,\n * tools: [readTool, bashTool],\n * resourceLoader: loader,\n * sessionManager: SessionManager.inMemory(),\n * });\n * ```\n */\nexport async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {\n\tconst cwd = options.cwd ?? options.sessionManager?.getCwd() ?? process.cwd();\n\tconst agentDir = options.agentDir ?? getDefaultAgentDir();\n\tlet resourceLoader = options.resourceLoader;\n\n\t// Use provided or create AuthStorage and ModelRegistry\n\tconst authPath = options.agentDir ? join(agentDir, \"auth.json\") : undefined;\n\tconst modelsPath = options.agentDir ? join(agentDir, \"models.json\") : undefined;\n\tconst authStorage = options.authStorage ?? AuthStorage.create(authPath);\n\tconst modelRegistry = options.modelRegistry ?? ModelRegistry.create(authStorage, modelsPath);\n\n\tconst settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);\n\tconst sessionManager = options.sessionManager ?? SessionManager.create(cwd, getDefaultSessionDir(cwd, agentDir));\n\n\tif (!resourceLoader) {\n\t\tresourceLoader = new DefaultResourceLoader({ cwd, agentDir, settingsManager });\n\t\tawait resourceLoader.reload();\n\t\ttime(\"resourceLoader.reload\");\n\t}\n\n\t// Check if session has existing data to restore\n\tconst existingSession = sessionManager.buildSessionContext();\n\tconst hasExistingSession = existingSession.messages.length > 0;\n\tconst hasThinkingEntry = sessionManager.getBranch().some((entry) => entry.type === \"thinking_level_change\");\n\n\tlet model = options.model;\n\tlet modelFallbackMessage: string | undefined;\n\n\t// If session has data, try to restore model from it\n\tif (!model && hasExistingSession && existingSession.model) {\n\t\tconst restoredModel = modelRegistry.find(existingSession.model.provider, existingSession.model.modelId);\n\t\tif (restoredModel && modelRegistry.hasConfiguredAuth(restoredModel)) {\n\t\t\tmodel = restoredModel;\n\t\t}\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = `Could not restore model ${existingSession.model.provider}/${existingSession.model.modelId}`;\n\t\t}\n\t}\n\n\t// If still no model, use findInitialModel (checks settings default, then provider defaults)\n\tif (!model) {\n\t\t// The pi-layer settings.json default wins; when it is unset, fall back to the\n\t\t// hoo-config.json `llm.default_provider` so the seeded/user default actually\n\t\t// takes effect. findInitialModel only honours it if that provider has auth.\n\t\tconst hooLlm = readHooConfig().llm;\n\t\tconst defaultProvider = settingsManager.getDefaultProvider() ?? hooLlm?.default_provider;\n\t\tconst defaultModelId =\n\t\t\tsettingsManager.getDefaultModel() ??\n\t\t\t(settingsManager.getDefaultProvider()\n\t\t\t\t? undefined\n\t\t\t\t: (hooLlm?.default_model ??\n\t\t\t\t\t(defaultProvider && defaultProvider in defaultModelPerProvider\n\t\t\t\t\t\t? defaultModelPerProvider[defaultProvider as keyof typeof defaultModelPerProvider]\n\t\t\t\t\t\t: undefined)));\n\t\tconst result = await findInitialModel({\n\t\t\tscopedModels: [],\n\t\t\tisContinuing: hasExistingSession,\n\t\t\tdefaultProvider,\n\t\t\tdefaultModelId,\n\t\t\tdefaultThinkingLevel: settingsManager.getDefaultThinkingLevel(),\n\t\t\tmodelRegistry,\n\t\t});\n\t\tmodel = result.model;\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = formatNoModelsAvailableMessage();\n\t\t} else if (modelFallbackMessage) {\n\t\t\tmodelFallbackMessage += `. Using ${model.provider}/${model.id}`;\n\t\t}\n\t}\n\n\tlet thinkingLevel = options.thinkingLevel;\n\n\t// If session has data, restore thinking level from it\n\tif (thinkingLevel === undefined && hasExistingSession) {\n\t\tthinkingLevel = hasThinkingEntry\n\t\t\t? (existingSession.thinkingLevel as ThinkingLevel)\n\t\t\t: (settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL);\n\t}\n\n\t// Fall back to settings default\n\tif (thinkingLevel === undefined) {\n\t\tthinkingLevel = settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;\n\t}\n\n\t// Clamp to model capabilities\n\tif (!model) {\n\t\tthinkingLevel = \"off\";\n\t} else {\n\t\tthinkingLevel = clampThinkingLevel(model, thinkingLevel) as ThinkingLevel;\n\t}\n\n\t// `search` is always active: it answers \"find where X lives\" with ranked\n\t// results and degrades to grep-backed lexical retrieval when no semantic\n\t// index is present, so it needs no binary to be useful. The\n\t// `enableEmbsearchTools` flag only controls whether the semantic index is\n\t// built and fused in (see main.ts) — not whether the tool exists.\n\tconst defaultActiveToolNames: ToolName[] = [\"read\", \"bash\", \"edit\", \"write\", \"grep\", \"find\", \"ls\", \"search\"];\n\t// Web tools are registered as base tools but inactive by default; opt-in adds\n\t// them to the default active set. An explicit allowlist (`tools`) takes over\n\t// fully, so callers must list them there to enable in that mode.\n\tconst optInActiveToolNames: ToolName[] = [\n\t\t...(options.enableWebTools ? [\"webfetch\", \"websearch\"] : []),\n\t\t...(options.enableBrowserTools ? [\"browser_run\", \"browser_continue\"] : []),\n\t\t...(options.enableFileTools ? [\"DocRead\", \"DocEdit\", \"DocWrite\", \"DocScan\", \"DocGrep\", \"DocPeek\"] : []),\n\t] as ToolName[];\n\tconst allowedToolNames = options.tools ?? (options.noTools === \"all\" ? [] : undefined);\n\tconst initialActiveToolNames: string[] = options.tools\n\t\t? [...options.tools]\n\t\t: options.noTools\n\t\t\t? []\n\t\t\t: [...defaultActiveToolNames, ...optInActiveToolNames];\n\n\tlet agent: Agent;\n\n\t// Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)\n\tconst convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {\n\t\tconst converted = convertToLlm(messages);\n\t\t// Check setting dynamically so mid-session changes take effect\n\t\tif (!settingsManager.getBlockImages()) {\n\t\t\treturn converted;\n\t\t}\n\t\t// Filter out ImageContent from all messages, replacing with text placeholder\n\t\treturn converted.map((msg) => {\n\t\t\tif (msg.role === \"user\" || msg.role === \"toolResult\") {\n\t\t\t\tconst content = msg.content;\n\t\t\t\tif (Array.isArray(content)) {\n\t\t\t\t\tconst hasImages = content.some((c) => c.type === \"image\");\n\t\t\t\t\tif (hasImages) {\n\t\t\t\t\t\tconst filteredContent = content\n\t\t\t\t\t\t\t.map((c) =>\n\t\t\t\t\t\t\t\tc.type === \"image\" ? { type: \"text\" as const, text: \"Image reading is disabled.\" } : c,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t.filter(\n\t\t\t\t\t\t\t\t(c, i, arr) =>\n\t\t\t\t\t\t\t\t\t// Dedupe consecutive \"Image reading is disabled.\" texts\n\t\t\t\t\t\t\t\t\t!(\n\t\t\t\t\t\t\t\t\t\tc.type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\tc.text === \"Image reading is disabled.\" &&\n\t\t\t\t\t\t\t\t\t\ti > 0 &&\n\t\t\t\t\t\t\t\t\t\tarr[i - 1].type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\t(arr[i - 1] as { type: \"text\"; text: string }).text === \"Image reading is disabled.\"\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\treturn { ...msg, content: filteredContent };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn msg;\n\t\t});\n\t};\n\n\tconst extensionRunnerRef: { current?: ExtensionRunner } = {};\n\n\t// Token-budget pressure for context GC: the fraction of the active model's\n\t// context window in use, measured from the real usage on the outgoing\n\t// message copy. It latches to a high-water mark so that our own evictions —\n\t// which shrink the next turn's measured usage — cannot oscillate a message\n\t// in and out of the transcript and thrash the provider's prefix cache. A\n\t// large drop (compaction, fork) resets the latch to the new, smaller size.\n\tlet budgetPressureHighWater = 0;\n\tconst getBudgetPressure = (contextMessages: AgentMessage[]): number => {\n\t\tconst contextWindow = agent.state.model?.contextWindow ?? 0;\n\t\tif (contextWindow <= 0) return 0;\n\t\tconst gauge = Math.min(estimateContextTokens(contextMessages).tokens / contextWindow, 1);\n\t\tif (gauge > budgetPressureHighWater) {\n\t\t\tbudgetPressureHighWater = gauge; // rising usage — track it\n\t\t} else if (gauge < budgetPressureHighWater - 0.15) {\n\t\t\tbudgetPressureHighWater = gauge; // context collapsed — reset the latch\n\t\t}\n\t\treturn budgetPressureHighWater;\n\t};\n\n\tagent = new Agent({\n\t\tinitialState: {\n\t\t\tsystemPrompt: \"\",\n\t\t\tmodel,\n\t\t\tthinkingLevel,\n\t\t\ttools: [],\n\t\t},\n\t\tconvertToLlm: convertToLlmWithBlockImages,\n\t\tcreateBackgroundResultMessage: createBackgroundTaskMessage,\n\t\tcreateBackgroundPlaceholder: (toolCall) => createBackgroundPlaceholderText(toolCall),\n\t\t// Report in-process background tool load (e.g. background MCP tools) to the\n\t\t// subagent lifeguard so it widens its heartbeat/timeout tolerance for\n\t\t// concurrently-monitored subagents. Peek (don't create) the pool: background\n\t\t// tools can run before any subagent is ever dispatched.\n\t\tonBackgroundTaskCountChange: (count) => peekSubagentPool()?.setExternalLoad(count),\n\t\tstreamFn: async (model, context, options) => {\n\t\t\tconst auth = await modelRegistry.getApiKeyAndHeaders(model);\n\t\t\tif (!auth.ok) {\n\t\t\t\tthrow new Error(auth.error);\n\t\t\t}\n\t\t\tconst providerRetrySettings = settingsManager.getProviderRetrySettings();\n\t\t\tconst attributionHeaders = getAttributionHeaders(model, settingsManager);\n\t\t\treturn streamSimple(model, context, {\n\t\t\t\t...options,\n\t\t\t\tapiKey: auth.apiKey,\n\t\t\t\ttimeoutMs: options?.timeoutMs ?? providerRetrySettings.timeoutMs,\n\t\t\t\tmaxRetries: options?.maxRetries ?? providerRetrySettings.maxRetries,\n\t\t\t\tmaxRetryDelayMs: options?.maxRetryDelayMs ?? providerRetrySettings.maxRetryDelayMs,\n\t\t\t\theaders:\n\t\t\t\t\tattributionHeaders || auth.headers || options?.headers\n\t\t\t\t\t\t? { ...attributionHeaders, ...auth.headers, ...options?.headers }\n\t\t\t\t\t\t: undefined,\n\t\t\t});\n\t\t},\n\t\tonPayload: async (payload, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"before_provider_request\")) {\n\t\t\t\treturn payload;\n\t\t\t}\n\t\t\treturn runner.emitBeforeProviderRequest(payload);\n\t\t},\n\t\tonResponse: async (response, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"after_provider_response\")) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait runner.emit({\n\t\t\t\ttype: \"after_provider_response\",\n\t\t\t\tstatus: response.status,\n\t\t\t\theaders: response.headers,\n\t\t\t});\n\t\t},\n\t\tsessionId: sessionManager.getSessionId(),\n\t\ttransformContext: async (messages) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tconst transformed = runner ? await runner.emitContext(messages) : messages;\n\t\t\tif (!settingsManager.getContextGcEnabled()) return transformed;\n\t\t\treturn evictSupersededReads(transformed, { cwd, budgetPressure: getBudgetPressure(transformed) });\n\t\t},\n\t\tsteeringMode: settingsManager.getSteeringMode(),\n\t\tfollowUpMode: settingsManager.getFollowUpMode(),\n\t\ttransport: settingsManager.getTransport(),\n\t\tthinkingBudgets: settingsManager.getThinkingBudgets(),\n\t\tthinkingDisplay: settingsManager.getThinkingDisplay(),\n\t\tmaxRetryDelayMs: settingsManager.getProviderRetrySettings().maxRetryDelayMs,\n\t});\n\n\t// Restore messages if session has existing data\n\tif (hasExistingSession) {\n\t\tagent.state.messages = existingSession.messages;\n\t\tif (!hasThinkingEntry) {\n\t\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t\t}\n\t} else {\n\t\t// Save initial model and thinking level for new sessions so they can be restored on resume\n\t\tif (model) {\n\t\t\tsessionManager.appendModelChange(model.provider, model.id);\n\t\t}\n\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t}\n\n\tconst session = new AgentSession({\n\t\tagent,\n\t\tsessionManager,\n\t\tsettingsManager,\n\t\tcwd,\n\t\tscopedModels: options.scopedModels,\n\t\tresourceLoader,\n\t\tcustomTools: options.customTools,\n\t\tbaseToolsOverride: options.baseToolsOverride,\n\t\tmodelRegistry,\n\t\tinitialActiveToolNames,\n\t\tallowedToolNames,\n\t\tdisallowedToolNames: options.disallowedTools,\n\t\textensionRunnerRef,\n\t\tsessionStartEvent: options.sessionStartEvent,\n\t});\n\tconst extensionsResult = resourceLoader.getExtensions();\n\n\treturn {\n\t\tsession,\n\t\textensionsResult,\n\t\tmodelFallbackMessage,\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../src/core/sdk.ts"],"names":[],"mappings":"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","sourcesContent":["import { join } from \"node:path\";\nimport {\n\tAgent,\n\ttype AgentMessage,\n\ttype AgentTool,\n\tconvertToLlm,\n\tcreateBackgroundPlaceholderText,\n\tcreateBackgroundTaskMessage,\n\testimateContextTokens,\n\ttype ThinkingLevel,\n} from \"@kolisachint/hoocode-agent-core\";\nimport { clampThinkingLevel, type Message, type Model, streamSimple } from \"@kolisachint/hoocode-ai\";\nimport { getAgentDir } from \"../config.js\";\nimport { readConfig as readHooConfig } from \"../extensions/core/config.js\";\nimport { AgentSession } from \"./agent-session.js\";\nimport { formatNoModelsAvailableMessage } from \"./auth-guidance.js\";\nimport { AuthStorage } from \"./auth-storage.js\";\nimport { evictSupersededReads } from \"./context-gc.js\";\nimport { DEFAULT_THINKING_LEVEL } from \"./defaults.js\";\nimport type { ExtensionRunner, LoadExtensionsResult, SessionStartEvent, ToolDefinition } from \"./extensions/index.js\";\nimport { ModelRegistry } from \"./model-registry.js\";\nimport { defaultModelPerProvider, findInitialModel } from \"./model-resolver.js\";\nimport type { ResourceLoader } from \"./resource-loader.js\";\nimport { DefaultResourceLoader } from \"./resource-loader.js\";\nimport { getDefaultSessionDir, SessionManager } from \"./session-manager.js\";\nimport { SettingsManager } from \"./settings-manager.js\";\nimport { peekSubagentPool } from \"./subagent-pool-instance.js\";\nimport { isInstallTelemetryEnabled } from \"./telemetry.js\";\nimport { time } from \"./timings.js\";\nimport {\n\tcreateBashTool,\n\tcreateCodingTools,\n\tcreateEditTool,\n\tcreateFindTool,\n\tcreateGrepTool,\n\tcreateLsTool,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateWriteTool,\n\ttype ToolName,\n\twithFileMutationQueue,\n} from \"./tools/index.js\";\n\nexport interface CreateAgentSessionOptions {\n\t/** Working directory for project-local discovery. Default: process.cwd() */\n\tcwd?: string;\n\t/** Global config directory. Default: ~/.hoocode/agent */\n\tagentDir?: string;\n\n\t/** Auth storage for credentials. Default: AuthStorage.create(agentDir/auth.json) */\n\tauthStorage?: AuthStorage;\n\t/** Model registry. Default: ModelRegistry.create(authStorage, agentDir/models.json) */\n\tmodelRegistry?: ModelRegistry;\n\n\t/** Model to use. Default: from settings, else first available */\n\tmodel?: Model<any>;\n\t/** Thinking level. Default: from settings, else 'medium' (clamped to model capabilities) */\n\tthinkingLevel?: ThinkingLevel;\n\t/** Models available for cycling (Ctrl+P in interactive mode) */\n\tscopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;\n\n\t/**\n\t * Optional default tool suppression mode when no explicit allowlist is provided.\n\t *\n\t * - \"all\": start with no tools enabled\n\t * - \"builtin\": disable the default built-in tools (read, bash, edit, write)\n\t * but keep extension/custom tools enabled\n\t */\n\tnoTools?: \"all\" | \"builtin\";\n\t/**\n\t * Optional allowlist of tool names.\n\t *\n\t * When omitted, hoocode enables the default built-in tools (read, bash, edit, write)\n\t * and leaves extension/custom tools enabled unless `noTools` changes that default.\n\t * When provided, only the listed tool names are enabled.\n\t */\n\ttools?: string[];\n\t/**\n\t * Optional denylist of tool names, subtracted from whatever set is otherwise\n\t * enabled (allowlist or default). Applied to built-in, extension, and custom tools.\n\t */\n\tdisallowedTools?: string[];\n\t/**\n\t * Enable the built-in `webfetch` + `websearch` tools, which are defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list `webfetch`/`websearch` there instead). Network access is still gated\n\t * per call and filtered by `.webtoolsignore`.\n\t */\n\tenableWebTools?: boolean;\n\t/**\n\t * Enable the built-in `browser_run` + `browser_continue` tools, which drive the\n\t * `browsertools` deterministic browser engine (parent-in-the-loop). Defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list them there instead).\n\t */\n\tenableBrowserTools?: boolean;\n\t/**\n\t * Enable the built-in document tools — `DocRead`/`DocEdit`/`DocWrite` (extract\n\t * and lossless id-based editing) plus `DocScan`/`DocGrep`/`DocPeek` (the\n\t * token-sensitive discovery loop: outline, search, partial read). Defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list them there instead). They shell out to the `filetools` binary to\n\t * losslessly extract/edit structured/binary documents.\n\t */\n\tenableFileTools?: boolean;\n\t/**\n\t * Enable the semantic index layer for the built-in `search` tool (ranked\n\t * lexical + semantic retrieval, rank-fused). The search tool is active by\n\t * default; this flag only toggles the optional embedding index. When false,\n\t * search degrades to lexical-only. Ignored when an explicit `tools` allowlist\n\t * is provided (list it there instead).\n\t */\n\tenableEmbsearchTools?: boolean;\n\t/** Custom tools to register (in addition to built-in tools). */\n\tcustomTools?: ToolDefinition[];\n\t/**\n\t * Replace the built-in base tools entirely (the light preset uses this to\n\t * swap in short-schema read/write/edit/bash variants). Keys become the\n\t * default active tool set when no explicit `tools` allowlist is provided.\n\t */\n\tbaseToolsOverride?: Record<string, AgentTool>;\n\n\t/** Resource loader. When omitted, DefaultResourceLoader is used. */\n\tresourceLoader?: ResourceLoader;\n\n\t/** Session manager. Default: SessionManager.create(cwd) */\n\tsessionManager?: SessionManager;\n\n\t/** Settings manager. Default: SettingsManager.create(cwd, agentDir) */\n\tsettingsManager?: SettingsManager;\n\t/** Session start event metadata for extension runtime startup. */\n\tsessionStartEvent?: SessionStartEvent;\n}\n\n/** Result from createAgentSession */\nexport interface CreateAgentSessionResult {\n\t/** The created session */\n\tsession: AgentSession;\n\t/** Extensions result (for UI context setup in interactive mode) */\n\textensionsResult: LoadExtensionsResult;\n\t/** Warning if session was restored with a different model than saved */\n\tmodelFallbackMessage?: string;\n}\n\n// Re-exports\n\nexport type { AgentDefinition, AgentSource } from \"./agent-frontmatter.js\";\nexport { AgentRegistry, formatAgentsForPrompt, loadAgentRegistry } from \"./agent-registry.js\";\nexport * from \"./agent-session-runtime.js\";\nexport type {\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tExtensionFactory,\n\tSlashCommandInfo,\n\tSlashCommandSource,\n\tToolDefinition,\n} from \"./extensions/index.js\";\nexport type { PromptTemplate } from \"./prompt-templates.js\";\nexport type { Skill } from \"./skills.js\";\nexport type { Tool } from \"./tools/index.js\";\n\nexport {\n\twithFileMutationQueue,\n\t// Tool factories (for custom cwd)\n\tcreateCodingTools,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateBashTool,\n\tcreateEditTool,\n\tcreateWriteTool,\n\tcreateGrepTool,\n\tcreateFindTool,\n\tcreateLsTool,\n};\n\n// Helper Functions\n\nfunction getDefaultAgentDir(): string {\n\treturn getAgentDir();\n}\n\nfunction getAttributionHeaders(\n\tmodel: Model<any>,\n\tsettingsManager: SettingsManager,\n): Record<string, string> | undefined {\n\tif (!isInstallTelemetryEnabled(settingsManager)) {\n\t\treturn undefined;\n\t}\n\n\tif (model.provider === \"openrouter\" || model.baseUrl.includes(\"openrouter.ai\")) {\n\t\treturn {\n\t\t\t\"HTTP-Referer\": \"https://github.com/kolisachint/hoocode\",\n\t\t\t\"X-OpenRouter-Title\": \"hoocode\",\n\t\t\t\"X-OpenRouter-Categories\": \"cli-agent\",\n\t\t};\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Create an AgentSession with the specified options.\n *\n * @example\n * ```typescript\n * // Minimal - uses defaults\n * const { session } = await createAgentSession();\n *\n * // With explicit model\n * import { getModel } from '@kolisachint/hoocode-ai';\n * const { session } = await createAgentSession({\n * model: getModel('anthropic', 'claude-opus-4-5'),\n * thinkingLevel: 'high',\n * });\n *\n * // Continue previous session\n * const { session, modelFallbackMessage } = await createAgentSession({\n * continueSession: true,\n * });\n *\n * // Full control\n * const loader = new DefaultResourceLoader({\n * cwd: process.cwd(),\n * agentDir: getAgentDir(),\n * settingsManager: SettingsManager.create(),\n * });\n * await loader.reload();\n * const { session } = await createAgentSession({\n * model: myModel,\n * tools: [readTool, bashTool],\n * resourceLoader: loader,\n * sessionManager: SessionManager.inMemory(),\n * });\n * ```\n */\nexport async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {\n\tconst cwd = options.cwd ?? options.sessionManager?.getCwd() ?? process.cwd();\n\tconst agentDir = options.agentDir ?? getDefaultAgentDir();\n\tlet resourceLoader = options.resourceLoader;\n\n\t// Use provided or create AuthStorage and ModelRegistry\n\tconst authPath = options.agentDir ? join(agentDir, \"auth.json\") : undefined;\n\tconst modelsPath = options.agentDir ? join(agentDir, \"models.json\") : undefined;\n\tconst authStorage = options.authStorage ?? AuthStorage.create(authPath);\n\tconst modelRegistry = options.modelRegistry ?? ModelRegistry.create(authStorage, modelsPath);\n\n\tconst settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);\n\tconst sessionManager = options.sessionManager ?? SessionManager.create(cwd, getDefaultSessionDir(cwd, agentDir));\n\n\tif (!resourceLoader) {\n\t\tresourceLoader = new DefaultResourceLoader({ cwd, agentDir, settingsManager });\n\t\tawait resourceLoader.reload();\n\t\ttime(\"resourceLoader.reload\");\n\t}\n\n\t// Check if session has existing data to restore\n\tconst existingSession = sessionManager.buildSessionContext();\n\tconst hasExistingSession = existingSession.messages.length > 0;\n\tconst hasThinkingEntry = sessionManager.getBranch().some((entry) => entry.type === \"thinking_level_change\");\n\n\tlet model = options.model;\n\tlet modelFallbackMessage: string | undefined;\n\n\t// If session has data, try to restore model from it\n\tif (!model && hasExistingSession && existingSession.model) {\n\t\tconst restoredModel = modelRegistry.find(existingSession.model.provider, existingSession.model.modelId);\n\t\tif (restoredModel && modelRegistry.hasConfiguredAuth(restoredModel)) {\n\t\t\tmodel = restoredModel;\n\t\t}\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = `Could not restore model ${existingSession.model.provider}/${existingSession.model.modelId}`;\n\t\t}\n\t}\n\n\t// If still no model, use findInitialModel (checks settings default, then provider defaults)\n\tif (!model) {\n\t\t// The pi-layer settings.json default wins; when it is unset, fall back to the\n\t\t// hoo-config.json `llm.default_provider` so the seeded/user default actually\n\t\t// takes effect. findInitialModel only honours it if that provider has auth.\n\t\tconst hooLlm = readHooConfig().llm;\n\t\tconst defaultProvider = settingsManager.getDefaultProvider() ?? hooLlm?.default_provider;\n\t\tconst defaultModelId =\n\t\t\tsettingsManager.getDefaultModel() ??\n\t\t\t(settingsManager.getDefaultProvider()\n\t\t\t\t? undefined\n\t\t\t\t: (hooLlm?.default_model ??\n\t\t\t\t\t(defaultProvider && defaultProvider in defaultModelPerProvider\n\t\t\t\t\t\t? defaultModelPerProvider[defaultProvider as keyof typeof defaultModelPerProvider]\n\t\t\t\t\t\t: undefined)));\n\t\tconst result = await findInitialModel({\n\t\t\tscopedModels: [],\n\t\t\tisContinuing: hasExistingSession,\n\t\t\tdefaultProvider,\n\t\t\tdefaultModelId,\n\t\t\tdefaultThinkingLevel: settingsManager.getDefaultThinkingLevel(),\n\t\t\tmodelRegistry,\n\t\t});\n\t\tmodel = result.model;\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = formatNoModelsAvailableMessage();\n\t\t} else if (modelFallbackMessage) {\n\t\t\tmodelFallbackMessage += `. Using ${model.provider}/${model.id}`;\n\t\t}\n\t}\n\n\tlet thinkingLevel = options.thinkingLevel;\n\n\t// If session has data, restore thinking level from it\n\tif (thinkingLevel === undefined && hasExistingSession) {\n\t\tthinkingLevel = hasThinkingEntry\n\t\t\t? (existingSession.thinkingLevel as ThinkingLevel)\n\t\t\t: (settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL);\n\t}\n\n\t// Fall back to settings default\n\tif (thinkingLevel === undefined) {\n\t\tthinkingLevel = settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;\n\t}\n\n\t// Clamp to model capabilities\n\tif (!model) {\n\t\tthinkingLevel = \"off\";\n\t} else {\n\t\tthinkingLevel = clampThinkingLevel(model, thinkingLevel) as ThinkingLevel;\n\t}\n\n\t// `search` is always active: it answers \"find where X lives\" with ranked\n\t// results and degrades to grep-backed lexical retrieval when no semantic\n\t// index is present, so it needs no binary to be useful. The\n\t// `enableEmbsearchTools` flag only controls whether the semantic index is\n\t// built and fused in (see main.ts) — not whether the tool exists.\n\tconst defaultActiveToolNames: ToolName[] = [\"read\", \"bash\", \"edit\", \"write\", \"grep\", \"find\", \"ls\", \"search\"];\n\t// Web tools are registered as base tools but inactive by default; opt-in adds\n\t// them to the default active set. An explicit allowlist (`tools`) takes over\n\t// fully, so callers must list them there to enable in that mode.\n\tconst optInActiveToolNames: ToolName[] = [\n\t\t...(options.enableWebTools ? [\"webfetch\", \"websearch\"] : []),\n\t\t...(options.enableBrowserTools ? [\"browser_run\", \"browser_continue\"] : []),\n\t\t...(options.enableFileTools ? [\"DocRead\", \"DocEdit\", \"DocWrite\", \"DocScan\", \"DocGrep\", \"DocPeek\"] : []),\n\t] as ToolName[];\n\tconst allowedToolNames = options.tools ?? (options.noTools === \"all\" ? [] : undefined);\n\tconst initialActiveToolNames: string[] = options.tools\n\t\t? [...options.tools]\n\t\t: options.noTools\n\t\t\t? []\n\t\t\t: [...defaultActiveToolNames, ...optInActiveToolNames];\n\n\tlet agent: Agent;\n\n\t// Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)\n\tconst convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {\n\t\tconst converted = convertToLlm(messages);\n\t\t// Check setting dynamically so mid-session changes take effect\n\t\tif (!settingsManager.getBlockImages()) {\n\t\t\treturn converted;\n\t\t}\n\t\t// Filter out ImageContent from all messages, replacing with text placeholder\n\t\treturn converted.map((msg) => {\n\t\t\tif (msg.role === \"user\" || msg.role === \"toolResult\") {\n\t\t\t\tconst content = msg.content;\n\t\t\t\tif (Array.isArray(content)) {\n\t\t\t\t\tconst hasImages = content.some((c) => c.type === \"image\");\n\t\t\t\t\tif (hasImages) {\n\t\t\t\t\t\tconst filteredContent = content\n\t\t\t\t\t\t\t.map((c) =>\n\t\t\t\t\t\t\t\tc.type === \"image\" ? { type: \"text\" as const, text: \"Image reading is disabled.\" } : c,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t.filter(\n\t\t\t\t\t\t\t\t(c, i, arr) =>\n\t\t\t\t\t\t\t\t\t// Dedupe consecutive \"Image reading is disabled.\" texts\n\t\t\t\t\t\t\t\t\t!(\n\t\t\t\t\t\t\t\t\t\tc.type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\tc.text === \"Image reading is disabled.\" &&\n\t\t\t\t\t\t\t\t\t\ti > 0 &&\n\t\t\t\t\t\t\t\t\t\tarr[i - 1].type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\t(arr[i - 1] as { type: \"text\"; text: string }).text === \"Image reading is disabled.\"\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\treturn { ...msg, content: filteredContent };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn msg;\n\t\t});\n\t};\n\n\tconst extensionRunnerRef: { current?: ExtensionRunner } = {};\n\n\t// Token-budget pressure for context GC: the fraction of the active model's\n\t// context window in use, measured from the real usage on the outgoing\n\t// message copy. It latches to a high-water mark so that our own evictions —\n\t// which shrink the next turn's measured usage — cannot oscillate a message\n\t// in and out of the transcript and thrash the provider's prefix cache. A\n\t// large drop (compaction, fork) resets the latch to the new, smaller size.\n\tlet budgetPressureHighWater = 0;\n\tconst getBudgetPressure = (contextMessages: AgentMessage[]): number => {\n\t\tconst contextWindow = agent.state.model?.contextWindow ?? 0;\n\t\tif (contextWindow <= 0) return 0;\n\t\tconst gauge = Math.min(estimateContextTokens(contextMessages).tokens / contextWindow, 1);\n\t\tif (gauge > budgetPressureHighWater) {\n\t\t\tbudgetPressureHighWater = gauge; // rising usage — track it\n\t\t} else if (gauge < budgetPressureHighWater - 0.15) {\n\t\t\tbudgetPressureHighWater = gauge; // context collapsed — reset the latch\n\t\t}\n\t\treturn budgetPressureHighWater;\n\t};\n\n\tagent = new Agent({\n\t\tinitialState: {\n\t\t\tsystemPrompt: \"\",\n\t\t\tmodel,\n\t\t\tthinkingLevel,\n\t\t\ttools: [],\n\t\t},\n\t\tconvertToLlm: convertToLlmWithBlockImages,\n\t\tcreateBackgroundResultMessage: createBackgroundTaskMessage,\n\t\tcreateBackgroundPlaceholder: (toolCall) => createBackgroundPlaceholderText(toolCall),\n\t\t// Report in-process background tool load (e.g. background MCP tools) to the\n\t\t// subagent lifeguard so it widens its heartbeat/timeout tolerance for\n\t\t// concurrently-monitored subagents. Peek (don't create) the pool: background\n\t\t// tools can run before any subagent is ever dispatched.\n\t\tonBackgroundTaskCountChange: (count) => peekSubagentPool()?.setExternalLoad(count),\n\t\tstreamFn: async (model, context, options) => {\n\t\t\tconst auth = await modelRegistry.getApiKeyAndHeaders(model);\n\t\t\tif (!auth.ok) {\n\t\t\t\tthrow new Error(auth.error);\n\t\t\t}\n\t\t\tconst providerRetrySettings = settingsManager.getProviderRetrySettings();\n\t\t\tconst attributionHeaders = getAttributionHeaders(model, settingsManager);\n\t\t\treturn streamSimple(model, context, {\n\t\t\t\t...options,\n\t\t\t\tapiKey: auth.apiKey,\n\t\t\t\ttimeoutMs: options?.timeoutMs ?? providerRetrySettings.timeoutMs,\n\t\t\t\tmaxRetries: options?.maxRetries ?? providerRetrySettings.maxRetries,\n\t\t\t\tmaxRetryDelayMs: options?.maxRetryDelayMs ?? providerRetrySettings.maxRetryDelayMs,\n\t\t\t\theaders:\n\t\t\t\t\tattributionHeaders || auth.headers || options?.headers\n\t\t\t\t\t\t? { ...attributionHeaders, ...auth.headers, ...options?.headers }\n\t\t\t\t\t\t: undefined,\n\t\t\t});\n\t\t},\n\t\tonPayload: async (payload, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"before_provider_request\")) {\n\t\t\t\treturn payload;\n\t\t\t}\n\t\t\treturn runner.emitBeforeProviderRequest(payload);\n\t\t},\n\t\tonResponse: async (response, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"after_provider_response\")) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait runner.emit({\n\t\t\t\ttype: \"after_provider_response\",\n\t\t\t\tstatus: response.status,\n\t\t\t\theaders: response.headers,\n\t\t\t});\n\t\t},\n\t\tsessionId: sessionManager.getSessionId(),\n\t\ttransformContext: async (messages) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tconst transformed = runner ? await runner.emitContext(messages) : messages;\n\t\t\tif (!settingsManager.getContextGcEnabled()) return transformed;\n\t\t\treturn evictSupersededReads(transformed, { cwd, budgetPressure: getBudgetPressure(transformed) });\n\t\t},\n\t\tsteeringMode: settingsManager.getSteeringMode(),\n\t\tfollowUpMode: settingsManager.getFollowUpMode(),\n\t\ttransport: settingsManager.getTransport(),\n\t\tthinkingBudgets: settingsManager.getThinkingBudgets(),\n\t\tthinkingDisplay: settingsManager.getThinkingDisplay(),\n\t\tmaxRetryDelayMs: settingsManager.getProviderRetrySettings().maxRetryDelayMs,\n\t});\n\n\t// Restore messages if session has existing data\n\tif (hasExistingSession) {\n\t\tagent.state.messages = existingSession.messages;\n\t\tif (!hasThinkingEntry) {\n\t\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t\t}\n\t} else {\n\t\t// Save initial model and thinking level for new sessions so they can be restored on resume\n\t\tif (model) {\n\t\t\tsessionManager.appendModelChange(model.provider, model.id);\n\t\t}\n\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t}\n\n\tconst session = new AgentSession({\n\t\tagent,\n\t\tsessionManager,\n\t\tsettingsManager,\n\t\tcwd,\n\t\tscopedModels: options.scopedModels,\n\t\tresourceLoader,\n\t\tcustomTools: options.customTools,\n\t\tbaseToolsOverride: options.baseToolsOverride,\n\t\tmodelRegistry,\n\t\tinitialActiveToolNames,\n\t\tallowedToolNames,\n\t\tdisallowedToolNames: options.disallowedTools,\n\t\textensionRunnerRef,\n\t\tsessionStartEvent: options.sessionStartEvent,\n\t});\n\tconst extensionsResult = resourceLoader.getExtensions();\n\n\treturn {\n\t\tsession,\n\t\textensionsResult,\n\t\tmodelFallbackMessage,\n\t};\n}\n"]}
|
package/dist/core/sdk.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sdk.js","sourceRoot":"","sources":["../../src/core/sdk.ts"],"names":[],"mappings":"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","sourcesContent":["import { join } from \"node:path\";\nimport {\n\tAgent,\n\ttype AgentMessage,\n\ttype AgentTool,\n\tconvertToLlm,\n\tcreateBackgroundPlaceholderText,\n\tcreateBackgroundTaskMessage,\n\testimateContextTokens,\n\ttype ThinkingLevel,\n} from \"@kolisachint/hoocode-agent-core\";\nimport { clampThinkingLevel, type Message, type Model, streamSimple } from \"@kolisachint/hoocode-ai\";\nimport { getAgentDir } from \"../config.js\";\nimport { readConfig as readHooConfig } from \"../extensions/core/config.js\";\nimport { AgentSession } from \"./agent-session.js\";\nimport { formatNoModelsAvailableMessage } from \"./auth-guidance.js\";\nimport { AuthStorage } from \"./auth-storage.js\";\nimport { evictSupersededReads } from \"./context-gc.js\";\nimport { DEFAULT_THINKING_LEVEL } from \"./defaults.js\";\nimport type { ExtensionRunner, LoadExtensionsResult, SessionStartEvent, ToolDefinition } from \"./extensions/index.js\";\nimport { ModelRegistry } from \"./model-registry.js\";\nimport { defaultModelPerProvider, findInitialModel } from \"./model-resolver.js\";\nimport type { ResourceLoader } from \"./resource-loader.js\";\nimport { DefaultResourceLoader } from \"./resource-loader.js\";\nimport { getDefaultSessionDir, SessionManager } from \"./session-manager.js\";\nimport { SettingsManager } from \"./settings-manager.js\";\nimport { peekSubagentPool } from \"./subagent-pool-instance.js\";\nimport { isInstallTelemetryEnabled } from \"./telemetry.js\";\nimport { time } from \"./timings.js\";\nimport {\n\tcreateBashTool,\n\tcreateCodingTools,\n\tcreateEditTool,\n\tcreateFindTool,\n\tcreateGrepTool,\n\tcreateLsTool,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateWriteTool,\n\ttype ToolName,\n\twithFileMutationQueue,\n} from \"./tools/index.js\";\n\nexport interface CreateAgentSessionOptions {\n\t/** Working directory for project-local discovery. Default: process.cwd() */\n\tcwd?: string;\n\t/** Global config directory. Default: ~/.hoocode/agent */\n\tagentDir?: string;\n\n\t/** Auth storage for credentials. Default: AuthStorage.create(agentDir/auth.json) */\n\tauthStorage?: AuthStorage;\n\t/** Model registry. Default: ModelRegistry.create(authStorage, agentDir/models.json) */\n\tmodelRegistry?: ModelRegistry;\n\n\t/** Model to use. Default: from settings, else first available */\n\tmodel?: Model<any>;\n\t/** Thinking level. Default: from settings, else 'medium' (clamped to model capabilities) */\n\tthinkingLevel?: ThinkingLevel;\n\t/** Models available for cycling (Ctrl+P in interactive mode) */\n\tscopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;\n\n\t/**\n\t * Optional default tool suppression mode when no explicit allowlist is provided.\n\t *\n\t * - \"all\": start with no tools enabled\n\t * - \"builtin\": disable the default built-in tools (read, bash, edit, write)\n\t * but keep extension/custom tools enabled\n\t */\n\tnoTools?: \"all\" | \"builtin\";\n\t/**\n\t * Optional allowlist of tool names.\n\t *\n\t * When omitted, hoocode enables the default built-in tools (read, bash, edit, write)\n\t * and leaves extension/custom tools enabled unless `noTools` changes that default.\n\t * When provided, only the listed tool names are enabled.\n\t */\n\ttools?: string[];\n\t/**\n\t * Optional denylist of tool names, subtracted from whatever set is otherwise\n\t * enabled (allowlist or default). Applied to built-in, extension, and custom tools.\n\t */\n\tdisallowedTools?: string[];\n\t/**\n\t * Enable the built-in `webfetch` + `websearch` tools, which are defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list `webfetch`/`websearch` there instead). Network access is still gated\n\t * per call and filtered by `.webtoolsignore`.\n\t */\n\tenableWebTools?: boolean;\n\t/**\n\t * Enable the built-in `browser_run` + `browser_continue` tools, which drive the\n\t * `browsertools` deterministic browser engine (parent-in-the-loop). Defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list them there instead).\n\t */\n\tenableBrowserTools?: boolean;\n\t/**\n\t * Enable the built-in document tools — `DocRead`/`DocEdit`/`DocWrite` (extract\n\t * and lossless id-based editing) plus `DocScan`/`DocGrep`/`DocPeek` (the\n\t * token-sensitive discovery loop: outline, search, partial read). Defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list them there instead). They shell out to the `filetools` binary to\n\t * losslessly extract/edit structured/binary documents.\n\t */\n\tenableFileTools?: boolean;\n\t/**\n\t * Enable the built-in `search` tool (ranked lexical + semantic retrieval,\n\t * rank-fused; the semantic side uses a local embedding index via the\n\t * `embsearch` binary). Defined but inactive by default. Ignored when an\n\t * explicit `tools` allowlist is provided (list it there instead). While the\n\t * per-session index service is unavailable the tool degrades to lexical\n\t * retrieval instead of erroring.\n\t */\n\tenableEmbsearchTools?: boolean;\n\t/** Custom tools to register (in addition to built-in tools). */\n\tcustomTools?: ToolDefinition[];\n\t/**\n\t * Replace the built-in base tools entirely (the light preset uses this to\n\t * swap in short-schema read/write/edit/bash variants). Keys become the\n\t * default active tool set when no explicit `tools` allowlist is provided.\n\t */\n\tbaseToolsOverride?: Record<string, AgentTool>;\n\n\t/** Resource loader. When omitted, DefaultResourceLoader is used. */\n\tresourceLoader?: ResourceLoader;\n\n\t/** Session manager. Default: SessionManager.create(cwd) */\n\tsessionManager?: SessionManager;\n\n\t/** Settings manager. Default: SettingsManager.create(cwd, agentDir) */\n\tsettingsManager?: SettingsManager;\n\t/** Session start event metadata for extension runtime startup. */\n\tsessionStartEvent?: SessionStartEvent;\n}\n\n/** Result from createAgentSession */\nexport interface CreateAgentSessionResult {\n\t/** The created session */\n\tsession: AgentSession;\n\t/** Extensions result (for UI context setup in interactive mode) */\n\textensionsResult: LoadExtensionsResult;\n\t/** Warning if session was restored with a different model than saved */\n\tmodelFallbackMessage?: string;\n}\n\n// Re-exports\n\nexport type { AgentDefinition, AgentSource } from \"./agent-frontmatter.js\";\nexport { AgentRegistry, formatAgentsForPrompt, loadAgentRegistry } from \"./agent-registry.js\";\nexport * from \"./agent-session-runtime.js\";\nexport type {\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tExtensionFactory,\n\tSlashCommandInfo,\n\tSlashCommandSource,\n\tToolDefinition,\n} from \"./extensions/index.js\";\nexport type { PromptTemplate } from \"./prompt-templates.js\";\nexport type { Skill } from \"./skills.js\";\nexport type { Tool } from \"./tools/index.js\";\n\nexport {\n\twithFileMutationQueue,\n\t// Tool factories (for custom cwd)\n\tcreateCodingTools,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateBashTool,\n\tcreateEditTool,\n\tcreateWriteTool,\n\tcreateGrepTool,\n\tcreateFindTool,\n\tcreateLsTool,\n};\n\n// Helper Functions\n\nfunction getDefaultAgentDir(): string {\n\treturn getAgentDir();\n}\n\nfunction getAttributionHeaders(\n\tmodel: Model<any>,\n\tsettingsManager: SettingsManager,\n): Record<string, string> | undefined {\n\tif (!isInstallTelemetryEnabled(settingsManager)) {\n\t\treturn undefined;\n\t}\n\n\tif (model.provider === \"openrouter\" || model.baseUrl.includes(\"openrouter.ai\")) {\n\t\treturn {\n\t\t\t\"HTTP-Referer\": \"https://github.com/kolisachint/hoocode\",\n\t\t\t\"X-OpenRouter-Title\": \"hoocode\",\n\t\t\t\"X-OpenRouter-Categories\": \"cli-agent\",\n\t\t};\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Create an AgentSession with the specified options.\n *\n * @example\n * ```typescript\n * // Minimal - uses defaults\n * const { session } = await createAgentSession();\n *\n * // With explicit model\n * import { getModel } from '@kolisachint/hoocode-ai';\n * const { session } = await createAgentSession({\n * model: getModel('anthropic', 'claude-opus-4-5'),\n * thinkingLevel: 'high',\n * });\n *\n * // Continue previous session\n * const { session, modelFallbackMessage } = await createAgentSession({\n * continueSession: true,\n * });\n *\n * // Full control\n * const loader = new DefaultResourceLoader({\n * cwd: process.cwd(),\n * agentDir: getAgentDir(),\n * settingsManager: SettingsManager.create(),\n * });\n * await loader.reload();\n * const { session } = await createAgentSession({\n * model: myModel,\n * tools: [readTool, bashTool],\n * resourceLoader: loader,\n * sessionManager: SessionManager.inMemory(),\n * });\n * ```\n */\nexport async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {\n\tconst cwd = options.cwd ?? options.sessionManager?.getCwd() ?? process.cwd();\n\tconst agentDir = options.agentDir ?? getDefaultAgentDir();\n\tlet resourceLoader = options.resourceLoader;\n\n\t// Use provided or create AuthStorage and ModelRegistry\n\tconst authPath = options.agentDir ? join(agentDir, \"auth.json\") : undefined;\n\tconst modelsPath = options.agentDir ? join(agentDir, \"models.json\") : undefined;\n\tconst authStorage = options.authStorage ?? AuthStorage.create(authPath);\n\tconst modelRegistry = options.modelRegistry ?? ModelRegistry.create(authStorage, modelsPath);\n\n\tconst settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);\n\tconst sessionManager = options.sessionManager ?? SessionManager.create(cwd, getDefaultSessionDir(cwd, agentDir));\n\n\tif (!resourceLoader) {\n\t\tresourceLoader = new DefaultResourceLoader({ cwd, agentDir, settingsManager });\n\t\tawait resourceLoader.reload();\n\t\ttime(\"resourceLoader.reload\");\n\t}\n\n\t// Check if session has existing data to restore\n\tconst existingSession = sessionManager.buildSessionContext();\n\tconst hasExistingSession = existingSession.messages.length > 0;\n\tconst hasThinkingEntry = sessionManager.getBranch().some((entry) => entry.type === \"thinking_level_change\");\n\n\tlet model = options.model;\n\tlet modelFallbackMessage: string | undefined;\n\n\t// If session has data, try to restore model from it\n\tif (!model && hasExistingSession && existingSession.model) {\n\t\tconst restoredModel = modelRegistry.find(existingSession.model.provider, existingSession.model.modelId);\n\t\tif (restoredModel && modelRegistry.hasConfiguredAuth(restoredModel)) {\n\t\t\tmodel = restoredModel;\n\t\t}\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = `Could not restore model ${existingSession.model.provider}/${existingSession.model.modelId}`;\n\t\t}\n\t}\n\n\t// If still no model, use findInitialModel (checks settings default, then provider defaults)\n\tif (!model) {\n\t\t// The pi-layer settings.json default wins; when it is unset, fall back to the\n\t\t// hoo-config.json `llm.default_provider` so the seeded/user default actually\n\t\t// takes effect. findInitialModel only honours it if that provider has auth.\n\t\tconst hooLlm = readHooConfig().llm;\n\t\tconst defaultProvider = settingsManager.getDefaultProvider() ?? hooLlm?.default_provider;\n\t\tconst defaultModelId =\n\t\t\tsettingsManager.getDefaultModel() ??\n\t\t\t(settingsManager.getDefaultProvider()\n\t\t\t\t? undefined\n\t\t\t\t: (hooLlm?.default_model ??\n\t\t\t\t\t(defaultProvider && defaultProvider in defaultModelPerProvider\n\t\t\t\t\t\t? defaultModelPerProvider[defaultProvider as keyof typeof defaultModelPerProvider]\n\t\t\t\t\t\t: undefined)));\n\t\tconst result = await findInitialModel({\n\t\t\tscopedModels: [],\n\t\t\tisContinuing: hasExistingSession,\n\t\t\tdefaultProvider,\n\t\t\tdefaultModelId,\n\t\t\tdefaultThinkingLevel: settingsManager.getDefaultThinkingLevel(),\n\t\t\tmodelRegistry,\n\t\t});\n\t\tmodel = result.model;\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = formatNoModelsAvailableMessage();\n\t\t} else if (modelFallbackMessage) {\n\t\t\tmodelFallbackMessage += `. Using ${model.provider}/${model.id}`;\n\t\t}\n\t}\n\n\tlet thinkingLevel = options.thinkingLevel;\n\n\t// If session has data, restore thinking level from it\n\tif (thinkingLevel === undefined && hasExistingSession) {\n\t\tthinkingLevel = hasThinkingEntry\n\t\t\t? (existingSession.thinkingLevel as ThinkingLevel)\n\t\t\t: (settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL);\n\t}\n\n\t// Fall back to settings default\n\tif (thinkingLevel === undefined) {\n\t\tthinkingLevel = settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;\n\t}\n\n\t// Clamp to model capabilities\n\tif (!model) {\n\t\tthinkingLevel = \"off\";\n\t} else {\n\t\tthinkingLevel = clampThinkingLevel(model, thinkingLevel) as ThinkingLevel;\n\t}\n\n\t// `search` is always active: it answers \"find where X lives\" with ranked\n\t// results and degrades to grep-backed lexical retrieval when no semantic\n\t// index is present, so it needs no binary to be useful. The\n\t// `enableEmbsearchTools` flag only controls whether the semantic index is\n\t// built and fused in (see main.ts) — not whether the tool exists.\n\tconst defaultActiveToolNames: ToolName[] = [\"read\", \"bash\", \"edit\", \"write\", \"grep\", \"find\", \"ls\", \"search\"];\n\t// Web tools are registered as base tools but inactive by default; opt-in adds\n\t// them to the default active set. An explicit allowlist (`tools`) takes over\n\t// fully, so callers must list them there to enable in that mode.\n\tconst optInActiveToolNames: ToolName[] = [\n\t\t...(options.enableWebTools ? [\"webfetch\", \"websearch\"] : []),\n\t\t...(options.enableBrowserTools ? [\"browser_run\", \"browser_continue\"] : []),\n\t\t...(options.enableFileTools ? [\"DocRead\", \"DocEdit\", \"DocWrite\", \"DocScan\", \"DocGrep\", \"DocPeek\"] : []),\n\t] as ToolName[];\n\tconst allowedToolNames = options.tools ?? (options.noTools === \"all\" ? [] : undefined);\n\tconst initialActiveToolNames: string[] = options.tools\n\t\t? [...options.tools]\n\t\t: options.noTools\n\t\t\t? []\n\t\t\t: [...defaultActiveToolNames, ...optInActiveToolNames];\n\n\tlet agent: Agent;\n\n\t// Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)\n\tconst convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {\n\t\tconst converted = convertToLlm(messages);\n\t\t// Check setting dynamically so mid-session changes take effect\n\t\tif (!settingsManager.getBlockImages()) {\n\t\t\treturn converted;\n\t\t}\n\t\t// Filter out ImageContent from all messages, replacing with text placeholder\n\t\treturn converted.map((msg) => {\n\t\t\tif (msg.role === \"user\" || msg.role === \"toolResult\") {\n\t\t\t\tconst content = msg.content;\n\t\t\t\tif (Array.isArray(content)) {\n\t\t\t\t\tconst hasImages = content.some((c) => c.type === \"image\");\n\t\t\t\t\tif (hasImages) {\n\t\t\t\t\t\tconst filteredContent = content\n\t\t\t\t\t\t\t.map((c) =>\n\t\t\t\t\t\t\t\tc.type === \"image\" ? { type: \"text\" as const, text: \"Image reading is disabled.\" } : c,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t.filter(\n\t\t\t\t\t\t\t\t(c, i, arr) =>\n\t\t\t\t\t\t\t\t\t// Dedupe consecutive \"Image reading is disabled.\" texts\n\t\t\t\t\t\t\t\t\t!(\n\t\t\t\t\t\t\t\t\t\tc.type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\tc.text === \"Image reading is disabled.\" &&\n\t\t\t\t\t\t\t\t\t\ti > 0 &&\n\t\t\t\t\t\t\t\t\t\tarr[i - 1].type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\t(arr[i - 1] as { type: \"text\"; text: string }).text === \"Image reading is disabled.\"\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\treturn { ...msg, content: filteredContent };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn msg;\n\t\t});\n\t};\n\n\tconst extensionRunnerRef: { current?: ExtensionRunner } = {};\n\n\t// Token-budget pressure for context GC: the fraction of the active model's\n\t// context window in use, measured from the real usage on the outgoing\n\t// message copy. It latches to a high-water mark so that our own evictions —\n\t// which shrink the next turn's measured usage — cannot oscillate a message\n\t// in and out of the transcript and thrash the provider's prefix cache. A\n\t// large drop (compaction, fork) resets the latch to the new, smaller size.\n\tlet budgetPressureHighWater = 0;\n\tconst getBudgetPressure = (contextMessages: AgentMessage[]): number => {\n\t\tconst contextWindow = agent.state.model?.contextWindow ?? 0;\n\t\tif (contextWindow <= 0) return 0;\n\t\tconst gauge = Math.min(estimateContextTokens(contextMessages).tokens / contextWindow, 1);\n\t\tif (gauge > budgetPressureHighWater) {\n\t\t\tbudgetPressureHighWater = gauge; // rising usage — track it\n\t\t} else if (gauge < budgetPressureHighWater - 0.15) {\n\t\t\tbudgetPressureHighWater = gauge; // context collapsed — reset the latch\n\t\t}\n\t\treturn budgetPressureHighWater;\n\t};\n\n\tagent = new Agent({\n\t\tinitialState: {\n\t\t\tsystemPrompt: \"\",\n\t\t\tmodel,\n\t\t\tthinkingLevel,\n\t\t\ttools: [],\n\t\t},\n\t\tconvertToLlm: convertToLlmWithBlockImages,\n\t\tcreateBackgroundResultMessage: createBackgroundTaskMessage,\n\t\tcreateBackgroundPlaceholder: (toolCall) => createBackgroundPlaceholderText(toolCall),\n\t\t// Report in-process background tool load (e.g. background MCP tools) to the\n\t\t// subagent lifeguard so it widens its heartbeat/timeout tolerance for\n\t\t// concurrently-monitored subagents. Peek (don't create) the pool: background\n\t\t// tools can run before any subagent is ever dispatched.\n\t\tonBackgroundTaskCountChange: (count) => peekSubagentPool()?.setExternalLoad(count),\n\t\tstreamFn: async (model, context, options) => {\n\t\t\tconst auth = await modelRegistry.getApiKeyAndHeaders(model);\n\t\t\tif (!auth.ok) {\n\t\t\t\tthrow new Error(auth.error);\n\t\t\t}\n\t\t\tconst providerRetrySettings = settingsManager.getProviderRetrySettings();\n\t\t\tconst attributionHeaders = getAttributionHeaders(model, settingsManager);\n\t\t\treturn streamSimple(model, context, {\n\t\t\t\t...options,\n\t\t\t\tapiKey: auth.apiKey,\n\t\t\t\ttimeoutMs: options?.timeoutMs ?? providerRetrySettings.timeoutMs,\n\t\t\t\tmaxRetries: options?.maxRetries ?? providerRetrySettings.maxRetries,\n\t\t\t\tmaxRetryDelayMs: options?.maxRetryDelayMs ?? providerRetrySettings.maxRetryDelayMs,\n\t\t\t\theaders:\n\t\t\t\t\tattributionHeaders || auth.headers || options?.headers\n\t\t\t\t\t\t? { ...attributionHeaders, ...auth.headers, ...options?.headers }\n\t\t\t\t\t\t: undefined,\n\t\t\t});\n\t\t},\n\t\tonPayload: async (payload, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"before_provider_request\")) {\n\t\t\t\treturn payload;\n\t\t\t}\n\t\t\treturn runner.emitBeforeProviderRequest(payload);\n\t\t},\n\t\tonResponse: async (response, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"after_provider_response\")) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait runner.emit({\n\t\t\t\ttype: \"after_provider_response\",\n\t\t\t\tstatus: response.status,\n\t\t\t\theaders: response.headers,\n\t\t\t});\n\t\t},\n\t\tsessionId: sessionManager.getSessionId(),\n\t\ttransformContext: async (messages) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tconst transformed = runner ? await runner.emitContext(messages) : messages;\n\t\t\tif (!settingsManager.getContextGcEnabled()) return transformed;\n\t\t\treturn evictSupersededReads(transformed, { cwd, budgetPressure: getBudgetPressure(transformed) });\n\t\t},\n\t\tsteeringMode: settingsManager.getSteeringMode(),\n\t\tfollowUpMode: settingsManager.getFollowUpMode(),\n\t\ttransport: settingsManager.getTransport(),\n\t\tthinkingBudgets: settingsManager.getThinkingBudgets(),\n\t\tthinkingDisplay: settingsManager.getThinkingDisplay(),\n\t\tmaxRetryDelayMs: settingsManager.getProviderRetrySettings().maxRetryDelayMs,\n\t});\n\n\t// Restore messages if session has existing data\n\tif (hasExistingSession) {\n\t\tagent.state.messages = existingSession.messages;\n\t\tif (!hasThinkingEntry) {\n\t\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t\t}\n\t} else {\n\t\t// Save initial model and thinking level for new sessions so they can be restored on resume\n\t\tif (model) {\n\t\t\tsessionManager.appendModelChange(model.provider, model.id);\n\t\t}\n\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t}\n\n\tconst session = new AgentSession({\n\t\tagent,\n\t\tsessionManager,\n\t\tsettingsManager,\n\t\tcwd,\n\t\tscopedModels: options.scopedModels,\n\t\tresourceLoader,\n\t\tcustomTools: options.customTools,\n\t\tbaseToolsOverride: options.baseToolsOverride,\n\t\tmodelRegistry,\n\t\tinitialActiveToolNames,\n\t\tallowedToolNames,\n\t\tdisallowedToolNames: options.disallowedTools,\n\t\textensionRunnerRef,\n\t\tsessionStartEvent: options.sessionStartEvent,\n\t});\n\tconst extensionsResult = resourceLoader.getExtensions();\n\n\treturn {\n\t\tsession,\n\t\textensionsResult,\n\t\tmodelFallbackMessage,\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"sdk.js","sourceRoot":"","sources":["../../src/core/sdk.ts"],"names":[],"mappings":"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","sourcesContent":["import { join } from \"node:path\";\nimport {\n\tAgent,\n\ttype AgentMessage,\n\ttype AgentTool,\n\tconvertToLlm,\n\tcreateBackgroundPlaceholderText,\n\tcreateBackgroundTaskMessage,\n\testimateContextTokens,\n\ttype ThinkingLevel,\n} from \"@kolisachint/hoocode-agent-core\";\nimport { clampThinkingLevel, type Message, type Model, streamSimple } from \"@kolisachint/hoocode-ai\";\nimport { getAgentDir } from \"../config.js\";\nimport { readConfig as readHooConfig } from \"../extensions/core/config.js\";\nimport { AgentSession } from \"./agent-session.js\";\nimport { formatNoModelsAvailableMessage } from \"./auth-guidance.js\";\nimport { AuthStorage } from \"./auth-storage.js\";\nimport { evictSupersededReads } from \"./context-gc.js\";\nimport { DEFAULT_THINKING_LEVEL } from \"./defaults.js\";\nimport type { ExtensionRunner, LoadExtensionsResult, SessionStartEvent, ToolDefinition } from \"./extensions/index.js\";\nimport { ModelRegistry } from \"./model-registry.js\";\nimport { defaultModelPerProvider, findInitialModel } from \"./model-resolver.js\";\nimport type { ResourceLoader } from \"./resource-loader.js\";\nimport { DefaultResourceLoader } from \"./resource-loader.js\";\nimport { getDefaultSessionDir, SessionManager } from \"./session-manager.js\";\nimport { SettingsManager } from \"./settings-manager.js\";\nimport { peekSubagentPool } from \"./subagent-pool-instance.js\";\nimport { isInstallTelemetryEnabled } from \"./telemetry.js\";\nimport { time } from \"./timings.js\";\nimport {\n\tcreateBashTool,\n\tcreateCodingTools,\n\tcreateEditTool,\n\tcreateFindTool,\n\tcreateGrepTool,\n\tcreateLsTool,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateWriteTool,\n\ttype ToolName,\n\twithFileMutationQueue,\n} from \"./tools/index.js\";\n\nexport interface CreateAgentSessionOptions {\n\t/** Working directory for project-local discovery. Default: process.cwd() */\n\tcwd?: string;\n\t/** Global config directory. Default: ~/.hoocode/agent */\n\tagentDir?: string;\n\n\t/** Auth storage for credentials. Default: AuthStorage.create(agentDir/auth.json) */\n\tauthStorage?: AuthStorage;\n\t/** Model registry. Default: ModelRegistry.create(authStorage, agentDir/models.json) */\n\tmodelRegistry?: ModelRegistry;\n\n\t/** Model to use. Default: from settings, else first available */\n\tmodel?: Model<any>;\n\t/** Thinking level. Default: from settings, else 'medium' (clamped to model capabilities) */\n\tthinkingLevel?: ThinkingLevel;\n\t/** Models available for cycling (Ctrl+P in interactive mode) */\n\tscopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;\n\n\t/**\n\t * Optional default tool suppression mode when no explicit allowlist is provided.\n\t *\n\t * - \"all\": start with no tools enabled\n\t * - \"builtin\": disable the default built-in tools (read, bash, edit, write)\n\t * but keep extension/custom tools enabled\n\t */\n\tnoTools?: \"all\" | \"builtin\";\n\t/**\n\t * Optional allowlist of tool names.\n\t *\n\t * When omitted, hoocode enables the default built-in tools (read, bash, edit, write)\n\t * and leaves extension/custom tools enabled unless `noTools` changes that default.\n\t * When provided, only the listed tool names are enabled.\n\t */\n\ttools?: string[];\n\t/**\n\t * Optional denylist of tool names, subtracted from whatever set is otherwise\n\t * enabled (allowlist or default). Applied to built-in, extension, and custom tools.\n\t */\n\tdisallowedTools?: string[];\n\t/**\n\t * Enable the built-in `webfetch` + `websearch` tools, which are defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list `webfetch`/`websearch` there instead). Network access is still gated\n\t * per call and filtered by `.webtoolsignore`.\n\t */\n\tenableWebTools?: boolean;\n\t/**\n\t * Enable the built-in `browser_run` + `browser_continue` tools, which drive the\n\t * `browsertools` deterministic browser engine (parent-in-the-loop). Defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list them there instead).\n\t */\n\tenableBrowserTools?: boolean;\n\t/**\n\t * Enable the built-in document tools — `DocRead`/`DocEdit`/`DocWrite` (extract\n\t * and lossless id-based editing) plus `DocScan`/`DocGrep`/`DocPeek` (the\n\t * token-sensitive discovery loop: outline, search, partial read). Defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list them there instead). They shell out to the `filetools` binary to\n\t * losslessly extract/edit structured/binary documents.\n\t */\n\tenableFileTools?: boolean;\n\t/**\n\t * Enable the semantic index layer for the built-in `search` tool (ranked\n\t * lexical + semantic retrieval, rank-fused). The search tool is active by\n\t * default; this flag only toggles the optional embedding index. When false,\n\t * search degrades to lexical-only. Ignored when an explicit `tools` allowlist\n\t * is provided (list it there instead).\n\t */\n\tenableEmbsearchTools?: boolean;\n\t/** Custom tools to register (in addition to built-in tools). */\n\tcustomTools?: ToolDefinition[];\n\t/**\n\t * Replace the built-in base tools entirely (the light preset uses this to\n\t * swap in short-schema read/write/edit/bash variants). Keys become the\n\t * default active tool set when no explicit `tools` allowlist is provided.\n\t */\n\tbaseToolsOverride?: Record<string, AgentTool>;\n\n\t/** Resource loader. When omitted, DefaultResourceLoader is used. */\n\tresourceLoader?: ResourceLoader;\n\n\t/** Session manager. Default: SessionManager.create(cwd) */\n\tsessionManager?: SessionManager;\n\n\t/** Settings manager. Default: SettingsManager.create(cwd, agentDir) */\n\tsettingsManager?: SettingsManager;\n\t/** Session start event metadata for extension runtime startup. */\n\tsessionStartEvent?: SessionStartEvent;\n}\n\n/** Result from createAgentSession */\nexport interface CreateAgentSessionResult {\n\t/** The created session */\n\tsession: AgentSession;\n\t/** Extensions result (for UI context setup in interactive mode) */\n\textensionsResult: LoadExtensionsResult;\n\t/** Warning if session was restored with a different model than saved */\n\tmodelFallbackMessage?: string;\n}\n\n// Re-exports\n\nexport type { AgentDefinition, AgentSource } from \"./agent-frontmatter.js\";\nexport { AgentRegistry, formatAgentsForPrompt, loadAgentRegistry } from \"./agent-registry.js\";\nexport * from \"./agent-session-runtime.js\";\nexport type {\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tExtensionFactory,\n\tSlashCommandInfo,\n\tSlashCommandSource,\n\tToolDefinition,\n} from \"./extensions/index.js\";\nexport type { PromptTemplate } from \"./prompt-templates.js\";\nexport type { Skill } from \"./skills.js\";\nexport type { Tool } from \"./tools/index.js\";\n\nexport {\n\twithFileMutationQueue,\n\t// Tool factories (for custom cwd)\n\tcreateCodingTools,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateBashTool,\n\tcreateEditTool,\n\tcreateWriteTool,\n\tcreateGrepTool,\n\tcreateFindTool,\n\tcreateLsTool,\n};\n\n// Helper Functions\n\nfunction getDefaultAgentDir(): string {\n\treturn getAgentDir();\n}\n\nfunction getAttributionHeaders(\n\tmodel: Model<any>,\n\tsettingsManager: SettingsManager,\n): Record<string, string> | undefined {\n\tif (!isInstallTelemetryEnabled(settingsManager)) {\n\t\treturn undefined;\n\t}\n\n\tif (model.provider === \"openrouter\" || model.baseUrl.includes(\"openrouter.ai\")) {\n\t\treturn {\n\t\t\t\"HTTP-Referer\": \"https://github.com/kolisachint/hoocode\",\n\t\t\t\"X-OpenRouter-Title\": \"hoocode\",\n\t\t\t\"X-OpenRouter-Categories\": \"cli-agent\",\n\t\t};\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Create an AgentSession with the specified options.\n *\n * @example\n * ```typescript\n * // Minimal - uses defaults\n * const { session } = await createAgentSession();\n *\n * // With explicit model\n * import { getModel } from '@kolisachint/hoocode-ai';\n * const { session } = await createAgentSession({\n * model: getModel('anthropic', 'claude-opus-4-5'),\n * thinkingLevel: 'high',\n * });\n *\n * // Continue previous session\n * const { session, modelFallbackMessage } = await createAgentSession({\n * continueSession: true,\n * });\n *\n * // Full control\n * const loader = new DefaultResourceLoader({\n * cwd: process.cwd(),\n * agentDir: getAgentDir(),\n * settingsManager: SettingsManager.create(),\n * });\n * await loader.reload();\n * const { session } = await createAgentSession({\n * model: myModel,\n * tools: [readTool, bashTool],\n * resourceLoader: loader,\n * sessionManager: SessionManager.inMemory(),\n * });\n * ```\n */\nexport async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {\n\tconst cwd = options.cwd ?? options.sessionManager?.getCwd() ?? process.cwd();\n\tconst agentDir = options.agentDir ?? getDefaultAgentDir();\n\tlet resourceLoader = options.resourceLoader;\n\n\t// Use provided or create AuthStorage and ModelRegistry\n\tconst authPath = options.agentDir ? join(agentDir, \"auth.json\") : undefined;\n\tconst modelsPath = options.agentDir ? join(agentDir, \"models.json\") : undefined;\n\tconst authStorage = options.authStorage ?? AuthStorage.create(authPath);\n\tconst modelRegistry = options.modelRegistry ?? ModelRegistry.create(authStorage, modelsPath);\n\n\tconst settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);\n\tconst sessionManager = options.sessionManager ?? SessionManager.create(cwd, getDefaultSessionDir(cwd, agentDir));\n\n\tif (!resourceLoader) {\n\t\tresourceLoader = new DefaultResourceLoader({ cwd, agentDir, settingsManager });\n\t\tawait resourceLoader.reload();\n\t\ttime(\"resourceLoader.reload\");\n\t}\n\n\t// Check if session has existing data to restore\n\tconst existingSession = sessionManager.buildSessionContext();\n\tconst hasExistingSession = existingSession.messages.length > 0;\n\tconst hasThinkingEntry = sessionManager.getBranch().some((entry) => entry.type === \"thinking_level_change\");\n\n\tlet model = options.model;\n\tlet modelFallbackMessage: string | undefined;\n\n\t// If session has data, try to restore model from it\n\tif (!model && hasExistingSession && existingSession.model) {\n\t\tconst restoredModel = modelRegistry.find(existingSession.model.provider, existingSession.model.modelId);\n\t\tif (restoredModel && modelRegistry.hasConfiguredAuth(restoredModel)) {\n\t\t\tmodel = restoredModel;\n\t\t}\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = `Could not restore model ${existingSession.model.provider}/${existingSession.model.modelId}`;\n\t\t}\n\t}\n\n\t// If still no model, use findInitialModel (checks settings default, then provider defaults)\n\tif (!model) {\n\t\t// The pi-layer settings.json default wins; when it is unset, fall back to the\n\t\t// hoo-config.json `llm.default_provider` so the seeded/user default actually\n\t\t// takes effect. findInitialModel only honours it if that provider has auth.\n\t\tconst hooLlm = readHooConfig().llm;\n\t\tconst defaultProvider = settingsManager.getDefaultProvider() ?? hooLlm?.default_provider;\n\t\tconst defaultModelId =\n\t\t\tsettingsManager.getDefaultModel() ??\n\t\t\t(settingsManager.getDefaultProvider()\n\t\t\t\t? undefined\n\t\t\t\t: (hooLlm?.default_model ??\n\t\t\t\t\t(defaultProvider && defaultProvider in defaultModelPerProvider\n\t\t\t\t\t\t? defaultModelPerProvider[defaultProvider as keyof typeof defaultModelPerProvider]\n\t\t\t\t\t\t: undefined)));\n\t\tconst result = await findInitialModel({\n\t\t\tscopedModels: [],\n\t\t\tisContinuing: hasExistingSession,\n\t\t\tdefaultProvider,\n\t\t\tdefaultModelId,\n\t\t\tdefaultThinkingLevel: settingsManager.getDefaultThinkingLevel(),\n\t\t\tmodelRegistry,\n\t\t});\n\t\tmodel = result.model;\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = formatNoModelsAvailableMessage();\n\t\t} else if (modelFallbackMessage) {\n\t\t\tmodelFallbackMessage += `. Using ${model.provider}/${model.id}`;\n\t\t}\n\t}\n\n\tlet thinkingLevel = options.thinkingLevel;\n\n\t// If session has data, restore thinking level from it\n\tif (thinkingLevel === undefined && hasExistingSession) {\n\t\tthinkingLevel = hasThinkingEntry\n\t\t\t? (existingSession.thinkingLevel as ThinkingLevel)\n\t\t\t: (settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL);\n\t}\n\n\t// Fall back to settings default\n\tif (thinkingLevel === undefined) {\n\t\tthinkingLevel = settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;\n\t}\n\n\t// Clamp to model capabilities\n\tif (!model) {\n\t\tthinkingLevel = \"off\";\n\t} else {\n\t\tthinkingLevel = clampThinkingLevel(model, thinkingLevel) as ThinkingLevel;\n\t}\n\n\t// `search` is always active: it answers \"find where X lives\" with ranked\n\t// results and degrades to grep-backed lexical retrieval when no semantic\n\t// index is present, so it needs no binary to be useful. The\n\t// `enableEmbsearchTools` flag only controls whether the semantic index is\n\t// built and fused in (see main.ts) — not whether the tool exists.\n\tconst defaultActiveToolNames: ToolName[] = [\"read\", \"bash\", \"edit\", \"write\", \"grep\", \"find\", \"ls\", \"search\"];\n\t// Web tools are registered as base tools but inactive by default; opt-in adds\n\t// them to the default active set. An explicit allowlist (`tools`) takes over\n\t// fully, so callers must list them there to enable in that mode.\n\tconst optInActiveToolNames: ToolName[] = [\n\t\t...(options.enableWebTools ? [\"webfetch\", \"websearch\"] : []),\n\t\t...(options.enableBrowserTools ? [\"browser_run\", \"browser_continue\"] : []),\n\t\t...(options.enableFileTools ? [\"DocRead\", \"DocEdit\", \"DocWrite\", \"DocScan\", \"DocGrep\", \"DocPeek\"] : []),\n\t] as ToolName[];\n\tconst allowedToolNames = options.tools ?? (options.noTools === \"all\" ? [] : undefined);\n\tconst initialActiveToolNames: string[] = options.tools\n\t\t? [...options.tools]\n\t\t: options.noTools\n\t\t\t? []\n\t\t\t: [...defaultActiveToolNames, ...optInActiveToolNames];\n\n\tlet agent: Agent;\n\n\t// Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)\n\tconst convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {\n\t\tconst converted = convertToLlm(messages);\n\t\t// Check setting dynamically so mid-session changes take effect\n\t\tif (!settingsManager.getBlockImages()) {\n\t\t\treturn converted;\n\t\t}\n\t\t// Filter out ImageContent from all messages, replacing with text placeholder\n\t\treturn converted.map((msg) => {\n\t\t\tif (msg.role === \"user\" || msg.role === \"toolResult\") {\n\t\t\t\tconst content = msg.content;\n\t\t\t\tif (Array.isArray(content)) {\n\t\t\t\t\tconst hasImages = content.some((c) => c.type === \"image\");\n\t\t\t\t\tif (hasImages) {\n\t\t\t\t\t\tconst filteredContent = content\n\t\t\t\t\t\t\t.map((c) =>\n\t\t\t\t\t\t\t\tc.type === \"image\" ? { type: \"text\" as const, text: \"Image reading is disabled.\" } : c,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t.filter(\n\t\t\t\t\t\t\t\t(c, i, arr) =>\n\t\t\t\t\t\t\t\t\t// Dedupe consecutive \"Image reading is disabled.\" texts\n\t\t\t\t\t\t\t\t\t!(\n\t\t\t\t\t\t\t\t\t\tc.type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\tc.text === \"Image reading is disabled.\" &&\n\t\t\t\t\t\t\t\t\t\ti > 0 &&\n\t\t\t\t\t\t\t\t\t\tarr[i - 1].type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\t(arr[i - 1] as { type: \"text\"; text: string }).text === \"Image reading is disabled.\"\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\treturn { ...msg, content: filteredContent };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn msg;\n\t\t});\n\t};\n\n\tconst extensionRunnerRef: { current?: ExtensionRunner } = {};\n\n\t// Token-budget pressure for context GC: the fraction of the active model's\n\t// context window in use, measured from the real usage on the outgoing\n\t// message copy. It latches to a high-water mark so that our own evictions —\n\t// which shrink the next turn's measured usage — cannot oscillate a message\n\t// in and out of the transcript and thrash the provider's prefix cache. A\n\t// large drop (compaction, fork) resets the latch to the new, smaller size.\n\tlet budgetPressureHighWater = 0;\n\tconst getBudgetPressure = (contextMessages: AgentMessage[]): number => {\n\t\tconst contextWindow = agent.state.model?.contextWindow ?? 0;\n\t\tif (contextWindow <= 0) return 0;\n\t\tconst gauge = Math.min(estimateContextTokens(contextMessages).tokens / contextWindow, 1);\n\t\tif (gauge > budgetPressureHighWater) {\n\t\t\tbudgetPressureHighWater = gauge; // rising usage — track it\n\t\t} else if (gauge < budgetPressureHighWater - 0.15) {\n\t\t\tbudgetPressureHighWater = gauge; // context collapsed — reset the latch\n\t\t}\n\t\treturn budgetPressureHighWater;\n\t};\n\n\tagent = new Agent({\n\t\tinitialState: {\n\t\t\tsystemPrompt: \"\",\n\t\t\tmodel,\n\t\t\tthinkingLevel,\n\t\t\ttools: [],\n\t\t},\n\t\tconvertToLlm: convertToLlmWithBlockImages,\n\t\tcreateBackgroundResultMessage: createBackgroundTaskMessage,\n\t\tcreateBackgroundPlaceholder: (toolCall) => createBackgroundPlaceholderText(toolCall),\n\t\t// Report in-process background tool load (e.g. background MCP tools) to the\n\t\t// subagent lifeguard so it widens its heartbeat/timeout tolerance for\n\t\t// concurrently-monitored subagents. Peek (don't create) the pool: background\n\t\t// tools can run before any subagent is ever dispatched.\n\t\tonBackgroundTaskCountChange: (count) => peekSubagentPool()?.setExternalLoad(count),\n\t\tstreamFn: async (model, context, options) => {\n\t\t\tconst auth = await modelRegistry.getApiKeyAndHeaders(model);\n\t\t\tif (!auth.ok) {\n\t\t\t\tthrow new Error(auth.error);\n\t\t\t}\n\t\t\tconst providerRetrySettings = settingsManager.getProviderRetrySettings();\n\t\t\tconst attributionHeaders = getAttributionHeaders(model, settingsManager);\n\t\t\treturn streamSimple(model, context, {\n\t\t\t\t...options,\n\t\t\t\tapiKey: auth.apiKey,\n\t\t\t\ttimeoutMs: options?.timeoutMs ?? providerRetrySettings.timeoutMs,\n\t\t\t\tmaxRetries: options?.maxRetries ?? providerRetrySettings.maxRetries,\n\t\t\t\tmaxRetryDelayMs: options?.maxRetryDelayMs ?? providerRetrySettings.maxRetryDelayMs,\n\t\t\t\theaders:\n\t\t\t\t\tattributionHeaders || auth.headers || options?.headers\n\t\t\t\t\t\t? { ...attributionHeaders, ...auth.headers, ...options?.headers }\n\t\t\t\t\t\t: undefined,\n\t\t\t});\n\t\t},\n\t\tonPayload: async (payload, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"before_provider_request\")) {\n\t\t\t\treturn payload;\n\t\t\t}\n\t\t\treturn runner.emitBeforeProviderRequest(payload);\n\t\t},\n\t\tonResponse: async (response, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"after_provider_response\")) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait runner.emit({\n\t\t\t\ttype: \"after_provider_response\",\n\t\t\t\tstatus: response.status,\n\t\t\t\theaders: response.headers,\n\t\t\t});\n\t\t},\n\t\tsessionId: sessionManager.getSessionId(),\n\t\ttransformContext: async (messages) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tconst transformed = runner ? await runner.emitContext(messages) : messages;\n\t\t\tif (!settingsManager.getContextGcEnabled()) return transformed;\n\t\t\treturn evictSupersededReads(transformed, { cwd, budgetPressure: getBudgetPressure(transformed) });\n\t\t},\n\t\tsteeringMode: settingsManager.getSteeringMode(),\n\t\tfollowUpMode: settingsManager.getFollowUpMode(),\n\t\ttransport: settingsManager.getTransport(),\n\t\tthinkingBudgets: settingsManager.getThinkingBudgets(),\n\t\tthinkingDisplay: settingsManager.getThinkingDisplay(),\n\t\tmaxRetryDelayMs: settingsManager.getProviderRetrySettings().maxRetryDelayMs,\n\t});\n\n\t// Restore messages if session has existing data\n\tif (hasExistingSession) {\n\t\tagent.state.messages = existingSession.messages;\n\t\tif (!hasThinkingEntry) {\n\t\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t\t}\n\t} else {\n\t\t// Save initial model and thinking level for new sessions so they can be restored on resume\n\t\tif (model) {\n\t\t\tsessionManager.appendModelChange(model.provider, model.id);\n\t\t}\n\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t}\n\n\tconst session = new AgentSession({\n\t\tagent,\n\t\tsessionManager,\n\t\tsettingsManager,\n\t\tcwd,\n\t\tscopedModels: options.scopedModels,\n\t\tresourceLoader,\n\t\tcustomTools: options.customTools,\n\t\tbaseToolsOverride: options.baseToolsOverride,\n\t\tmodelRegistry,\n\t\tinitialActiveToolNames,\n\t\tallowedToolNames,\n\t\tdisallowedToolNames: options.disallowedTools,\n\t\textensionRunnerRef,\n\t\tsessionStartEvent: options.sessionStartEvent,\n\t});\n\tconst extensionsResult = resourceLoader.getExtensions();\n\n\treturn {\n\t\tsession,\n\t\textensionsResult,\n\t\tmodelFallbackMessage,\n\t};\n}\n"]}
|
|
@@ -6,7 +6,7 @@ import { type Skill } from "./skills.js";
|
|
|
6
6
|
export interface BuildSystemPromptOptions {
|
|
7
7
|
/** Custom system prompt (replaces default). */
|
|
8
8
|
customPrompt?: string;
|
|
9
|
-
/** Tools to include in prompt. Default: [read, bash, edit, write, grep, find, ls] */
|
|
9
|
+
/** Tools to include in prompt. Default: [read, bash, edit, write, search, grep, find, ls] */
|
|
10
10
|
selectedTools?: string[];
|
|
11
11
|
/** Optional one-line tool snippets keyed by tool name. */
|
|
12
12
|
toolSnippets?: Record<string, string>;
|