@vellumai/assistant 0.8.11 → 0.8.12-staging.2
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/ARCHITECTURE.md +15 -17
- package/README.md +0 -6
- package/bun.lock +6 -122
- package/node_modules/@vellumai/gateway-client/bun.lock +1 -0
- package/node_modules/@vellumai/gateway-client/package.json +3 -1
- package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -1
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +87 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +3 -5
- package/openapi.yaml +633 -4
- package/package.json +1 -3
- package/src/__tests__/adaptive-thinking-repair.test.ts +185 -0
- package/src/__tests__/agent-loop-compaction-events.test.ts +7 -6
- package/src/__tests__/anthropic-provider.test.ts +129 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +4 -1
- package/src/__tests__/btw-routes.test.ts +7 -34
- package/src/__tests__/checker.test.ts +6 -12
- package/src/__tests__/config-loader-backfill.test.ts +4 -2
- package/src/__tests__/config-loader-quarantine-notice.test.ts +167 -0
- package/src/__tests__/config-watcher.test.ts +2 -2
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
- package/src/__tests__/conversation-error.test.ts +2 -6
- package/src/__tests__/conversation-history-web-search.test.ts +8 -0
- package/src/__tests__/conversation-title-service.test.ts +2 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/disk-pressure-tools.test.ts +1 -1
- package/src/__tests__/exploration-drift-hook.test.ts +692 -0
- package/src/__tests__/filing-service.test.ts +8 -3
- package/src/__tests__/guardian-action-store.test.ts +0 -167
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -1
- package/src/__tests__/heartbeat-service.test.ts +5 -2
- package/src/__tests__/identity-intro-cache.test.ts +12 -5
- package/src/__tests__/identity-routes.test.ts +16 -57
- package/src/__tests__/injector-chain.test.ts +8 -3
- package/src/__tests__/injector-config-quarantine-notice.test.ts +115 -0
- package/src/__tests__/llm-catalog-parity.test.ts +16 -0
- package/src/__tests__/llm-usage-store.test.ts +11 -0
- package/src/__tests__/log-export-workspace.test.ts +468 -3
- package/src/__tests__/memory-v2-static-injector.test.ts +22 -0
- package/src/__tests__/model-intents.test.ts +1 -1
- package/src/__tests__/oauth-cli.test.ts +19 -8
- package/src/__tests__/openai-provider.test.ts +34 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +0 -1
- package/src/__tests__/recurrence-engine.test.ts +45 -0
- package/src/__tests__/schedule-routes.test.ts +34 -0
- package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
- package/src/__tests__/script-proxy-conversation-manager.test.ts +10 -5
- package/src/__tests__/secret-fixtures.ts +20 -0
- package/src/__tests__/skill-tool-factory.test.ts +49 -0
- package/src/__tests__/subagent-role-registry.test.ts +24 -1
- package/src/__tests__/subagent-tools.test.ts +1 -0
- package/src/__tests__/system-prompt.test.ts +109 -11
- package/src/__tests__/tool-approval-handler.test.ts +85 -0
- package/src/__tests__/tool-audit-listener.test.ts +86 -0
- package/src/__tests__/tool-error-hook.test.ts +1 -0
- package/src/__tests__/tool-result-spool.test.ts +337 -0
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
- package/src/__tests__/validate-input.test.ts +95 -1
- package/src/__tests__/workspace-migration-098-remove-stale-updates-bulletin-file.test.ts +65 -0
- package/src/__tests__/workspace-migration-099-disable-cache-one-shot-callsites.test.ts +139 -0
- package/src/__tests__/workspace-migration-100-upgrade-quality-profile-to-fable-5.test.ts +174 -0
- package/src/__tests__/workspace-migration-101-upgrade-balanced-economy-to-minimax-m3.test.ts +162 -0
- package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
- package/src/acp/__tests__/agent-process.test.ts +315 -2
- package/src/acp/__tests__/prepare-agent-env.test.ts +79 -5
- package/src/acp/agent-process.ts +163 -34
- package/src/acp/prepare-agent-env.ts +55 -15
- package/src/agent/loop.ts +81 -24
- package/src/api/events/usage-progress.ts +28 -0
- package/src/api/index.ts +6 -0
- package/src/background-wake/wake-intent-hooks.test.ts +2 -0
- package/src/bundler/app-bundler.ts +25 -42
- package/src/bundler/app-compiler.ts +8 -0
- package/src/calls/call-controller.ts +1 -1
- package/src/cli/commands/plugins.ts +248 -15
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +318 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +16 -9
- package/src/cli/lib/__tests__/plugin-artifact.test.ts +183 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +158 -0
- package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +245 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +307 -0
- package/src/cli/lib/inspect-plugin.ts +252 -0
- package/src/cli/lib/install-from-github.ts +214 -21
- package/src/cli/lib/list-installed-plugins.ts +17 -6
- package/src/cli/lib/plugin-artifact.ts +103 -0
- package/src/cli/lib/plugin-details.ts +18 -1
- package/src/cli/lib/plugin-fingerprint.ts +197 -0
- package/src/cli/lib/upgrade-plugin.ts +225 -0
- package/src/config/bundled-skills/subagent/SKILL.md +2 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +8 -2
- package/src/config/call-site-defaults.ts +13 -2
- package/src/config/feature-flag-registry.json +8 -16
- package/src/config/loader.ts +52 -59
- package/src/config/schema.ts +0 -2
- package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
- package/src/config/schemas/__tests__/memory-v3.test.ts +10 -0
- package/src/config/schemas/llm.ts +10 -0
- package/src/config/schemas/memory-v2.ts +13 -0
- package/src/config/schemas/memory-v3.ts +92 -0
- package/src/config/seed-inference-profiles.ts +4 -8
- package/src/context/post-turn-tool-result-truncation.ts +32 -18
- package/src/context/tool-result-spool.ts +104 -0
- package/src/credential-execution/feature-gates.ts +0 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +41 -16
- package/src/daemon/conversation-error.ts +6 -15
- package/src/daemon/conversation.ts +9 -0
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/lifecycle.ts +1 -20
- package/src/daemon/message-types/conversations.ts +2 -15
- package/src/daemon/trust-context.ts +1 -1
- package/src/events/tool-audit-listener.ts +40 -9
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
- package/src/home/__tests__/home-content-refresh.test.ts +114 -0
- package/src/home/__tests__/suggested-prompts.test.ts +86 -5
- package/src/home/home-content-refresh.ts +43 -31
- package/src/home/home-greeting-cache.ts +8 -1
- package/src/home/home-greeting.ts +13 -9
- package/src/home/suggested-prompts.ts +77 -24
- package/src/ipc/routes/trust-rules.test.ts +66 -72
- package/src/media/image-credentials.ts +2 -2
- package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
- package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts} +264 -55
- package/src/memory/conversation-attention-store.ts +1 -0
- package/src/memory/conversation-bootstrap.ts +18 -9
- package/src/memory/conversation-crud.ts +12 -2
- package/src/memory/conversation-title-service.ts +53 -9
- package/src/memory/delivery-channels.ts +0 -69
- package/src/memory/graph/extraction-job.ts +0 -15
- package/src/memory/guardian-action-store.ts +1 -376
- package/src/memory/llm-usage-store.ts +5 -1
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +2 -2
- package/src/memory/v2/__tests__/consolidation-job.test.ts +183 -2
- package/src/memory/v2/__tests__/injection.test.ts +70 -0
- package/src/memory/v2/__tests__/static-context.test.ts +12 -0
- package/src/memory/v2/consolidation-job.ts +93 -9
- package/src/memory/v2/injection.ts +53 -0
- package/src/memory/v2/prompts/consolidation.ts +1 -0
- package/src/memory/v2/static-context.ts +13 -1
- package/src/memory/v2/sweep-job.ts +1 -1
- package/src/memory/v2/types.ts +5 -0
- package/src/plugin-api/types.ts +7 -0
- package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +300 -0
- package/src/plugins/defaults/exploration-drift/package.json +15 -0
- package/src/plugins/defaults/index.ts +25 -0
- package/src/plugins/defaults/memory-retrieval/injectors.ts +132 -4
- package/src/plugins/defaults/memory-v3-shadow/__tests__/card.test.ts +92 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +2 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/fresh-set.test.ts +52 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +2 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +136 -5
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +17 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +6 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +5 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +68 -4
- package/src/plugins/defaults/memory-v3-shadow/card.ts +49 -5
- package/src/plugins/defaults/memory-v3-shadow/fresh-set.ts +59 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +4 -2
- package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +169 -0
- package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +178 -0
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +115 -26
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -9
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +144 -22
- package/src/plugins/defaults/memory-v3-shadow/types.ts +24 -6
- package/src/plugins/defaults/title-generate/hooks/stop.ts +13 -0
- package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +16 -0
- package/src/prompts/cache-boundary.ts +17 -0
- package/src/prompts/sections.ts +50 -17
- package/src/prompts/system-prompt.ts +12 -4
- package/src/prompts/templates/system-sections.ts +22 -0
- package/src/providers/__tests__/unparseable-tool-args.test.ts +53 -0
- package/src/providers/anthropic/client.ts +74 -28
- package/src/providers/gemini/client.ts +5 -1
- package/src/providers/minimax/client.ts +9 -0
- package/src/providers/model-catalog.ts +28 -0
- package/src/providers/model-intents.ts +3 -3
- package/src/providers/openai/chat-completions-provider.ts +4 -2
- package/src/providers/openai/responses-provider.ts +7 -2
- package/src/providers/retry.ts +8 -0
- package/src/providers/types.ts +11 -0
- package/src/providers/unparseable-tool-args.ts +56 -0
- package/src/runtime/AGENTS.md +6 -0
- package/src/runtime/__tests__/agent-wake.test.ts +2 -2
- package/src/runtime/agent-wake.ts +5 -5
- package/src/runtime/background-job-runner.ts +2 -2
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +150 -3
- package/src/runtime/migrations/vbundle-import-analyzer.ts +29 -6
- package/src/runtime/migrations/vbundle-import-policy.ts +23 -0
- package/src/runtime/migrations/vbundle-importer.ts +9 -4
- package/src/runtime/migrations/vbundle-streaming-importer.ts +8 -3
- package/src/runtime/pre-first-message-gate.ts +1 -1
- package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +241 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +132 -0
- package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +97 -185
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +17 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +348 -0
- package/src/runtime/routes/__tests__/task-routes.test.ts +3 -3
- package/src/runtime/routes/btw-routes.ts +0 -14
- package/src/runtime/routes/conversation-compaction-routes.ts +86 -19
- package/src/runtime/routes/conversation-list-routes.ts +77 -5
- package/src/runtime/routes/conversation-management-routes.ts +54 -0
- package/src/runtime/routes/conversation-query-routes.ts +79 -4
- package/src/runtime/routes/gateway-log-routes.ts +14 -64
- package/src/runtime/routes/home-feed-routes.ts +10 -0
- package/src/runtime/routes/identity-intro-cache.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +76 -20
- package/src/runtime/routes/inbound-message-handler.ts +0 -36
- package/src/runtime/routes/log-export-routes.ts +143 -96
- package/src/runtime/routes/plugins-routes.ts +380 -0
- package/src/runtime/routes/redact-staged-export.ts +259 -0
- package/src/runtime/routes/schedule-routes.ts +19 -2
- package/src/runtime/routes/trust-rules-routes.ts +14 -67
- package/src/schedule/recurrence-engine.ts +34 -0
- package/src/schedule/scheduler.ts +1 -0
- package/src/security/redact-json.ts +61 -0
- package/src/skills/validate-input.ts +41 -1
- package/src/subagent/types.ts +26 -1
- package/src/telemetry/types.ts +15 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
- package/src/telemetry/usage-telemetry-reporter.ts +1 -0
- package/src/tools/apps/executors.ts +1 -1
- package/src/tools/skills/skill-tool-factory.ts +19 -8
- package/src/tools/tool-approval-handler.ts +31 -0
- package/src/usage/types.ts +8 -1
- package/src/util/platform.ts +16 -0
- package/src/watcher/engine.ts +1 -0
- package/src/workspace/adaptive-thinking-repair.ts +113 -0
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +70 -67
- package/src/workspace/migrations/098-remove-stale-updates-bulletin-file.ts +31 -0
- package/src/workspace/migrations/099-disable-cache-one-shot-callsites.ts +81 -0
- package/src/workspace/migrations/100-upgrade-quality-profile-to-fable-5.ts +86 -0
- package/src/workspace/migrations/101-upgrade-balanced-economy-to-minimax-m3.ts +70 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/__tests__/config-loader-quarantine-bulletin.test.ts +0 -202
- package/src/__tests__/conversation-starters-cadence.test.ts +0 -161
- package/src/__tests__/guardian-action-followup-executor.test.ts +0 -322
- package/src/__tests__/guardian-action-followup-store.test.ts +0 -373
- package/src/__tests__/guardian-action-late-reply.test.ts +0 -1083
- package/src/__tests__/update-bulletin-job.test.ts +0 -292
- package/src/config/schemas/updates.ts +0 -14
- package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +0 -227
- package/src/memory/conversation-starters-cadence.ts +0 -78
- package/src/prompts/update-bulletin-job.ts +0 -180
- package/src/runtime/guardian-action-followup-executor.ts +0 -306
package/src/config/loader.ts
CHANGED
|
@@ -5,14 +5,14 @@ import {
|
|
|
5
5
|
renameSync,
|
|
6
6
|
writeFileSync,
|
|
7
7
|
} from "node:fs";
|
|
8
|
-
import {
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
9
|
|
|
10
10
|
import { safeStatSync } from "../util/fs.js";
|
|
11
11
|
import { getLogger } from "../util/logger.js";
|
|
12
12
|
import {
|
|
13
13
|
ensureDataDir,
|
|
14
|
+
getConfigQuarantineNoticePath,
|
|
14
15
|
getWorkspaceConfigPath,
|
|
15
|
-
getWorkspaceDir,
|
|
16
16
|
} from "../util/platform.js";
|
|
17
17
|
import { AssistantConfigSchema } from "./schema.js";
|
|
18
18
|
import type { AssistantConfig } from "./types.js";
|
|
@@ -201,10 +201,9 @@ function filesystemSafeTimestamp(date: Date = new Date()): string {
|
|
|
201
201
|
* in `.json` so editors syntax-highlight the preserved content:
|
|
202
202
|
* `<path>.corrupt-<ISO-timestamp>.json`
|
|
203
203
|
*
|
|
204
|
-
* On a successful rename, also
|
|
205
|
-
* so the
|
|
206
|
-
*
|
|
207
|
-
* to users).
|
|
204
|
+
* On a successful rename, also writes a small JSON sentinel recording the
|
|
205
|
+
* event so the per-turn `config-quarantine-notice` injector can surface it to
|
|
206
|
+
* the agent (log-level errors alone are invisible to users).
|
|
208
207
|
*/
|
|
209
208
|
function quarantineCorruptConfig(configPath: string, err: unknown): string {
|
|
210
209
|
const quarantinePath = `${configPath}.corrupt-${filesystemSafeTimestamp()}.json`;
|
|
@@ -215,7 +214,7 @@ function quarantineCorruptConfig(configPath: string, err: unknown): string {
|
|
|
215
214
|
`quarantined to ${quarantinePath} and loaded defaults. ` +
|
|
216
215
|
`Inspect the quarantined file to recover any hand-edited settings.`,
|
|
217
216
|
);
|
|
218
|
-
|
|
217
|
+
writeQuarantineNotice(configPath, quarantinePath);
|
|
219
218
|
} catch (renameErr) {
|
|
220
219
|
log.error(
|
|
221
220
|
{ renameErr },
|
|
@@ -227,66 +226,55 @@ function quarantineCorruptConfig(configPath: string, err: unknown): string {
|
|
|
227
226
|
}
|
|
228
227
|
|
|
229
228
|
/**
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
* cases are the user asking why their settings changed or noticing missing
|
|
235
|
-
* API keys. The bulletin is agent-visible context, not a push notification.
|
|
229
|
+
* Write a small JSON sentinel recording that the config file was quarantined.
|
|
230
|
+
* The per-turn `config-quarantine-notice` injector reads this sentinel and, if
|
|
231
|
+
* it is recent, injects a system block so the agent can explain the reset when
|
|
232
|
+
* the user asks why their settings changed or notices missing API keys.
|
|
236
233
|
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
234
|
+
* Writes with pure `node:fs` and a workspace-derived path
|
|
235
|
+
* ({@link getConfigQuarantineNoticePath}) deliberately: config load happens
|
|
236
|
+
* extremely early at daemon startup — before the SQLite DB is initialized and
|
|
237
|
+
* before `getConfig().dataDir` is available — so neither a DB checkpoint nor a
|
|
238
|
+
* config-dependent path can be used here without risking import-time DB init.
|
|
239
|
+
*
|
|
240
|
+
* Idempotent per quarantine event: each call overwrites the sentinel with the
|
|
241
|
+
* latest event, so a crash-then-retry re-records the same (or newer) event
|
|
242
|
+
* rather than accumulating duplicates.
|
|
243
243
|
*
|
|
244
244
|
* Best-effort: any write failure is logged at `warn` and swallowed. The
|
|
245
245
|
* quarantine path must never block startup, and the error log from
|
|
246
246
|
* `quarantineCorruptConfig` remains the authoritative record.
|
|
247
247
|
*
|
|
248
|
-
* Exported with an underscore-prefixed alias (`
|
|
249
|
-
* tests can exercise the
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
* point.
|
|
248
|
+
* Exported with an underscore-prefixed alias (`_writeQuarantineNotice`) so
|
|
249
|
+
* tests can exercise the write directly. Non-test callers should never import
|
|
250
|
+
* the underscore alias — the wiring into `quarantineCorruptConfig` is the
|
|
251
|
+
* production entry point.
|
|
253
252
|
*/
|
|
254
|
-
function
|
|
253
|
+
function writeQuarantineNotice(
|
|
255
254
|
originalPath: string,
|
|
256
255
|
quarantinePath: string,
|
|
257
256
|
): void {
|
|
258
257
|
try {
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
:
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
`## Config was reset to defaults\n\n` +
|
|
271
|
-
`Your \`config.json\` was unreadable at ${timestamp} and couldn't be parsed ` +
|
|
272
|
-
`as JSON. The assistant preserved the original file at \`${quarantinePath}\` ` +
|
|
273
|
-
`and loaded defaults so the app stays working.\n\n` +
|
|
274
|
-
`If you had custom settings (API keys, model choices, voice preferences), ` +
|
|
275
|
-
`they are still in the quarantined file — \`cat ${quarantinePath}\` to ` +
|
|
276
|
-
`recover them, then re-enter through Settings or the CLI.\n\n` +
|
|
277
|
-
`${marker}\n`;
|
|
278
|
-
|
|
279
|
-
const toWrite = existing.length === 0 ? block : `${existing}\n${block}`;
|
|
280
|
-
writeFileSync(updatesPath, toWrite, "utf-8");
|
|
258
|
+
const noticePath = getConfigQuarantineNoticePath();
|
|
259
|
+
const dir = dirname(noticePath);
|
|
260
|
+
if (!existsSync(dir)) {
|
|
261
|
+
mkdirSync(dir, { recursive: true });
|
|
262
|
+
}
|
|
263
|
+
const notice = {
|
|
264
|
+
quarantinedAt: new Date().toISOString(),
|
|
265
|
+
quarantinePath,
|
|
266
|
+
originalPath,
|
|
267
|
+
};
|
|
268
|
+
writeFileSync(noticePath, JSON.stringify(notice, null, 2) + "\n", "utf-8");
|
|
281
269
|
log.info(
|
|
282
|
-
`
|
|
283
|
-
`(quarantined as ${
|
|
270
|
+
`Wrote config-quarantine notice to ${noticePath} for ${originalPath} ` +
|
|
271
|
+
`(quarantined as ${quarantinePath}).`,
|
|
284
272
|
);
|
|
285
|
-
} catch (
|
|
273
|
+
} catch (noticeErr) {
|
|
286
274
|
log.warn(
|
|
287
|
-
{
|
|
288
|
-
`Failed to
|
|
289
|
-
`
|
|
275
|
+
{ noticeErr },
|
|
276
|
+
`Failed to write config-quarantine notice; the quarantine event is ` +
|
|
277
|
+
`still recorded in the assistant logs.`,
|
|
290
278
|
);
|
|
291
279
|
}
|
|
292
280
|
}
|
|
@@ -660,7 +648,9 @@ export function loadConfig(): AssistantConfig {
|
|
|
660
648
|
quarantineCorruptConfig(
|
|
661
649
|
configPath,
|
|
662
650
|
new Error(
|
|
663
|
-
`config.json must contain a JSON object at the top level; got ${describeJsonShape(
|
|
651
|
+
`config.json must contain a JSON object at the top level; got ${describeJsonShape(
|
|
652
|
+
parsed,
|
|
653
|
+
)}`,
|
|
664
654
|
),
|
|
665
655
|
);
|
|
666
656
|
fileConfig = {};
|
|
@@ -889,7 +879,9 @@ export function loadRawConfig(): Record<string, unknown> {
|
|
|
889
879
|
quarantineCorruptConfig(
|
|
890
880
|
configPath,
|
|
891
881
|
new Error(
|
|
892
|
-
`config.json must contain a JSON object at the top level; got ${describeJsonShape(
|
|
882
|
+
`config.json must contain a JSON object at the top level; got ${describeJsonShape(
|
|
883
|
+
parsed,
|
|
884
|
+
)}`,
|
|
893
885
|
),
|
|
894
886
|
);
|
|
895
887
|
return {};
|
|
@@ -963,8 +955,9 @@ export function setNestedValue(
|
|
|
963
955
|
}
|
|
964
956
|
|
|
965
957
|
/**
|
|
966
|
-
* Test-only alias for `
|
|
967
|
-
*
|
|
968
|
-
*
|
|
958
|
+
* Test-only alias for `writeQuarantineNotice`. Exists so the sentinel write
|
|
959
|
+
* (and its overwrite/idempotency semantics) can be exercised directly with a
|
|
960
|
+
* deterministic quarantine path without widening the runtime surface. Not for
|
|
961
|
+
* production use.
|
|
969
962
|
*/
|
|
970
|
-
export const
|
|
963
|
+
export const _writeQuarantineNotice = writeQuarantineNotice;
|
package/src/config/schema.ts
CHANGED
|
@@ -56,7 +56,6 @@ import {
|
|
|
56
56
|
TimeoutConfigSchema,
|
|
57
57
|
} from "./schemas/timeouts.js";
|
|
58
58
|
import { ToolsConfigSchema } from "./schemas/tools.js";
|
|
59
|
-
import { UpdatesConfigSchema } from "./schemas/updates.js";
|
|
60
59
|
import { WorkspaceGitConfigSchema } from "./schemas/workspace-git.js";
|
|
61
60
|
|
|
62
61
|
export const AssistantConfigSchema = z
|
|
@@ -89,7 +88,6 @@ export const AssistantConfigSchema = z
|
|
|
89
88
|
llmRequestLogs: LlmRequestLogsConfigSchema,
|
|
90
89
|
filing: FilingConfigSchema.default(FilingConfigSchema.parse({})),
|
|
91
90
|
heartbeat: HeartbeatConfigSchema.default(HeartbeatConfigSchema.parse({})),
|
|
92
|
-
updates: UpdatesConfigSchema.default(UpdatesConfigSchema.parse({})),
|
|
93
91
|
hostBrowser: HostBrowserConfigSchema.default(
|
|
94
92
|
HostBrowserConfigSchema.parse({}),
|
|
95
93
|
),
|
|
@@ -8,9 +8,19 @@ describe("MemoryV3ConfigSchema", () => {
|
|
|
8
8
|
expect(parsed).toEqual({
|
|
9
9
|
prune: { maxResidentBytes: 393216, targetResidentBytes: 262144 },
|
|
10
10
|
hotSet: { k: 40, halfLifeDays: 14 },
|
|
11
|
+
freshSet: { k: 100 },
|
|
12
|
+
learnedEdges: {
|
|
13
|
+
halfLifeDays: 30,
|
|
14
|
+
minCount: 3,
|
|
15
|
+
npmiFloor: 0.2,
|
|
16
|
+
maxPerPage: 6,
|
|
17
|
+
perSeed: 3,
|
|
18
|
+
cap: 20,
|
|
19
|
+
},
|
|
11
20
|
spotlight: { n: 6, windowTurns: 2 },
|
|
12
21
|
needleK: 100,
|
|
13
22
|
denseK: 100,
|
|
23
|
+
replyQueryK: 12,
|
|
14
24
|
edge: { hubDegree: 30, seedCount: 18, perSeed: 6, cap: 45 },
|
|
15
25
|
});
|
|
16
26
|
});
|
|
@@ -331,6 +331,15 @@ export const LLMConfigBase = z.object({
|
|
|
331
331
|
// `profileConfigFragment`, which strips it from the merge), so a preset
|
|
332
332
|
// can't bleed from a lower-precedence profile into one that didn't opt in.
|
|
333
333
|
logitBias: LogitBiasPresetSchema.optional(),
|
|
334
|
+
/**
|
|
335
|
+
* Opt this config out of prompt caching. Providers send no cache
|
|
336
|
+
* breakpoints and strip caller-stamped `cache_control` markers. Intended
|
|
337
|
+
* for one-shot call sites whose prompts never repeat (or repeat slower
|
|
338
|
+
* than the cache TTL), where every breakpoint is a paid cache write with
|
|
339
|
+
* no future read. Optional (no schema default) so it only appears in
|
|
340
|
+
* resolved configs when a layer sets it.
|
|
341
|
+
*/
|
|
342
|
+
disableCache: z.boolean().optional(),
|
|
334
343
|
});
|
|
335
344
|
export type LLMConfigBase = z.infer<typeof LLMConfigBase>;
|
|
336
345
|
|
|
@@ -352,6 +361,7 @@ const LLMConfigFragment = z.object({
|
|
|
352
361
|
contextWindow: ContextWindowDeepPartialSchema.optional(),
|
|
353
362
|
openrouter: OpenRouterDeepPartialSchema.optional(),
|
|
354
363
|
logitBias: LogitBiasPresetSchema.optional(),
|
|
364
|
+
disableCache: z.boolean().optional(),
|
|
355
365
|
});
|
|
356
366
|
type LLMConfigFragment = z.infer<typeof LLMConfigFragment>;
|
|
357
367
|
|
|
@@ -207,6 +207,19 @@ export const MemoryV2ConfigSchema = z
|
|
|
207
207
|
.describe(
|
|
208
208
|
"Size-based trigger for consolidation. When `memory/buffer.md` reaches this many non-empty lines, consolidation runs even if the time-based interval hasn't elapsed. Defaults to 100. Set to `null` to disable the size trigger and rely solely on `consolidation_interval_hours`.",
|
|
209
209
|
),
|
|
210
|
+
consolidation_max_entries_per_run: z
|
|
211
|
+
.number({
|
|
212
|
+
error: "memory.v2.consolidation_max_entries_per_run must be a number",
|
|
213
|
+
})
|
|
214
|
+
.int("memory.v2.consolidation_max_entries_per_run must be an integer")
|
|
215
|
+
.positive(
|
|
216
|
+
"memory.v2.consolidation_max_entries_per_run must be a positive integer",
|
|
217
|
+
)
|
|
218
|
+
.nullable()
|
|
219
|
+
.default(150)
|
|
220
|
+
.describe(
|
|
221
|
+
"Upper bound on buffer entries one consolidation run may process. When the buffer holds more, the run's cutoff is moved back to the first over-cap entry's timestamp so the overflow is deferred to a follow-up pass (the `consolidation_max_buffer_lines` size trigger re-fires while the remainder stays over threshold). Bounds the context a single agentic run must read after a backlog. Set to `null` to always process the full buffer.",
|
|
222
|
+
),
|
|
210
223
|
max_page_chars: z
|
|
211
224
|
.number({ error: "memory.v2.max_page_chars must be a number" })
|
|
212
225
|
.int("memory.v2.max_page_chars must be an integer")
|
|
@@ -63,6 +63,86 @@ export const MemoryV3HotSetSchema = z
|
|
|
63
63
|
})
|
|
64
64
|
.describe("Memory v3 hot-set lane (decayed selection frequency) tuning.");
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Fresh-set lane tuning: the top-K pages by most-recent on-disk modification
|
|
68
|
+
* folded into the candidate pool as a stable-prefix lane. Recency covers the
|
|
69
|
+
* window before the other lanes can reach a just-written page (no selection
|
|
70
|
+
* history for the hot set; nothing lexical for the finders on summary-shaped
|
|
71
|
+
* messages).
|
|
72
|
+
*/
|
|
73
|
+
export const MemoryV3FreshSetSchema = z
|
|
74
|
+
.object({
|
|
75
|
+
k: z
|
|
76
|
+
.number({ error: "memory.v3.freshSet.k must be a number" })
|
|
77
|
+
.int("memory.v3.freshSet.k must be an integer")
|
|
78
|
+
.nonnegative("memory.v3.freshSet.k must be a non-negative integer")
|
|
79
|
+
.default(100)
|
|
80
|
+
.describe(
|
|
81
|
+
"Number of most-recently-modified pages included in the fresh-set lane (0 disables the lane). Sized to cover roughly the last day or two of page writes — the recency window conversations reference most.",
|
|
82
|
+
),
|
|
83
|
+
})
|
|
84
|
+
.describe("Memory v3 fresh-set lane (page-modification recency) tuning.");
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Learned-edge lane tuning: a co-selection NPMI association graph over the
|
|
88
|
+
* selection log, expanded alongside the static link graph. Behavioral edges
|
|
89
|
+
* reach association-relevant pages no lexical, semantic, or authored-link
|
|
90
|
+
* lane can surface.
|
|
91
|
+
*/
|
|
92
|
+
export const MemoryV3LearnedEdgesSchema = z
|
|
93
|
+
.object({
|
|
94
|
+
halfLifeDays: z
|
|
95
|
+
.number({ error: "memory.v3.learnedEdges.halfLifeDays must be a number" })
|
|
96
|
+
.positive("memory.v3.learnedEdges.halfLifeDays must be a positive number")
|
|
97
|
+
.default(30)
|
|
98
|
+
.describe(
|
|
99
|
+
"Co-selection decay half-life in days: a selector call this old contributes half the weight of one made now.",
|
|
100
|
+
),
|
|
101
|
+
minCount: z
|
|
102
|
+
.number({ error: "memory.v3.learnedEdges.minCount must be a number" })
|
|
103
|
+
.positive("memory.v3.learnedEdges.minCount must be a positive number")
|
|
104
|
+
.default(3)
|
|
105
|
+
.describe(
|
|
106
|
+
"Minimum decayed co-occurrence mass for a pair to form an edge (the rare-pair noise floor).",
|
|
107
|
+
),
|
|
108
|
+
npmiFloor: z
|
|
109
|
+
.number({ error: "memory.v3.learnedEdges.npmiFloor must be a number" })
|
|
110
|
+
.nonnegative(
|
|
111
|
+
"memory.v3.learnedEdges.npmiFloor must be a non-negative number",
|
|
112
|
+
)
|
|
113
|
+
.default(0.2)
|
|
114
|
+
.describe("Minimum NPMI for a pair to form an edge."),
|
|
115
|
+
maxPerPage: z
|
|
116
|
+
.number({ error: "memory.v3.learnedEdges.maxPerPage must be a number" })
|
|
117
|
+
.int("memory.v3.learnedEdges.maxPerPage must be an integer")
|
|
118
|
+
.nonnegative(
|
|
119
|
+
"memory.v3.learnedEdges.maxPerPage must be a non-negative integer",
|
|
120
|
+
)
|
|
121
|
+
.default(6)
|
|
122
|
+
.describe(
|
|
123
|
+
"Maximum learned out-edges kept per page, strongest NPMI first (0 disables the lane).",
|
|
124
|
+
),
|
|
125
|
+
perSeed: z
|
|
126
|
+
.number({ error: "memory.v3.learnedEdges.perSeed must be a number" })
|
|
127
|
+
.int("memory.v3.learnedEdges.perSeed must be an integer")
|
|
128
|
+
.positive("memory.v3.learnedEdges.perSeed must be a positive integer")
|
|
129
|
+
.default(3)
|
|
130
|
+
.describe(
|
|
131
|
+
"Maximum learned neighbours surfaced per expanded seed each turn.",
|
|
132
|
+
),
|
|
133
|
+
cap: z
|
|
134
|
+
.number({ error: "memory.v3.learnedEdges.cap must be a number" })
|
|
135
|
+
.int("memory.v3.learnedEdges.cap must be an integer")
|
|
136
|
+
.nonnegative("memory.v3.learnedEdges.cap must be a non-negative integer")
|
|
137
|
+
.default(20)
|
|
138
|
+
.describe(
|
|
139
|
+
"Hard cap on total learned-lane surfaced articles per turn (0 disables the pass).",
|
|
140
|
+
),
|
|
141
|
+
})
|
|
142
|
+
.describe(
|
|
143
|
+
"Memory v3 learned-edge lane (co-selection NPMI association graph) tuning.",
|
|
144
|
+
);
|
|
145
|
+
|
|
66
146
|
/**
|
|
67
147
|
* Ephemeral section-spotlight tuning: how many of the current turn's selected
|
|
68
148
|
* finder hits render their matched section into the `<memory_spotlight>`
|
|
@@ -142,6 +222,10 @@ export const MemoryV3ConfigSchema = z
|
|
|
142
222
|
.object({
|
|
143
223
|
prune: MemoryV3PruneSchema.default(MemoryV3PruneSchema.parse({})),
|
|
144
224
|
hotSet: MemoryV3HotSetSchema.default(MemoryV3HotSetSchema.parse({})),
|
|
225
|
+
freshSet: MemoryV3FreshSetSchema.default(MemoryV3FreshSetSchema.parse({})),
|
|
226
|
+
learnedEdges: MemoryV3LearnedEdgesSchema.default(
|
|
227
|
+
MemoryV3LearnedEdgesSchema.parse({}),
|
|
228
|
+
),
|
|
145
229
|
spotlight: MemoryV3SpotlightSchema.default(
|
|
146
230
|
MemoryV3SpotlightSchema.parse({}),
|
|
147
231
|
),
|
|
@@ -161,6 +245,14 @@ export const MemoryV3ConfigSchema = z
|
|
|
161
245
|
.describe(
|
|
162
246
|
"Number of dense-lane articles folded into the candidate pool each turn.",
|
|
163
247
|
),
|
|
248
|
+
replyQueryK: z
|
|
249
|
+
.number({ error: "memory.v3.replyQueryK must be a number" })
|
|
250
|
+
.int("memory.v3.replyQueryK must be an integer")
|
|
251
|
+
.nonnegative("memory.v3.replyQueryK must be a non-negative integer")
|
|
252
|
+
.default(12)
|
|
253
|
+
.describe(
|
|
254
|
+
"Per-lane article budget for the reply-query finder pass: needle and dense each re-run over the assistant's previous message as separate queries (never concatenated with the user's message). 0 disables the pass. Deliberately small next to needleK/denseK — the pass adds the assistant-side retrieval signal, not a second full sweep.",
|
|
255
|
+
),
|
|
164
256
|
edge: MemoryV3EdgeSchema.default(MemoryV3EdgeSchema.parse({})),
|
|
165
257
|
})
|
|
166
258
|
.describe("Memory v3 — section-grain lane retrieval");
|
|
@@ -74,23 +74,19 @@ const MANAGED_PROFILE_TEMPLATES: Record<string, ManagedProfileTemplate> = {
|
|
|
74
74
|
thinking: { enabled: false, streamThinking: false },
|
|
75
75
|
contextWindow: { maxInputTokens: DEFAULT_CONTEXT_WINDOW_MAX_INPUT_TOKENS },
|
|
76
76
|
},
|
|
77
|
-
// Open-weight economy option:
|
|
78
|
-
// platform inference.
|
|
79
|
-
// discourage the model from spontaneously emitting Chinese in English
|
|
80
|
-
// output; the preset is profile-scoped and only forwarded on the Fireworks
|
|
81
|
-
// path (see `providers/inference/logit-bias.ts`).
|
|
77
|
+
// Open-weight economy option: MiniMax M3 served by Fireworks via managed
|
|
78
|
+
// platform inference.
|
|
82
79
|
"balanced-economy": {
|
|
83
80
|
intent: "balanced",
|
|
84
81
|
provider: "fireworks",
|
|
85
82
|
connectionName: "fireworks-managed",
|
|
86
83
|
source: "managed",
|
|
87
84
|
label: "Balanced Economy",
|
|
88
|
-
description: "Strong open model (
|
|
89
|
-
maxTokens:
|
|
85
|
+
description: "Strong open model (MiniMax M3) at a lower price point",
|
|
86
|
+
maxTokens: 32000,
|
|
90
87
|
effort: "high",
|
|
91
88
|
thinking: { enabled: true, streamThinking: true },
|
|
92
89
|
contextWindow: { maxInputTokens: DEFAULT_CONTEXT_WINDOW_MAX_INPUT_TOKENS },
|
|
93
|
-
logitBias: "suppress-cjk",
|
|
94
90
|
},
|
|
95
91
|
};
|
|
96
92
|
|
|
@@ -50,6 +50,27 @@ function buildToolNameById(messages: Message[]): Map<string, string> {
|
|
|
50
50
|
return byId;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Shared eligibility rules for replacing an oversized tool result with the
|
|
55
|
+
* on-disk stub: string content over the size threshold, not an error result,
|
|
56
|
+
* not from an exempt tool (durable instructions like skill bodies), and not
|
|
57
|
+
* already truncated (idempotency marker). Used by both the post-turn pass and
|
|
58
|
+
* the result-time spool pass so the two converge on the same results.
|
|
59
|
+
*/
|
|
60
|
+
export function isTruncationEligible(
|
|
61
|
+
tr: ToolResultContent,
|
|
62
|
+
toolName: string | undefined,
|
|
63
|
+
): boolean {
|
|
64
|
+
if (typeof tr.content !== "string") return false;
|
|
65
|
+
if (tr.content.length <= THRESHOLD_CHARS) return false;
|
|
66
|
+
if (tr.is_error) return false;
|
|
67
|
+
if (toolName !== undefined && TRUNCATION_EXEMPT_TOOLS.has(toolName)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
if (tr.content.includes(TRUNCATION_MARKER)) return false;
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
53
74
|
/**
|
|
54
75
|
* Deterministic file path for a tool result's full content on disk.
|
|
55
76
|
* Uses the first 12 hex chars of the SHA-256 of the tool_use_id.
|
|
@@ -58,7 +79,10 @@ export function getToolResultFilePath(
|
|
|
58
79
|
conversationDir: string,
|
|
59
80
|
toolUseId: string,
|
|
60
81
|
): string {
|
|
61
|
-
const hash = createHash("sha256")
|
|
82
|
+
const hash = createHash("sha256")
|
|
83
|
+
.update(toolUseId)
|
|
84
|
+
.digest("hex")
|
|
85
|
+
.slice(0, 12);
|
|
62
86
|
return join(conversationDir, TOOL_RESULT_DIR, `${hash}.txt`);
|
|
63
87
|
}
|
|
64
88
|
|
|
@@ -73,7 +97,11 @@ export function buildTruncatedContent(
|
|
|
73
97
|
): string {
|
|
74
98
|
const half = Math.floor(TARGET_CHARS / 2);
|
|
75
99
|
const prefix = safeStringSlice(original, 0, half);
|
|
76
|
-
const suffix = safeStringSlice(
|
|
100
|
+
const suffix = safeStringSlice(
|
|
101
|
+
original,
|
|
102
|
+
original.length - half,
|
|
103
|
+
original.length,
|
|
104
|
+
);
|
|
77
105
|
const omittedChars = original.length - TARGET_CHARS;
|
|
78
106
|
const estimatedTokens = Math.round(omittedChars / 4);
|
|
79
107
|
return `${prefix}\n\n...(${estimatedTokens} tokens omitted ${TRUNCATION_MARKER} ${filePath})\n\n${suffix}`;
|
|
@@ -86,9 +114,7 @@ export function buildTruncatedContent(
|
|
|
86
114
|
* - The full content is persisted to a deterministic file path on disk.
|
|
87
115
|
* - The in-context content is replaced with a prefix/suffix stub.
|
|
88
116
|
*
|
|
89
|
-
* Results are skipped
|
|
90
|
-
* have already been truncated (contain `TRUNCATION_MARKER`), or were produced
|
|
91
|
-
* by a tool in `TRUNCATION_EXEMPT_TOOLS` (durable instructions like skill bodies).
|
|
117
|
+
* Results are skipped unless {@link isTruncationEligible} allows them.
|
|
92
118
|
*
|
|
93
119
|
* Returns a shallow-copied messages array (only modified messages are cloned)
|
|
94
120
|
* and the count of results that were truncated.
|
|
@@ -107,22 +133,10 @@ export function postTurnTruncateToolResults(
|
|
|
107
133
|
if (block.type !== "tool_result") return block;
|
|
108
134
|
const tr = block as ToolResultContent;
|
|
109
135
|
|
|
110
|
-
|
|
111
|
-
if (tr.content.length <= THRESHOLD_CHARS) return block;
|
|
112
|
-
|
|
113
|
-
// Skip error results.
|
|
114
|
-
if (tr.is_error) return block;
|
|
115
|
-
|
|
116
|
-
// Skip results from tools whose output is durable operating instructions
|
|
117
|
-
// (e.g. skill_load); middle-truncating them strips the workflow.
|
|
118
|
-
const toolName = toolNameById.get(tr.tool_use_id);
|
|
119
|
-
if (toolName !== undefined && TRUNCATION_EXEMPT_TOOLS.has(toolName)) {
|
|
136
|
+
if (!isTruncationEligible(tr, toolNameById.get(tr.tool_use_id))) {
|
|
120
137
|
return block;
|
|
121
138
|
}
|
|
122
139
|
|
|
123
|
-
// Skip already-truncated results (idempotency).
|
|
124
|
-
if (tr.content.includes(TRUNCATION_MARKER)) return block;
|
|
125
|
-
|
|
126
140
|
const filePath = getToolResultFilePath(
|
|
127
141
|
options.conversationDir,
|
|
128
142
|
tr.tool_use_id,
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { ContentBlock, ToolResultContent } from "../providers/types.js";
|
|
5
|
+
import {
|
|
6
|
+
buildTruncatedContent,
|
|
7
|
+
getToolResultFilePath,
|
|
8
|
+
isTruncationEligible,
|
|
9
|
+
TOOL_RESULT_DIR,
|
|
10
|
+
} from "./post-turn-tool-result-truncation.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* AX-tree snapshots have their own dedicated history compactor
|
|
14
|
+
* (`compactAxTreeHistory`) with placeholder semantics tuned for computer-use
|
|
15
|
+
* sessions, and the model needs the freshest tree inline to act on it, so the
|
|
16
|
+
* result-time pass leaves them alone.
|
|
17
|
+
*/
|
|
18
|
+
const AX_TREE_TAG = "<ax-tree>";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Tools whose results keep their full content at result time even when
|
|
22
|
+
* oversized. `file_read` is the model's only way to page spooled content back
|
|
23
|
+
* into context: stubbing a read of a `.tool-results/` file would spool a
|
|
24
|
+
* fresh copy and hand back another stub, so oversized content could never be
|
|
25
|
+
* read at all. Explicit reads are therefore honored in full; the post-turn
|
|
26
|
+
* pass still truncates them at turn end, after the model has consumed the
|
|
27
|
+
* content.
|
|
28
|
+
*/
|
|
29
|
+
const RESULT_TIME_STUB_EXEMPT_TOOLS = new Set<string>(["file_read"]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Whether a tool result is eligible for the result-time spool/stub pass:
|
|
33
|
+
* the post-turn pass's shared rules plus the AX-tree and `file_read`
|
|
34
|
+
* exemptions above.
|
|
35
|
+
*/
|
|
36
|
+
function isSpoolEligible(
|
|
37
|
+
tr: ToolResultContent,
|
|
38
|
+
toolName: string | undefined,
|
|
39
|
+
): boolean {
|
|
40
|
+
if (!isTruncationEligible(tr, toolName)) return false;
|
|
41
|
+
if (toolName !== undefined && RESULT_TIME_STUB_EXEMPT_TOOLS.has(toolName)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (tr.content.includes(AX_TREE_TAG)) return false;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Spool every oversized tool result in `blocks` to its deterministic
|
|
50
|
+
* `.tool-results/` file and replace the inline content with the post-turn
|
|
51
|
+
* pass's prefix/suffix stub — at result time, before the blocks join history.
|
|
52
|
+
*
|
|
53
|
+
* Because the swap happens before the content is ever sent, the
|
|
54
|
+
* provider-bound history stays strictly append-only across the turn's model
|
|
55
|
+
* calls, which is what keeps the provider's prompt-cache prefix valid:
|
|
56
|
+
* rewriting an earlier message between calls would invalidate the cache from
|
|
57
|
+
* that point on every iteration. The model still gets the head/tail preview
|
|
58
|
+
* plus the on-disk path, so it can page the full content back in with
|
|
59
|
+
* `file_read` (whose results are exempt from this pass) when it actually
|
|
60
|
+
* needs it.
|
|
61
|
+
*
|
|
62
|
+
* Uses the same file paths, stub bytes, and eligibility rules as
|
|
63
|
+
* `postTurnTruncateToolResults`, whose `TRUNCATION_MARKER` guard then skips
|
|
64
|
+
* these results at turn end. Eligible elements of `blocks` are replaced in
|
|
65
|
+
* place (the block objects themselves are not mutated). A result is only
|
|
66
|
+
* stubbed after its file is written, so a stub never points at a missing
|
|
67
|
+
* file; on filesystem errors the remaining blocks keep their full content and
|
|
68
|
+
* the post-turn pass covers them.
|
|
69
|
+
*
|
|
70
|
+
* Returns the number of results spooled and stubbed.
|
|
71
|
+
*/
|
|
72
|
+
export function spoolAndStubOversizedToolResults(
|
|
73
|
+
blocks: ContentBlock[],
|
|
74
|
+
options: {
|
|
75
|
+
conversationDir: string;
|
|
76
|
+
toolNameById: (toolUseId: string) => string | undefined;
|
|
77
|
+
},
|
|
78
|
+
): number {
|
|
79
|
+
let stubbedCount = 0;
|
|
80
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
81
|
+
const block = blocks[i];
|
|
82
|
+
if (block.type !== "tool_result") continue;
|
|
83
|
+
if (!isSpoolEligible(block, options.toolNameById(block.tool_use_id))) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (stubbedCount === 0) {
|
|
88
|
+
mkdirSync(join(options.conversationDir, TOOL_RESULT_DIR), {
|
|
89
|
+
recursive: true,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const filePath = getToolResultFilePath(
|
|
93
|
+
options.conversationDir,
|
|
94
|
+
block.tool_use_id,
|
|
95
|
+
);
|
|
96
|
+
writeFileSync(filePath, block.content, "utf-8");
|
|
97
|
+
blocks[i] = {
|
|
98
|
+
...block,
|
|
99
|
+
content: buildTruncatedContent(block.content, filePath),
|
|
100
|
+
};
|
|
101
|
+
stubbedCount++;
|
|
102
|
+
}
|
|
103
|
+
return stubbedCount;
|
|
104
|
+
}
|