From ba4a57c7d19c3119f1f3876a8472b758902ddc9d Mon Sep 17 00:00:00 2001 From: Hypatia May Date: Thu, 16 Jul 2026 18:51:26 +0800 Subject: [PATCH] refactor(compact): align pruning contracts (round 3) --- docs/architecture.md | 2 +- docs/capability-seams.md | 6 ++-- docs/config-catalog.md | 32 +++++++++---------- docs/cordis-catalog/services.md | 2 +- docs/core-data-structures/compaction.md | 2 +- docs/module-graph.md | 12 +++---- .../2026-06-18-session-surface.md | 2 +- ...n-pressure-and-overflow-recovery.i18n.yaml | 4 +-- ...mpaction-pressure-and-overflow-recovery.md | 6 ++-- ...ction-pressure-and-overflow-recovery.zh.md | 6 ++-- .../2026-06-18-compaction-capability-seam.md | 14 ++++---- docs/tool-catalog.md | 4 +-- examples/coding-agent/composition.md | 4 +-- examples/coding-agent/cordis.yml | 2 +- examples/coding-agent/tests/harness.ts | 2 +- packages/compact/README.md | 4 +-- packages/compact/compact-basic/README.md | 10 +++--- packages/compact/compact-basic/package.json | 6 ++-- packages/compact/compact-basic/src/index.ts | 2 +- .../compact-basic/tests/compact-basic.spec.ts | 2 +- .../tests/loader-composition.spec.ts | 6 ++-- packages/compact/compact-basic/tsconfig.json | 2 +- .../README.md | 4 +-- .../package.json | 2 +- .../src/config.ts | 0 .../src/index.ts | 2 +- .../src/types.ts | 0 .../tests/loader-composition.spec.ts | 10 +++--- .../tests/tool-result-prune.spec.ts | 4 +-- .../tsconfig.json | 0 packages/compact/compact/README.md | 2 +- .../core/tools/tests/gen-tool-catalog.spec.ts | 5 --- pnpm-lock.yaml | 10 +++--- python/sdk-runtime/package.json | 2 +- scripts/gen-doc-graphs.ts | 2 +- scripts/gen-tool-catalog.ts | 29 ++++++----------- tsconfig.build.json | 2 +- tsconfig.json | 2 +- 38 files changed, 97 insertions(+), 111 deletions(-) rename packages/compact/{tool-result-prune => compact-tool-result-prune}/README.md (96%) rename packages/compact/{tool-result-prune => compact-tool-result-prune}/package.json (94%) rename packages/compact/{tool-result-prune => compact-tool-result-prune}/src/config.ts (100%) rename packages/compact/{tool-result-prune => compact-tool-result-prune}/src/index.ts (99%) rename packages/compact/{tool-result-prune => compact-tool-result-prune}/src/types.ts (100%) rename packages/compact/{tool-result-prune => compact-tool-result-prune}/tests/loader-composition.spec.ts (84%) rename packages/compact/{tool-result-prune => compact-tool-result-prune}/tests/tool-result-prune.spec.ts (98%) rename packages/compact/{tool-result-prune => compact-tool-result-prune}/tsconfig.json (100%) diff --git a/docs/architecture.md b/docs/architecture.md index ff45d38389..6c4fd39622 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -32,7 +32,7 @@ A harness is one [Cordis](cordis-primer.md) context. Packages add services (`ctx | `ctx.skills` | [`skill/`](../packages/skill/README.md) | skill provider registry and progressive disclosure | | `ctx.web` | [`web/`](../packages/web/README.md) | search/fetch provider registries | | `ctx.compact` | [`compact/`](../packages/compact/README.md) | session-log compaction | -| `ctx.toolResultPrune` | [`compact/tool-result-prune`](../packages/compact/tool-result-prune/README.md) | optional model-free tool-result pruning | +| `ctx.toolResultPrune` | [`compact/compact-tool-result-prune`](../packages/compact/compact-tool-result-prune/README.md) | optional model-free tool-result pruning | | `ctx.subagents` | [`subagent/`](../packages/subagent/README.md) | named delegation providers | | `ctx.tasks` | [`tasks/`](../packages/tasks/README.md) | background task registry + generic `task_*` control tools | | `ctx.workflows` | [`workflow/`](../packages/workflow/README.md) | script-driven multi-agent orchestration | diff --git a/docs/capability-seams.md b/docs/capability-seams.md index 816fc0ea3b..4ce3619025 100644 --- a/docs/capability-seams.md +++ b/docs/capability-seams.md @@ -16,7 +16,7 @@ flowchart LR pkg_compact_basic["compact-basic"] pkg_token_meter["token-meter"] svc_tokenMeter["ctx.tokenMeter
Replay token measurement"] - pkg_tool_result_prune["tool-result-prune"] + pkg_compact_tool_result_prune["compact-tool-result-prune"] svc_toolResultPrune["ctx.toolResultPrune
Model-free tool-result pruning"] pkg_session["session"] svc_sessions["ctx.sessions
In-memory session store"] @@ -104,6 +104,7 @@ flowchart LR pkg_code_runtime_worker --> svc_codeRuntime pkg_compact --> svc_compact pkg_compact_basic --> svc_compact + pkg_compact_tool_result_prune --> svc_toolResultPrune pkg_fs --> svc_fs pkg_fs_local --> svc_fs pkg_llm --> svc_llm @@ -129,7 +130,6 @@ flowchart LR pkg_system_prompt --> svc_systemPrompt pkg_tasks --> svc_tasks pkg_token_meter --> svc_tokenMeter - pkg_tool_result_prune --> svc_toolResultPrune pkg_tools --> svc_tools pkg_user_interaction --> svc_userInteraction pkg_web --> svc_web @@ -199,7 +199,7 @@ flowchart LR | --- | --- | --- | --- | --- | --- | --- | | `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-replay`](../packages/support/llm-replay) | [`agent-loop`](../packages/core/agent-loop), [`compact-basic`](../packages/compact/compact-basic) | - | Adapters register provider implementations; the loop and compaction call the provider-neutral stream service. | | `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compact-basic`](../packages/compact/compact-basic) | - | Owns isolated per-session replay folds; pressure consumers share immutable revisioned measurements. | -| `ctx.toolResultPrune` | `core` | [`tool-result-prune`](../packages/compact/tool-result-prune) | - | [`compact-basic`](../packages/compact/compact-basic) | - | Rewrites oversized current tool results through replayable single-node surface replacements before summary compaction. | +| `ctx.toolResultPrune` | `core` | [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | - | [`compact-basic`](../packages/compact/compact-basic) | - | Rewrites oversized current tool results through replayable single-node surface replacements before summary compaction. | | `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-query`](../packages/session-query/session-query), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`invariants`](../packages/support/invariants) | - | Owns append-only Session instances and emits the durable session event feed. | | `ctx.sessionPersistence` | `seam` | [`session-persistence`](../packages/session-persistence/session-persistence) | [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`session-persistence-sqlite`](../packages/session-persistence/session-persistence-sqlite) | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/ui/acp), [`session-query`](../packages/session-query/session-query) | - | Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time. | | `ctx.sessionQuery` | `seam` | [`session-query`](../packages/session-query/session-query) | - | - | - | Resolves live and optional persisted logs into one logical corpus for exact reads. | diff --git a/docs/config-catalog.md b/docs/config-catalog.md index cb266a8119..cb338a9c75 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -245,6 +245,22 @@ export interface BasicCompactConfig { Source: [`packages/compact/compact-basic/src/types.ts:8`](../packages/compact/compact-basic/src/types.ts) +## `@deepseek-ai/dsh-compact-tool-result-prune` + +```ts config-catalog +/** Character-budget policy for deterministic tool-result pruning. */ +export interface ToolResultPruneConfig { + /** Prune when total text exceeds this many Unicode code points. Defaults to `8192`. */ + thresholdChars?: number + /** Maximum leading Unicode code points retained. Defaults to `4096`. */ + headChars?: number + /** Maximum trailing Unicode code points retained. Defaults to `1024`. */ + tailChars?: number +} +``` + +Source: [`packages/compact/compact-tool-result-prune/src/types.ts:4`](../packages/compact/compact-tool-result-prune/src/types.ts) + ## `@deepseek-ai/dsh-fs-local` ```ts config-catalog @@ -930,22 +946,6 @@ export interface Config { Source: [`packages/fs/tool-fs/src/index.ts:22`](../packages/fs/tool-fs/src/index.ts) -## `@deepseek-ai/dsh-tool-result-prune` - -```ts config-catalog -/** Character-budget policy for deterministic tool-result pruning. */ -export interface ToolResultPruneConfig { - /** Prune when total text exceeds this many Unicode code points. Defaults to `8192`. */ - thresholdChars?: number - /** Maximum leading Unicode code points retained. Defaults to `4096`. */ - headChars?: number - /** Maximum trailing Unicode code points retained. Defaults to `1024`. */ - tailChars?: number -} -``` - -Source: [`packages/compact/tool-result-prune/src/types.ts:4`](../packages/compact/tool-result-prune/src/types.ts) - ## `@deepseek-ai/dsh-tool-skill` Requires: `tools` · `skills` diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index e2fc41b943..2a5f37cacb 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -284,7 +284,7 @@ pruneSession(session: Session): PruneResult Types: [ContentBlock](../core-data-structures/core.md) -Source: [`packages/compact/tool-result-prune/src/index.ts:39`](../../packages/compact/tool-result-prune/src/index.ts) +Source: [`packages/compact/compact-tool-result-prune/src/index.ts:39`](../../packages/compact/compact-tool-result-prune/src/index.ts) ## `ctx.tools` — `ToolRegistry` diff --git a/docs/core-data-structures/compaction.md b/docs/core-data-structures/compaction.md index e3774b87cd..5981ed2d0a 100644 --- a/docs/core-data-structures/compaction.md +++ b/docs/core-data-structures/compaction.md @@ -58,6 +58,6 @@ export type CompactionTrigger = 'pressure' | 'context-overflow' `CompactService` exposes `compactIfNeeded(agent, trigger, signal)` for automatic `pressure` or `context-overflow` policy, returning `null` when no safe work exists, and `compactRegion(...)` for an explicit inclusive surface range. Implementations must forward the supplied signal to summarization. The seam owns no pricing API: the singleton [`ctx.tokenMeter`](token-meter.md) directly owns estimation and replay, while `dsh-compact-basic` owns retention, event sequencing, routed summarization calls, and their configuration. -Pressure compaction runs at serial `agent/post-step`, after successful assistant output, tool results, buffered context, and steering are durable but before `step/end`. Once pressure or canonical overflow qualifies, compact-basic invokes optional [`ctx.toolResultPrune`](../../packages/compact/tool-result-prune/README.md) before range selection, remeasures through `ctx.tokenMeter`, and can advance the surface without a summary. Failed-request recovery runs through `agent/request-error` after the failed step closes and authorizes a fresh numbered-step retry only when the surface replacement generation advances, even if later summary work throws after pruning; cancellation still wins. Region boundaries preserve tool-call/result pairing but not whole turns, allowing early closed steps of one oversized turn to compact. `dsh-compact-basic` owns thresholds, retained-tail policy, overflow caps, and failure handling. +Pressure compaction runs at serial `agent/post-step`, after successful assistant output, tool results, buffered context, and steering are durable but before `step/end`. Once pressure or canonical overflow qualifies, compact-basic invokes optional [`ctx.toolResultPrune`](../../packages/compact/compact-tool-result-prune/README.md) before range selection, remeasures through `ctx.tokenMeter`, and can advance the surface without a summary. Failed-request recovery runs through `agent/request-error` after the failed step closes and authorizes a fresh numbered-step retry only when the surface replacement generation advances, even if later summary work throws after pruning; cancellation still wins. Region boundaries preserve tool-call/result pairing but not whole turns, allowing early closed steps of one oversized turn to compact. `dsh-compact-basic` owns thresholds, retained-tail policy, overflow caps, and failure handling. The seam exports `toolPairingBalancedBefore(session, node)` and `toolPairingBalancedAfter(session, node)` for those edge checks. Both validate current surface membership, reject stale or missing seqs and orphan results, and ignore a caller-retained `node.next`; the [package contract](../../packages/compact/compact/README.md#tool-pairing-boundaries) owns their cache semantics. diff --git a/docs/module-graph.md b/docs/module-graph.md index fbe9afd59b..685bd810ce 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -45,7 +45,7 @@ flowchart TD subgraph group_compact["packages/compact"] pkg_compact["compact"] pkg_compact_basic["compact-basic"] - pkg_tool_result_prune["tool-result-prune"] + pkg_compact_tool_result_prune["compact-tool-result-prune"] end subgraph group_subagent["packages/subagent"] pkg_subagent["subagent"] @@ -169,8 +169,8 @@ flowchart TD pkg_skill_local --> pkg_skill pkg_compact --> pkg_llm pkg_compact --> pkg_session - pkg_tool_result_prune --> pkg_llm - pkg_tool_result_prune --> pkg_session + pkg_compact_tool_result_prune --> pkg_llm + pkg_compact_tool_result_prune --> pkg_session pkg_web_fetch_local --> pkg_timeout pkg_web_fetch_local --> pkg_web pkg_web_search_deepseek --> pkg_web @@ -185,10 +185,10 @@ flowchart TD pkg_bash_local --> pkg_timeout pkg_compact_basic --> pkg_agent pkg_compact_basic --> pkg_compact + pkg_compact_basic --> pkg_compact_tool_result_prune pkg_compact_basic --> pkg_llm pkg_compact_basic --> pkg_session pkg_compact_basic --> pkg_token_meter - pkg_compact_basic --> pkg_tool_result_prune pkg_hook_protocol --> pkg_bash pkg_hook_protocol --> pkg_session pkg_session_persistence_jsonl --> pkg_session @@ -411,7 +411,7 @@ flowchart TD | [`fs-policy`](../packages/fs/fs-policy) | `fs` | [`fs`](../packages/fs/fs) | | [`skill-local`](../packages/skill/skill-local) | `skill` | [`fs`](../packages/fs/fs), [`skill`](../packages/skill/skill) | | [`compact`](../packages/compact/compact) | `compact` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | -| [`tool-result-prune`](../packages/compact/tool-result-prune) | `compact` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | +| [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | `compact` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`web-fetch-local`](../packages/web/web-fetch-local) | `web` | [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) | | [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`web`](../packages/web/web) | | [`web-search-exa`](../packages/web/web-search-exa) | `web` | [`web`](../packages/web/web) | @@ -420,7 +420,7 @@ flowchart TD | [`llm-replay`](../packages/support/llm-replay) | `support` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) | | [`bash-local`](../packages/bash/bash-local) | `bash` | [`bash`](../packages/bash/bash), [`timeout`](../packages/util/timeout) | -| [`compact-basic`](../packages/compact/compact-basic) | `compact` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter), [`tool-result-prune`](../packages/compact/tool-result-prune) | +| [`compact-basic`](../packages/compact/compact-basic) | `compact` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | | [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`bash`](../packages/bash/bash), [`session`](../packages/core/session) | | [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl) | `session-persistence` | [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence) | | [`session-persistence-sqlite`](../packages/session-persistence/session-persistence-sqlite) | `session-persistence` | [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence) | diff --git a/docs/rfc/implemented/architecture/2026-06-18-session-surface.md b/docs/rfc/implemented/architecture/2026-06-18-session-surface.md index 3a8805ddb6..38f1085b81 100644 --- a/docs/rfc/implemented/architecture/2026-06-18-session-surface.md +++ b/docs/rfc/implemented/architecture/2026-06-18-session-surface.md @@ -66,4 +66,4 @@ Every surface-eligible event must carry `surfaceOp` or it would disappear from d - **`packages/session-persistence/session-persistence-jsonl`**: No changes required. - **`packages/session-persistence/session-persistence`**: Abstract interface unchanged. -The surface is the foundation for future history manipulation. A compaction or tool-result-prune plugin appends one of the existing message-producing event types (a `user/message` carrying the summary, say) with `surfaceOp: { op: 'replace', start, end }` and `sourceEventSeqs` covering the shadowed nodes — the new node takes the range's place on the surface while the plugin's own trace events (e.g. `compaction/start`, `compaction/end`) stay off it. Replay preserves the decision deterministically. +The surface is the foundation for future history manipulation. A compaction or compact-tool-result-prune plugin appends one of the existing message-producing event types (a `user/message` carrying the summary, say) with `surfaceOp: { op: 'replace', start, end }` and `sourceEventSeqs` covering the shadowed nodes — the new node takes the range's place on the surface while the plugin's own trace events (e.g. `compaction/start`, `compaction/end`) stay off it. Replay preserves the decision deterministically. diff --git a/docs/rfc/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.i18n.yaml b/docs/rfc/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.i18n.yaml index c8eef0f1c9..6f6ca74dd2 100644 --- a/docs/rfc/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md: deedb81f8cf75ab80b70e2ef3148ba76d1278886 -2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md: 51d28fa243f522eedcac29002670575889d48369 +2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md: ea1e2b21c8036ab4d89c022792f252becc3826f6 +2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md: 594f396aaf891dccb9d6545e78039f0dbba51d1a diff --git a/docs/rfc/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md b/docs/rfc/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md index deedb81f8c..ea1e2b21c8 100644 --- a/docs/rfc/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md +++ b/docs/rfc/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md @@ -16,9 +16,9 @@ Successful calls are not the only pressure signal. A provider can reject a reque `agent/pre-step` is narrowed to `(agent, turn, step, signal)`. It remains a generic serial checkpoint before `step/start`, but it carries no compaction-only prompt or prefix fields. -The loop fires awaited serial `agent/post-step(agent, turn, step, signal)` after assistant output, every dispatched or synthetic tool result, post-tool context, and steering are durable, but before `step/end`. This placement gives pressure policy the complete successful-call state without splitting an assistant tool call from its result. A listener failure is an ordinary turn failure; it never enters model-request recovery. +The loop fires awaited serial `agent/post-step(agent, turn, step, signal)` after assistant output, every dispatched or synthetic tool result, post-tool context, and steering are durable, but before `step/end`. This placement gives pressure policy the complete successful-call state without splitting an assistant tool call from its result. A propagated listener failure is an ordinary turn failure; it never enters model-request recovery. Compact-basic contains its expected operational failures as described below. -`dsh-compact-basic` reads the exact latest routed model from the durable request header only to establish that a completed route exists, then asks the singleton `ctx.tokenMeter` to measure the canonical logged envelope and current surface. It does not fall back to `AgentOptions.model` for automatic pressure. A headerless session has no completed routed request to assess and produces no work; any durable non-empty model name uses the same estimator. Operational measurement or summarization failures warn and continue with full history. +`dsh-compact-basic` reads the exact latest routed model from the durable request header only to establish that a completed route exists, then asks the singleton `ctx.tokenMeter` to measure the canonical logged envelope and current surface. It does not fall back to `AgentOptions.model` for automatic pressure. A headerless session has no completed routed request to assess and produces no work; any durable non-empty model name uses the same estimator. Operational measurement or summarization failures warn and continue from the latest durable surface: full history before any replacement, or the pruned surface if pruning already landed. ### Request recovery is limited to the final model boundary @@ -58,6 +58,6 @@ Compact tests pin low-friction service-wide defaults, actual routed-model select Pressure describes the actual completed routed request, including durable tool results and request-only prefix fields, rather than a provisional next-call guess. Optional model-free pruning removes predictable tool-output bulk before summary selection and can independently create retry-worthy progress. Canonical overflow supplies the backstop when no successful usage anchor exists. Recovery is bounded, cancellation-owned, and monotonic: it retries only after a visible surface generation change. -The cost is one additional serial checkpoint on successful steps and adapter-maintained overflow classification. Provider wording and heuristic character density remain maintenance risks. Surface compaction still cannot repair an envelope that alone exceeds the window or split one indivisible oversized message/tool unit. +The cost is one additional serial checkpoint on successful steps and adapter-maintained overflow classification. Provider wording and heuristic character density remain maintenance risks. Surface compaction still cannot repair an envelope that alone exceeds the window, split an indivisible non-tool node, or repair a tool unit whose non-prunable remainder remains oversized. The optional pruner can repair an otherwise indivisible tool pair when removable text-bearing tool-result content is the bulk. This RFC supersedes only the pre-step automatic-trigger portion of the [compaction capability-seam RFC](../feature/2026-06-18-compaction-capability-seam.md). The service split, standalone token meter, balanced range contract, log-recorded lock, summary replacement, and sole `summarize()` subclass hook remain unchanged. diff --git a/docs/rfc/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md b/docs/rfc/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md index 51d28fa243..594f396aaf 100644 --- a/docs/rfc/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md +++ b/docs/rfc/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md @@ -16,9 +16,9 @@ Status: implemented `agent/pre-step` 收窄为 `(agent, turn, step, signal)`。它仍是 `step/start` 之前的通用串行检查点,但不再携带压缩专用的提示词或前缀字段。 -循环在 assistant 输出、所有已分发或合成的工具结果、工具后上下文与 steering 都持久化之后、`step/end` 之前,触发等待式串行 `agent/post-step(agent, turn, step, signal)`。该位置让压力策略看到完整的成功调用状态,同时不会拆开 assistant 工具调用与其结果。监听器失败属于普通 turn 失败,绝不会进入模型请求恢复。 +循环在 assistant 输出、所有已分发或合成的工具结果、工具后上下文与 steering 都持久化之后、`step/end` 之前,触发等待式串行 `agent/post-step(agent, turn, step, signal)`。该位置让压力策略看到完整的成功调用状态,同时不会拆开 assistant 工具调用与其结果。向外传播的监听器失败属于普通 turn 失败,绝不会进入模型请求恢复;compact-basic 会按下文所述在内部处理其预期的操作性失败。 -`dsh-compact-basic` 从持久请求头读取精确的最新实际路由模型,只用它确认已经存在完整路由,随后让单例 `ctx.tokenMeter` 计量规范日志信封与当前表层。自动压力不会回退到 `AgentOptions.model`。没有请求头的会话尚无已完成路由请求可供判断,因此不执行工作;任意持久记录的非空模型名都使用同一个估算器。操作性的计量或摘要失败会发出警告,并继续使用完整历史。 +`dsh-compact-basic` 从持久请求头读取精确的最新实际路由模型,只用它确认已经存在完整路由,随后让单例 `ctx.tokenMeter` 计量规范日志信封与当前表层。自动压力不会回退到 `AgentOptions.model`。没有请求头的会话尚无已完成路由请求可供判断,因此不执行工作;任意持久记录的非空模型名都使用同一个估算器。操作性的计量或摘要失败会发出警告,并从最新持久表层继续:任何替换发生前使用完整历史;若剪枝已经落盘,则使用已剪枝表层。 ### 请求恢复只覆盖最终模型边界 @@ -58,6 +58,6 @@ Status: implemented 压力描述实际完成的路由请求,包括持久工具结果与仅请求前缀字段,而不是对下一次调用的临时猜测。可选的无模型剪枝会在选择摘要前移除可预测的工具输出体积,也能独立产生足以重试的进展。当成功 usage 锚点不存在时,规范化溢出提供兜底路径。恢复有上限、受取消所有,并保持单调:只有模型可见的表层 generation 变化后才重试。 -代价是成功 step 增加一个串行检查点,并需要适配器持续维护溢出分类。提供方措辞与启发式字符密度仍是维护风险。表层压缩依然无法修复仅信封本身就超出窗口的情况,也不能拆分单个不可分割的超大消息或工具单元。 +代价是成功 step 增加一个串行检查点,并需要适配器持续维护溢出分类。提供方措辞与启发式字符密度仍是维护风险。表层压缩依然无法修复仅信封本身就超出窗口的情况,也不能拆分不可分割的非工具节点,或修复非可剪枝剩余部分仍然过大的工具单元。若可移除的文本工具结果是主要体积,可选剪枝器仍可修复原本不可分割的工具配对。 本 RFC 只取代[压缩能力接缝 RFC](../feature/2026-06-18-compaction-capability-seam.md) 中的 pre-step 自动触发部分。服务拆分、独立 token meter、平衡范围契约、日志记录锁、摘要替换与唯一 `summarize()` 子类 hook 均保持不变。 diff --git a/docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md b/docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md index 0f210cbd8e..67129e9bd7 100644 --- a/docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md +++ b/docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md @@ -18,7 +18,7 @@ Per the [capability-seams RFC](../../implemented/architecture/2026-06-13-capabil 1. **Interface** — `@deepseek-ai/dsh-compact`: an abstract `CompactService` owning the `ctx.compact` key, the `CompactionResult` vocabulary, and the `compact/*` session events. It declares `compactIfNeeded()` and `compactRegion()` as **abstract** — the contract states *what* compaction does, not *how*. 2. **Implementation** — `@deepseek-ai/dsh-compact-basic`: a concrete `BasicCompactService` that consumes `ctx.tokenMeter` and owns the tail→head retention walk, summarization via `ctx.llm.stream()`, the surface replacement, the lock, post-step pressure, and canonical context-overflow recovery. `summarize()` is its sole subclass hook; pricing and replay stay with the meter. -3. **Model-free companion** — `@deepseek-ai/dsh-tool-result-prune`: a concrete optional service that rewrites oversized current `tool/result` nodes before the backend selects a summary range. It is not a second compaction implementation and does not implement `CompactService`. +3. **Model-free companion** — `@deepseek-ai/dsh-compact-tool-result-prune`: a concrete optional service that rewrites oversized current `tool/result` nodes before the backend selects a summary range. It is not a second compaction implementation and does not implement `CompactService`. 4. **Consumer** — deferred. A `/compact` tool and slash command will `inject: ['compact']` and call the contract; they are intentionally out of scope here so the seam settles first. ### The contract depends on `dsh-session` and `dsh-llm` — a deliberate deviation @@ -37,7 +37,7 @@ An earlier draft put the full algorithm (the retention walk, token-summing, text The original pre-step placement used a provisional envelope and could not see final `agent/request` routing, tools, provider output, tool results, buffered context, or steering. The corrected lifecycle fires serial `agent/post-step(agent, turn, step, signal)` after those successful facts are durable and before `step/end`. `dsh-compact-basic` measures the canonical logged request through `ctx.tokenMeter`, so the next request sees any replacement without a speculative envelope override. Once pressure qualifies, it invokes optional `ctx.toolResultPrune`, remeasures the durable surface, and summarizes only if pruning did not restore safe pressure. -Canonical provider context overflow takes a separate path. The failed step closes, `agent/request-error` receives the original request error and consecutive retry count, and compact-basic prunes before forcing one useful balanced reduction. It returns retry only if `session.surface.replaceGeneration` increases, including pruning-only progress when no summary range exists; the loop then opens a new numbered step and reconstructs its request from the durable log. No replacement, recovery failure, cancellation, an exhausted cap, or an unrelated error preserves the original provider failure. The complete lifecycle decision is in the [after-call recovery RFC](../../implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md). +Canonical provider context overflow takes a separate path. The failed step closes, `agent/request-error` receives the original request error and consecutive retry count, and compact-basic prunes before forcing one useful balanced reduction. It returns retry only if `session.surface.replaceGeneration` increases, including pruning-only progress when no summary range exists; the loop then opens a new numbered step and reconstructs its request from the durable log. No replacement, a recovery failure before any replacement, cancellation, an exhausted cap, or an unrelated error preserves the original provider failure. If pruning already advanced the generation before later summary work fails, recovery retries from that durable pruned surface unless cancellation or disposal wins. The complete lifecycle decision is in the [after-call recovery RFC](../../implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md). ``` assistant/message → tool/result/context/steering @@ -57,7 +57,7 @@ Auto-compaction checks after **every successful** step, not once per turn. This A runaway turn thus compacts exactly like any other history: its early *closed* steps get summarized while its recent steps stay verbatim. When the only compactable content left is an un-splittable open tail step (its tool-calls have no results yet), compaction declines (`null`) and retries once that step closes. -**Single-unit overflow is out of scope, by design.** If a single retained unit — one closed step, or a large free node such as a pasted `user/message` — *alone* exceeds the budget, compaction cannot help and the next model call may go out over-budget. Bounding an individual unit's size is a separate concern (output truncation), handled elsewhere; compaction makes no promise about it, and the harness without such a mechanism can still break on a single oversized unit. This is named honestly rather than papered over. +**Some single-unit overflow remains out of scope, by design.** Summary range selection cannot split an indivisible unit. The optional pruning companion can nevertheless repair a closed tool pair when text-bearing tool-result content is the removable bulk and the pruned remainder fits. Envelope-only pressure, an oversized indivisible non-tool node such as a pasted `user/message`, and a tool unit whose non-prunable remainder is still oversized remain outside compaction. Bounding those individual units is a separate concern; the harness can still break on them. This is named honestly rather than papered over. ### Head-anchoring: one auto checkpoint, always at the head @@ -95,8 +95,8 @@ The `compact/start … compact/end` bracket is justified, in order of what now d Two failure paths, both documented: -- **Crash** (the loop dies mid-summarization): a dangling `compact/start`, no closer. Because `compact/*` are **log-only**, the orphan is **inert** — the surface replacement never landed, so the full, uncompacted history derives correctly. Generic turn-repair (`interruptedTurnClosers`) closes the turn with a synthetic `turn/end`; the orphan sits *before* that `turn/end`, so the turn-scoped in-progress check never sees it and a crash cannot wedge future compaction. -- **Recoverable** (summarization throws but the loop survives): the backend appends `compact/end` with its **`error`** field set and leaves the surface untouched. Post-step pressure warns and continues; overflow recovery delegates so the original provider error remains authoritative. +- **Crash** (the loop dies mid-summarization): a dangling `compact/start`, no closer. Because `compact/*` are **log-only**, the orphan is **inert** — no summary replacement lands. The derived surface remains the durable surface present at `compact/start`: full history when pruning made no replacement, or the already-pruned history when it did. Generic turn-repair (`interruptedTurnClosers`) closes the turn with a synthetic `turn/end`; the orphan sits *before* that `turn/end`, so the turn-scoped in-progress check never sees it and a crash cannot wedge future compaction. +- **Recoverable** (summarization throws but the loop survives): the backend appends `compact/end` with its **`error`** field set and lands no summary replacement. Post-step pressure warns and continues from the latest durable surface — full history if no replacement preceded the attempt, or the pruned surface if pruning already landed. Overflow recovery delegates only before any replacement; generation progress from earlier pruning authorizes a retry from that durable surface unless cancellation or disposal wins. `compact/end` keeps its `error?` field (mirroring `tool/result`'s self-contained error — one event tells success from failure without correlating a sibling). There is no separate `compact/error` event. @@ -111,12 +111,12 @@ Two failure paths, both documented: ## Consequences -- **Packages**: `packages/compact/compact` supplies the interface, `compact-basic` supplies the backend, and `tool-result-prune` supplies optional deterministic rewriting. `packages/llm/token-meter` owns replay-aware measurement independently. The consumer tier is deferred. +- **Packages**: `packages/compact/compact` supplies the interface, `compact-basic` supplies the backend, and `compact-tool-result-prune` supplies optional deterministic rewriting. `packages/llm/token-meter` owns replay-aware measurement independently. The consumer tier is deferred. - **Automatic seams**: `agent/post-step` (`@mode serial`) handles successful-call pressure and `agent/request-error` (`@mode waterfall`) handles final request failures after the failed step closes. Generic `agent/pre-step` remains a four-argument checkpoint with no compaction-only prompt/prefix payload. - **`SessionEventMap`** gains `compact/start` / `compact/summary` / `compact/end` by declaration merging (merge-extensible); `SurfaceEventType` is **not** touched. These are session events, not cordis `Events`, so the event-taxonomy gate needs no entry. - **`dsh-compact`** owns `toolPairingBalancedBefore(session, node)` and `toolPairingBalancedAfter(session, node)`, the cached surface-edge checks that `compactRegion` and `compactIfNeeded` use to avoid splitting a tool-call/result pair. The cache validates current membership by seq and answers both edges from one per-cut balance sequence instead of trusting a caller-retained `node.next`; stale or missing seqs and orphan results reject. `dsh-session` continues to own the surface `replace` operation, positional nodes, and rewrite generation. - **`dsh-invariants`** treats fresh appended tool results as executions that require an open step and pending call, while provenance-backed replacements are turn-enclosed surface rewrites. Positional replacement and complete-source checks validate the rewritten node. -- **Wiring**: `examples/coding-agent/cordis.yml` loads zero-config `dsh-token-meter`, `dsh-tool-result-prune`, then `dsh-compact-basic`; service-wide defaults make the composition usable without repeated numeric policy. +- **Wiring**: `examples/coding-agent/cordis.yml` loads zero-config `dsh-token-meter`, `dsh-compact-tool-result-prune`, then `dsh-compact-basic`; service-wide defaults make the composition usable without repeated numeric policy. ## Testing diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index 085b165a2f..121bfd09f7 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -5,9 +5,9 @@ Every model-facing tool a shipped plugin contributes to `ctx.tools`: the `name`, `description`, and JSON-Schema `parameters` the model receives via the system-prompt assembly. It complements the cordis [events](cordis-catalog/events.md) & [services](cordis-catalog/services.md) catalogs (the wiring a plugin listens to and calls) and [core-data-structures/](core-data-structures/core.md) (the types those signatures move) — this page is the *tools* the agent is offered. -This file is GENERATED and verified fresh by `pnpm run verify-tool-catalog` (part of `doc-sync`) — do not edit it by hand. Unlike the cordis catalog (a pure source-AST pass), this generator BOOTS each tool plugin on a real context and reads `ctx.tools.schemas()`, because a tool schema is not statically knowable (runtime-spread enums, concatenated descriptions, config-driven names, raw-JSON-Schema MCP tools). A completeness guard globs `packages/*/tool-*` and fails if any model-facing package is missing from the generator's boot manifest; service-only packages that share the prefix are explicitly excluded. See [the tool-schema-catalog RFC](rfc/implemented/process/2026-07-02-tool-schema-catalog.md). +This file is GENERATED and verified fresh by `pnpm run verify-tool-catalog` (part of `doc-sync`) — do not edit it by hand. Unlike the cordis catalog (a pure source-AST pass), this generator BOOTS each tool plugin on a real context and reads `ctx.tools.schemas()`, because a tool schema is not statically knowable (runtime-spread enums, concatenated descriptions, config-driven names, raw-JSON-Schema MCP tools). A completeness guard globs `packages/*/tool-*` and fails if any package is missing from the generator's boot manifest, so a new tool cannot be silently undocumented. See [the tool-schema-catalog RFC](rfc/implemented/process/2026-07-02-tool-schema-catalog.md). -Scope: shipped model-facing product tools under `packages/*/tool-*`, each booted with its DEFAULT config. Runtime service packages such as `tool-result-prune` do not register `ctx.tools` schemas and are explicitly excluded. The registered tool NAME can be a load-time config (e.g. `tool-subagent`'s `toolName`), so a deployment may surface a package under a different or additional name — a per-package note records those shipped aliases where they exist. The `examples/` demo tools (e.g. `echo`) are excluded, matching the cordis catalog's packages-only scope. +Scope: shipped product tools under `packages/*/tool-*`, each booted with its DEFAULT config. The registered tool NAME can be a load-time config (e.g. `tool-subagent`'s `toolName`), so a deployment may surface a package under a different or additional name — a per-package note records those shipped aliases where they exist. The `examples/` demo tools (e.g. `echo`) are excluded, matching the cordis catalog's packages-only scope. ## Tool Package Map diff --git a/examples/coding-agent/composition.md b/examples/coding-agent/composition.md index 5f35097603..c86aaa6c45 100644 --- a/examples/coding-agent/composition.md +++ b/examples/coding-agent/composition.md @@ -25,7 +25,7 @@ flowchart LR bundle_agent_core --> spine_loop["ctx.agents + ctx.agentLoop"] plugin_coding_token_meter["token-meter
@deepseek-ai/dsh-token-meter"] cfg --> plugin_coding_token_meter - plugin_coding_tool_result_prune["tool-result-prune
@deepseek-ai/dsh-tool-result-prune"] + plugin_coding_tool_result_prune["tool-result-prune
@deepseek-ai/dsh-compact-tool-result-prune"] cfg --> plugin_coding_tool_result_prune plugin_coding_compact_basic["compact-basic
@deepseek-ai/dsh-compact-basic"] cfg --> plugin_coding_compact_basic @@ -60,7 +60,7 @@ flowchart LR | `bash` | `@deepseek-ai/dsh-bash-local` | | `stdio-agent` | `@deepseek-ai/dsh-stdio-demo` | | `token-meter` | `@deepseek-ai/dsh-token-meter` | -| `tool-result-prune` | `@deepseek-ai/dsh-tool-result-prune` | +| `tool-result-prune` | `@deepseek-ai/dsh-compact-tool-result-prune` | | `compact-basic` | `@deepseek-ai/dsh-compact-basic` | | `subagent` | `@deepseek-ai/dsh-subagent` | | `subagent-spawn` | `@deepseek-ai/dsh-subagent-spawn` | diff --git a/examples/coding-agent/cordis.yml b/examples/coding-agent/cordis.yml index ba86b99e81..3b246fe825 100644 --- a/examples/coding-agent/cordis.yml +++ b/examples/coding-agent/cordis.yml @@ -52,7 +52,7 @@ # Prune oversized tool output without a model call before summary compaction. - id: tool-result-prune - name: '@deepseek-ai/dsh-tool-result-prune' + name: '@deepseek-ai/dsh-compact-tool-result-prune' # Summarize an older range after measured pressure or a canonical provider overflow. # Service-wide policy provides pressure, retention, and one overflow-retry default. diff --git a/examples/coding-agent/tests/harness.ts b/examples/coding-agent/tests/harness.ts index 6c06202a30..70ac92eafc 100644 --- a/examples/coding-agent/tests/harness.ts +++ b/examples/coding-agent/tests/harness.ts @@ -12,7 +12,7 @@ import * as ToolTodo from '@deepseek-ai/dsh-tool-todo' import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek' import TokenMeterService from '@deepseek-ai/dsh-token-meter' import type { TokenMeterConfig } from '@deepseek-ai/dsh-token-meter' -import ToolResultPruneService from '@deepseek-ai/dsh-tool-result-prune' +import ToolResultPruneService from '@deepseek-ai/dsh-compact-tool-result-prune' import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl' import { BasicCompactService } from '@deepseek-ai/dsh-compact-basic' import type { BasicCompactConfig } from '@deepseek-ai/dsh-compact-basic' diff --git a/packages/compact/README.md b/packages/compact/README.md index e251bfe442..d19343dc60 100644 --- a/packages/compact/README.md +++ b/packages/compact/README.md @@ -6,7 +6,7 @@ A compaction capability family (see [capability seams](../../docs/rfc/implemente |---|---|---| | `compact/` | Abstract compaction seam (interface + `compact/*` events + `CompactionResult`) | `ctx.compact` | | `compact-basic/` | A backend: `ctx.tokenMeter` pressure + token-budget retention + `llm.stream()` summarization | (registers `ctx.compact`) | -| `tool-result-prune/` | Optional model-free head/middle/tail rewriting before summary compaction | `ctx.toolResultPrune` | +| `compact-tool-result-prune/` | Optional model-free head/middle/tail rewriting before summary compaction | `ctx.toolResultPrune` | | `tool-compact/` (deferred) | Model-facing `/compact` tool over `ctx.compact` | (registers on `ctx.tools`) | -The interface lives at `compact/compact/`, the backend at `compact/compact-basic/`, and deterministic pruning at `compact/tool-result-prune/`. Unlike the bash seam, the interface depends on `dsh-session` and `dsh-llm` because its verbs are defined over a `Session` and its output uses `ContentBlock`. That deviation is recorded in the [compaction capability-seam RFC](../../docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md). Token measurement remains a reusable LLM-family service; a template- or model-backed compactor can replace `compact-basic` without changing the meter, pruner, or callers. +The interface lives at `compact/compact/`, the backend at `compact/compact-basic/`, and deterministic pruning at `compact/compact-tool-result-prune/`. Unlike the bash seam, the interface depends on `dsh-session` and `dsh-llm` because its verbs are defined over a `Session` and its output uses `ContentBlock`. That deviation is recorded in the [compaction capability-seam RFC](../../docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md). Token measurement remains a reusable LLM-family service; a template- or model-backed compactor can replace `compact-basic` without changing the meter, pruner, or callers. diff --git a/packages/compact/compact-basic/README.md b/packages/compact/compact-basic/README.md index 7acc1d0351..197fcd6ab6 100644 --- a/packages/compact/compact-basic/README.md +++ b/packages/compact/compact-basic/README.md @@ -9,8 +9,8 @@ This is the implementation tier of the compaction capability — see the [interf This backend owns the compaction policy: - **Measurement** — the singleton `ctx.tokenMeter` prices the latest canonical logged envelope and current surface at one consumed-log revision. Post-step pressure therefore includes the actual system prompt, tools, prefix, routing, assistant completion, tool results, buffered context, and steering. -- **Model-free pruning** — after pressure or canonical overflow qualifies, the optional [`ctx.toolResultPrune`](../tool-result-prune/README.md) service rewrites oversized tool results before range selection. Compact-basic remeasures through `ctx.tokenMeter`, skips summarization when pressure becomes safe, and otherwise summarizes the pruned surface. Below-pressure post-step checks never prune. -- **Retention** — compact the oldest whole surface units while preserving a recent tail and balanced tool-call/result cuts through the [`dsh-compact` boundary helpers](../compact/README.md#tool-pairing-boundaries). Turn boundaries do not protect old steps inside a runaway turn. An open indivisible tail declines until it closes; a single unit larger than the budget remains out of scope. +- **Model-free pruning** — after pressure or canonical overflow qualifies, the optional [`ctx.toolResultPrune`](../compact-tool-result-prune/README.md) service rewrites oversized tool results before range selection. Compact-basic remeasures through `ctx.tokenMeter`, skips summarization when pressure becomes safe, and otherwise summarizes the pruned surface. Below-pressure post-step checks never prune. +- **Retention** — compact the oldest whole surface units while preserving a recent tail and balanced tool-call/result cuts through the [`dsh-compact` boundary helpers](../compact/README.md#tool-pairing-boundaries). Turn boundaries do not protect old steps inside a runaway turn. An open indivisible tail declines until it closes. The optional pruner can repair an oversized closed tool unit when its text-bearing result is the removable bulk; indivisible non-tool units and non-prunable tool remainders remain out of scope. - **Convergence** — retry head-checkpoint compaction up to `compactionRetries`; reject a summary that does not shrink its source, and throw if retries cannot return below threshold. - **Summarization** — a direct `llm/stream` call uses the configured model and cap without running the loop-only `agent/request` seam. The input transcript preserves non-text blocks as tagged placeholders; only returned text enters the checkpoint, excluding reasoning and tool calls that would leak private reasoning or create an orphaned call. - **Framing** — the replacement user message marks established checkpoint context with `` tags. The raw summary remains on the provenance event, and later automatic cycles merge the prior checkpoint. @@ -50,7 +50,7 @@ export function apply(ctx: Context): void { } ``` -Loading the plugin registers `ctx.compact`. Add [`dsh-tool-result-prune`](../tool-result-prune/README.md) as a sibling before this plugin to enable the optional model-free pass. With `auto: true` (the default) it compacts automatically under token pressure; a consumer (a future `/compact` tool) can also call `ctx.compact.compactIfNeeded(...)` or `ctx.compact.compactRegion(...)` directly. +Loading the plugin registers `ctx.compact`. Add [`dsh-compact-tool-result-prune`](../compact-tool-result-prune/README.md) as a sibling before this plugin to enable the optional model-free pass. With `auto: true` (the default) it compacts automatically under token pressure; a consumer (a future `/compact` tool) can also call `ctx.compact.compactIfNeeded(...)` or `ctx.compact.compactRegion(...)` directly. ## Model Experience @@ -120,7 +120,7 @@ Rules: - **Meter accuracy follows the fixed heuristic** — missing reusable provider usage falls back to character count plus structural overhead rather than exact tokenization. - **Overflow classification is adapter-maintained** — provider wording can change; both DeepSeek adapters normalize currently recognized context-limit failures to `CONTEXT_WINDOW_EXCEEDED`. -- **Single-unit and envelope-only overflow remain outside surface compaction** — recovery cannot split one indivisible message/tool unit or shrink system/tools/prefix. +- **Some indivisible-unit and envelope-only overflow remains outside surface compaction** — recovery cannot shrink system/tools/prefix, split an indivisible non-tool node, or repair a tool unit whose non-prunable remainder still exceeds the window. The optional pruner can shrink text-bearing tool-result bulk inside an otherwise indivisible pair. - **`compactRegion` requires an open turn** — a manual call on a fully-closed session throws ("no open turn") rather than compacting. -- **Summarization failure fails closed with full, over-budget history** — including truncation at the summarization `maxTokens`, which hidden reasoning tokens can consume; the auto path logs a warning and proceeds. +- **Summarization failure preserves the latest durable surface** — before any replacement, the auto path logs a warning and proceeds with full over-budget history. If pruning already landed, a later summarization failure proceeds from that durable pruned surface. Summarization truncation at `maxTokens`, which hidden reasoning tokens can consume, follows the same rule. - **The summarization call has no transcript-snapshot coverage** — `dsh-llm-replay` derives calls from `assistant/chunk` events, so this chunk-less direct `ctx.llm.stream()` call cannot replay (named deferred replay infrastructure in [the seam RFC](../../../docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md)). diff --git a/packages/compact/compact-basic/package.json b/packages/compact/compact-basic/package.json index 55ca301878..d242e3bed0 100644 --- a/packages/compact/compact-basic/package.json +++ b/packages/compact/compact-basic/package.json @@ -27,11 +27,11 @@ "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-token-meter": "^0.0.1", - "@deepseek-ai/dsh-tool-result-prune": "^0.0.1", + "@deepseek-ai/dsh-compact-tool-result-prune": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "peerDependenciesMeta": { - "@deepseek-ai/dsh-tool-result-prune": { + "@deepseek-ai/dsh-compact-tool-result-prune": { "optional": true } }, @@ -49,7 +49,7 @@ "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-token-meter": "workspace:^", - "@deepseek-ai/dsh-tool-result-prune": "workspace:^", + "@deepseek-ai/dsh-compact-tool-result-prune": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", "cordis": "^4.0.0-rc.7" } diff --git a/packages/compact/compact-basic/src/index.ts b/packages/compact/compact-basic/src/index.ts index 34567f7e07..bd33e84355 100644 --- a/packages/compact/compact-basic/src/index.ts +++ b/packages/compact/compact-basic/src/index.ts @@ -13,7 +13,7 @@ import { CONTEXT_WINDOW_EXCEEDED_CODE } from '@deepseek-ai/dsh-llm' import type { ContentBlock } from '@deepseek-ai/dsh-llm' import type { Agent } from '@deepseek-ai/dsh-agent' // Type-only: makes the optional sibling service available to `ctx.get()`. -import type {} from '@deepseek-ai/dsh-tool-result-prune' +import type {} from '@deepseek-ai/dsh-compact-tool-result-prune' import { resolveConfig } from './config.ts' import { compactSurfaceRegion, selectCompactableRange } from './region.ts' import { summarizeWithLlm } from './summarizer.ts' diff --git a/packages/compact/compact-basic/tests/compact-basic.spec.ts b/packages/compact/compact-basic/tests/compact-basic.spec.ts index 094a22a11e..c2e09a5081 100644 --- a/packages/compact/compact-basic/tests/compact-basic.spec.ts +++ b/packages/compact/compact-basic/tests/compact-basic.spec.ts @@ -9,7 +9,7 @@ import LlmService, { CallId, CONTEXT_WINDOW_EXCEEDED_CODE, LlmAdapter } from '@d import type { ContentBlock, GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' import { Session, SessionId } from '@deepseek-ai/dsh-session' import TokenMeterService from '@deepseek-ai/dsh-token-meter' -import ToolResultPruneService from '@deepseek-ai/dsh-tool-result-prune' +import ToolResultPruneService from '@deepseek-ai/dsh-compact-tool-result-prune' import type { Agent } from '@deepseek-ai/dsh-agent' const SIGNAL = new AbortController().signal diff --git a/packages/compact/compact-basic/tests/loader-composition.spec.ts b/packages/compact/compact-basic/tests/loader-composition.spec.ts index 7294224452..2035627f64 100644 --- a/packages/compact/compact-basic/tests/loader-composition.spec.ts +++ b/packages/compact/compact-basic/tests/loader-composition.spec.ts @@ -9,7 +9,7 @@ import Include from '@cordisjs/plugin-include' import LlmService from '@deepseek-ai/dsh-llm' import TokenMeterService from '@deepseek-ai/dsh-token-meter' import BasicCompactService from '@deepseek-ai/dsh-compact-basic' -import ToolResultPruneService from '@deepseek-ai/dsh-tool-result-prune' +import ToolResultPruneService from '@deepseek-ai/dsh-compact-tool-result-prune' let root: string | undefined let context: Context | undefined @@ -33,7 +33,7 @@ async function loadYaml(lines: readonly string[]): Promise { const modules = new Map([ ['@deepseek-ai/dsh-llm', LlmService], ['@deepseek-ai/dsh-token-meter', TokenMeterService], - ['@deepseek-ai/dsh-tool-result-prune', ToolResultPruneService], + ['@deepseek-ai/dsh-compact-tool-result-prune', ToolResultPruneService], ['@deepseek-ai/dsh-compact-basic', BasicCompactService], ]) context.loader.internal = { @@ -58,7 +58,7 @@ describe('real Loader composition', () => { "- name: '@deepseek-ai/dsh-token-meter'", ' config:', ' contextWindow: 4096', - "- name: '@deepseek-ai/dsh-tool-result-prune'", + "- name: '@deepseek-ai/dsh-compact-tool-result-prune'", ' config:', ' thresholdChars: 100', ' headChars: 20', diff --git a/packages/compact/compact-basic/tsconfig.json b/packages/compact/compact-basic/tsconfig.json index 5dd00b83f1..47d552c3f0 100644 --- a/packages/compact/compact-basic/tsconfig.json +++ b/packages/compact/compact-basic/tsconfig.json @@ -14,6 +14,6 @@ { "path": "../../core/session" }, { "path": "../../core/agent" }, { "path": "../compact" }, - { "path": "../tool-result-prune" } + { "path": "../compact-tool-result-prune" } ] } diff --git a/packages/compact/tool-result-prune/README.md b/packages/compact/compact-tool-result-prune/README.md similarity index 96% rename from packages/compact/tool-result-prune/README.md rename to packages/compact/compact-tool-result-prune/README.md index e6d50f229c..9c405af12f 100644 --- a/packages/compact/tool-result-prune/README.md +++ b/packages/compact/compact-tool-result-prune/README.md @@ -1,4 +1,4 @@ -# @deepseek-ai/dsh-tool-result-prune +# @deepseek-ai/dsh-compact-tool-result-prune The replay-safe model-free pruning service (`ctx.toolResultPrune`). It rewrites over-budget `tool/result` surface nodes to a bounded head, a fixed omission marker, and a bounded tail while retaining the full original event in the append-only session log. @@ -28,7 +28,7 @@ All values are integers; the threshold is positive and head/tail are non-negativ ```ts import type { Context } from 'cordis' -import ToolResultPruneService from '@deepseek-ai/dsh-tool-result-prune' +import ToolResultPruneService from '@deepseek-ai/dsh-compact-tool-result-prune' export function apply(ctx: Context): void { ctx.plugin(ToolResultPruneService) diff --git a/packages/compact/tool-result-prune/package.json b/packages/compact/compact-tool-result-prune/package.json similarity index 94% rename from packages/compact/tool-result-prune/package.json rename to packages/compact/compact-tool-result-prune/package.json index 6b7ac89742..81c81eb894 100644 --- a/packages/compact/tool-result-prune/package.json +++ b/packages/compact/compact-tool-result-prune/package.json @@ -1,5 +1,5 @@ { - "name": "@deepseek-ai/dsh-tool-result-prune", + "name": "@deepseek-ai/dsh-compact-tool-result-prune", "description": "Replay-safe model-free head/middle/tail pruning for tool-result surface nodes", "version": "0.0.1", "private": true, diff --git a/packages/compact/tool-result-prune/src/config.ts b/packages/compact/compact-tool-result-prune/src/config.ts similarity index 100% rename from packages/compact/tool-result-prune/src/config.ts rename to packages/compact/compact-tool-result-prune/src/config.ts diff --git a/packages/compact/tool-result-prune/src/index.ts b/packages/compact/compact-tool-result-prune/src/index.ts similarity index 99% rename from packages/compact/tool-result-prune/src/index.ts rename to packages/compact/compact-tool-result-prune/src/index.ts index 287b0f2500..c03fa81724 100644 --- a/packages/compact/tool-result-prune/src/index.ts +++ b/packages/compact/compact-tool-result-prune/src/index.ts @@ -1,7 +1,7 @@ /** * Replay-safe, model-free tool-result pruning service. * - * @module @deepseek-ai/dsh-tool-result-prune + * @module @deepseek-ai/dsh-compact-tool-result-prune */ import { Context, Service } from 'cordis' diff --git a/packages/compact/tool-result-prune/src/types.ts b/packages/compact/compact-tool-result-prune/src/types.ts similarity index 100% rename from packages/compact/tool-result-prune/src/types.ts rename to packages/compact/compact-tool-result-prune/src/types.ts diff --git a/packages/compact/tool-result-prune/tests/loader-composition.spec.ts b/packages/compact/compact-tool-result-prune/tests/loader-composition.spec.ts similarity index 84% rename from packages/compact/tool-result-prune/tests/loader-composition.spec.ts rename to packages/compact/compact-tool-result-prune/tests/loader-composition.spec.ts index fef9326d2a..db4c29ebdb 100644 --- a/packages/compact/tool-result-prune/tests/loader-composition.spec.ts +++ b/packages/compact/compact-tool-result-prune/tests/loader-composition.spec.ts @@ -6,7 +6,7 @@ import { afterEach, describe, expect, it } from 'vitest' import { Context } from 'cordis' import Loader from '@cordisjs/plugin-loader' import Include from '@cordisjs/plugin-include' -import ToolResultPruneService from '@deepseek-ai/dsh-tool-result-prune' +import ToolResultPruneService from '@deepseek-ai/dsh-compact-tool-result-prune' let root: string | undefined let context: Context | undefined @@ -18,12 +18,12 @@ afterEach(async () => { root = undefined }) -describe('tool-result-prune real Loader composition', () => { +describe('compact-tool-result-prune real Loader composition', () => { it('loads and resolves the flat YAML plugin shape', async () => { - root = await mkdtemp(join(tmpdir(), 'dsh-tool-result-prune-loader-')) + root = await mkdtemp(join(tmpdir(), 'dsh-compact-tool-result-prune-loader-')) const configPath = join(root, 'cordis.yml') await writeFile(configPath, [ - "- name: '@deepseek-ai/dsh-tool-result-prune'", + "- name: '@deepseek-ai/dsh-compact-tool-result-prune'", ' config:', ' thresholdChars: 100', ' headChars: 20', @@ -38,7 +38,7 @@ describe('tool-result-prune real Loader composition', () => { context.loader.internal = { version: 'v2', async import(specifier: string) { - if (specifier !== '@deepseek-ai/dsh-tool-result-prune') { + if (specifier !== '@deepseek-ai/dsh-compact-tool-result-prune') { throw new Error(`unexpected Loader import: ${specifier}`) } return ToolResultPruneService diff --git a/packages/compact/tool-result-prune/tests/tool-result-prune.spec.ts b/packages/compact/compact-tool-result-prune/tests/tool-result-prune.spec.ts similarity index 98% rename from packages/compact/tool-result-prune/tests/tool-result-prune.spec.ts rename to packages/compact/compact-tool-result-prune/tests/tool-result-prune.spec.ts index 235ecdab52..b72c894891 100644 --- a/packages/compact/tool-result-prune/tests/tool-result-prune.spec.ts +++ b/packages/compact/compact-tool-result-prune/tests/tool-result-prune.spec.ts @@ -10,8 +10,8 @@ import ToolResultPruneService, { DEFAULTS, PRUNE_MARKER, resolveConfig, -} from '@deepseek-ai/dsh-tool-result-prune' -import type { ToolResultPruneConfig } from '@deepseek-ai/dsh-tool-result-prune' +} from '@deepseek-ai/dsh-compact-tool-result-prune' +import type { ToolResultPruneConfig } from '@deepseek-ai/dsh-compact-tool-result-prune' const SMALL: ToolResultPruneConfig = { thresholdChars: 50, diff --git a/packages/compact/tool-result-prune/tsconfig.json b/packages/compact/compact-tool-result-prune/tsconfig.json similarity index 100% rename from packages/compact/tool-result-prune/tsconfig.json rename to packages/compact/compact-tool-result-prune/tsconfig.json diff --git a/packages/compact/compact/README.md b/packages/compact/compact/README.md index 40bbed7eba..1ee37e5b47 100644 --- a/packages/compact/compact/README.md +++ b/packages/compact/compact/README.md @@ -72,5 +72,5 @@ Subclass `CompactService`, implement `compactIfNeeded` and `compactRegion`, and ## Known Limitations and Deferred Work - **No model-facing consumer tier yet** — `@deepseek-ai/dsh-tool-compact` (the `/compact` tool) is deferred; compaction is reachable only via direct `ctx.compact` calls or a backend's auto listener. -- **Single-unit overflow is out of contract** — one indivisible unit (a closed tool pair or a large pasted `user/message`) alone exceeding the budget cannot be compacted. +- **Some single-unit overflow is out of contract** — balanced summary compaction cannot split one indivisible unit. The optional pruning companion can still repair a closed tool pair when text-bearing tool-result bulk is removable; a large non-tool node or a tool unit whose non-prunable remainder is oversized cannot be compacted. - **An envelope that alone approaches the window is not surface-compaction work** — compaction shrinks derived history, never the system prompt, tools, or session prefix. diff --git a/packages/core/tools/tests/gen-tool-catalog.spec.ts b/packages/core/tools/tests/gen-tool-catalog.spec.ts index 8657dca3ca..1ba64f6326 100644 --- a/packages/core/tools/tests/gen-tool-catalog.spec.ts +++ b/packages/core/tools/tests/gen-tool-catalog.spec.ts @@ -69,11 +69,6 @@ describe('gen-tool-catalog assertManifestComplete', () => { // is unlisted, so the guard must fire and name them. expect(() => { assertManifestComplete([]) }).toThrow(/not in the boot manifest/) expect(() => { assertManifestComplete([]) }).toThrow(/tool-bash/) - try { - assertManifestComplete([]) - } catch (error) { - expect(String(error)).not.toContain('tool-result-prune') - } }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b0935bde01..5de2ae6d61 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -256,9 +256,9 @@ importers: '@deepseek-ai/dsh-token-meter': specifier: workspace:^ version: link:../../llm/token-meter - '@deepseek-ai/dsh-tool-result-prune': + '@deepseek-ai/dsh-compact-tool-result-prune': specifier: workspace:^ - version: link:../tool-result-prune + version: link:../compact-tool-result-prune '@deepseek-ai/dsh-tools': specifier: workspace:^ version: link:../../core/tools @@ -266,7 +266,7 @@ importers: specifier: ^4.0.0-rc.7 version: 4.0.0-rc.7(@cordisjs/plugin-include@vendor+include)(@cordisjs/plugin-loader@vendor+loader) - packages/compact/tool-result-prune: + packages/compact/compact-tool-result-prune: dependencies: schemastery: specifier: ^3.18.0 @@ -2140,9 +2140,9 @@ importers: '@deepseek-ai/dsh-tool-fs': specifier: workspace:^ version: link:../../packages/fs/tool-fs - '@deepseek-ai/dsh-tool-result-prune': + '@deepseek-ai/dsh-compact-tool-result-prune': specifier: workspace:^ - version: link:../../packages/compact/tool-result-prune + version: link:../../packages/compact/compact-tool-result-prune '@deepseek-ai/dsh-tool-skill': specifier: workspace:^ version: link:../../packages/skill/tool-skill diff --git a/python/sdk-runtime/package.json b/python/sdk-runtime/package.json index a436f859fc..7b0f6b6a56 100644 --- a/python/sdk-runtime/package.json +++ b/python/sdk-runtime/package.json @@ -31,7 +31,7 @@ "@deepseek-ai/dsh-jsonrpc-demo": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-token-meter": "workspace:^", - "@deepseek-ai/dsh-tool-result-prune": "workspace:^", + "@deepseek-ai/dsh-compact-tool-result-prune": "workspace:^", "@deepseek-ai/dsh-llm-deepseek": "workspace:^", "@deepseek-ai/dsh-llm-pi-ai": "workspace:^", "@deepseek-ai/dsh-permission": "workspace:^", diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index db6acf2c43..bfbaef1df6 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -96,7 +96,7 @@ const SERVICE_ROLES: ServiceRole[] = [ }, { key: 'toolResultPrune', - pkg: 'tool-result-prune', + pkg: 'compact-tool-result-prune', title: 'Model-free tool-result pruning', mode: 'core', consumers: ['compact-basic'], diff --git a/scripts/gen-tool-catalog.ts b/scripts/gen-tool-catalog.ts index 4a0efe66a7..3e3a0ce81b 100644 --- a/scripts/gen-tool-catalog.ts +++ b/scripts/gen-tool-catalog.ts @@ -1,9 +1,8 @@ /** * Generate `docs/tool-catalog.md` from schemas collected by booting each tool * plugin. Runtime registration is the source of truth for computed schemas; - * the manifest is checked against every on-disk model-facing `tool-*` package; - * non-model service packages with that prefix are explicitly excluded. - * `--check` verifies the committed artifact. Rationale and ownership live in + * the manifest is checked against every on-disk `tool-*` package. `--check` + * verifies the committed artifact. Rationale and ownership live in * `docs/rfc/implemented/process/2026-07-02-tool-schema-catalog.md`. */ @@ -39,9 +38,6 @@ import * as ToolWorkflow from '@deepseek-ai/dsh-tool-workflow' const root = resolve(import.meta.dirname, '..') const OUT = 'docs/tool-catalog.md' -/** `tool-*` leaves that are runtime services, not contributors to `ctx.tools`. */ -const NON_MODEL_TOOL_PACKAGES = new Set(['tool-result-prune']) - /** * Tool package plus its hand-maintained boot recipe. The caller mounts the * prompt and registry; each recipe supplies only package-specific seams and @@ -81,10 +77,9 @@ interface ToolPackage { } /** - * The boot manifest: every shipped model-facing tool package (a `tool-*` leaf - * under `packages/`, excluding {@link NON_MODEL_TOOL_PACKAGES}). Ordered by - * package name (the render order); the completeness guard proves it is - * exhaustive against the filtered on-disk glob. + * The boot manifest: every shipped tool package (a `tool-*` leaf under + * `packages/`). Ordered by package name (the render order); the completeness + * guard proves it is exhaustive against the on-disk glob. */ const TOOL_PACKAGES: ToolPackage[] = [ { @@ -261,9 +256,8 @@ interface CatalogPackage { export type ToolCatalog = CatalogPackage[] /** - * Assert the boot manifest covers every shipped model-facing tool package on - * disk (a `tool-*` leaf under `packages/`, excluding explicit service-only - * entries in {@link NON_MODEL_TOOL_PACKAGES}). + * Assert the boot manifest covers every shipped tool package on disk (a + * `tool-*` leaf under `packages/`). * Booting has no source declaration to enumerate, so this glob restores the * "a new tool cannot be silently undocumented" guarantee: an unlisted package * fails the generator (and the freshness gate) until it is added to @@ -272,10 +266,7 @@ export type ToolCatalog = CatalogPackage[] * `scanRoot` defaults to the repo root; a test may point it at a fixture tree. */ export function assertManifestComplete(packages: ToolPackage[] = TOOL_PACKAGES, scanRoot: string = root): void { - const onDisk = globSync('packages/*/tool-*', { cwd: scanRoot }) - .map(p => basename(p)) - .filter(dir => !NON_MODEL_TOOL_PACKAGES.has(dir)) - .sort() + const onDisk = globSync('packages/*/tool-*', { cwd: scanRoot }).map(p => basename(p)).sort() const listed = new Set(packages.map(p => p.dir)) const missing = onDisk.filter(dir => !listed.has(dir)) if (missing.length > 0) { @@ -348,9 +339,9 @@ export function render(catalog: ToolCatalog): string { '', 'Every model-facing tool a shipped plugin contributes to `ctx.tools`: the `name`, `description`, and JSON-Schema `parameters` the model receives via the system-prompt assembly. It complements the cordis [events](cordis-catalog/events.md) & [services](cordis-catalog/services.md) catalogs (the wiring a plugin listens to and calls) and [core-data-structures/](core-data-structures/core.md) (the types those signatures move) — this page is the *tools* the agent is offered.', '', - 'This file is GENERATED and verified fresh by `pnpm run verify-tool-catalog` (part of `doc-sync`) — do not edit it by hand. Unlike the cordis catalog (a pure source-AST pass), this generator BOOTS each tool plugin on a real context and reads `ctx.tools.schemas()`, because a tool schema is not statically knowable (runtime-spread enums, concatenated descriptions, config-driven names, raw-JSON-Schema MCP tools). A completeness guard globs `packages/*/tool-*` and fails if any model-facing package is missing from the generator\'s boot manifest; service-only packages that share the prefix are explicitly excluded. See [the tool-schema-catalog RFC](rfc/implemented/process/2026-07-02-tool-schema-catalog.md).', + 'This file is GENERATED and verified fresh by `pnpm run verify-tool-catalog` (part of `doc-sync`) — do not edit it by hand. Unlike the cordis catalog (a pure source-AST pass), this generator BOOTS each tool plugin on a real context and reads `ctx.tools.schemas()`, because a tool schema is not statically knowable (runtime-spread enums, concatenated descriptions, config-driven names, raw-JSON-Schema MCP tools). A completeness guard globs `packages/*/tool-*` and fails if any package is missing from the generator\'s boot manifest, so a new tool cannot be silently undocumented. See [the tool-schema-catalog RFC](rfc/implemented/process/2026-07-02-tool-schema-catalog.md).', '', - 'Scope: shipped model-facing product tools under `packages/*/tool-*`, each booted with its DEFAULT config. Runtime service packages such as `tool-result-prune` do not register `ctx.tools` schemas and are explicitly excluded. The registered tool NAME can be a load-time config (e.g. `tool-subagent`\'s `toolName`), so a deployment may surface a package under a different or additional name — a per-package note records those shipped aliases where they exist. The `examples/` demo tools (e.g. `echo`) are excluded, matching the cordis catalog\'s packages-only scope.', + 'Scope: shipped product tools under `packages/*/tool-*`, each booted with its DEFAULT config. The registered tool NAME can be a load-time config (e.g. `tool-subagent`\'s `toolName`), so a deployment may surface a package under a different or additional name — a per-package note records those shipped aliases where they exist. The `examples/` demo tools (e.g. `echo`) are excluded, matching the cordis catalog\'s packages-only scope.', '', '## Tool Package Map', '', diff --git a/tsconfig.build.json b/tsconfig.build.json index a099fc3a23..6cdef1f6c9 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -38,7 +38,7 @@ { "path": "./packages/code-runtime/code-runtime-worker" }, { "path": "./packages/compact/compact" }, { "path": "./packages/compact/compact-basic" }, - { "path": "./packages/compact/tool-result-prune" }, + { "path": "./packages/compact/compact-tool-result-prune" }, { "path": "./packages/llm/llm-deepseek" }, { "path": "./packages/llm/llm-pi-ai" }, { "path": "./packages/bash/bash-local" }, diff --git a/tsconfig.json b/tsconfig.json index 1decda2db0..9cc7c5ce83 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -60,7 +60,7 @@ { "path": "./packages/fs/tool-fs" }, { "path": "./packages/compact/compact" }, { "path": "./packages/compact/compact-basic" }, - { "path": "./packages/compact/tool-result-prune" }, + { "path": "./packages/compact/compact-tool-result-prune" }, { "path": "./packages/web/web" }, { "path": "./packages/web/web-search-exa" }, { "path": "./packages/web/web-search-perplexity" },