@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
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the default `exploration-drift` plugin's `post-tool-use` hook.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - The hook surfaces the canonical long-dig nudge via `additionalContext`
|
|
6
|
+
* (leaving the tool result's `content` untouched) once a turn accumulates an
|
|
7
|
+
* unbroken threshold-length run of exploration tool calls, and stays silent
|
|
8
|
+
* below it.
|
|
9
|
+
* - The loop trigger: on loop-prone models (Kimi K2.6, MiniMax M3),
|
|
10
|
+
* re-issuing a
|
|
11
|
+
* byte-identical exploration call fires a loop nudge well before the
|
|
12
|
+
* long-dig threshold; other models are unaffected; the signature comparison
|
|
13
|
+
* is key-order independent; the nudge re-fires on further duplicates and
|
|
14
|
+
* stops when the model moves on to fresh calls.
|
|
15
|
+
* - The streak is bounded by a real user message, a non-empty assistant text
|
|
16
|
+
* block, and a non-exploration tool result.
|
|
17
|
+
* - Repeat long-dig nudges are spaced one full threshold apart.
|
|
18
|
+
* - Subagent conversations are exempt.
|
|
19
|
+
* - The nudge appends to (not overwrites) `additionalContext` set by an
|
|
20
|
+
* earlier hook in the chain (e.g. tool-error coaching).
|
|
21
|
+
* - End-to-end through `runHook` + the registry.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
25
|
+
|
|
26
|
+
// Mock the subagent manager before importing the hook — the hook lazily
|
|
27
|
+
// imports it on the nudge path to exempt subagent conversations.
|
|
28
|
+
let mockParentInfo: (conversationId: string) => unknown = () => undefined;
|
|
29
|
+
mock.module("../subagent/index.js", () => ({
|
|
30
|
+
getSubagentManager: () => ({
|
|
31
|
+
getParentInfo: (conversationId: string) => mockParentInfo(conversationId),
|
|
32
|
+
}),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
import { HOOKS } from "../plugin-api/constants.js";
|
|
36
|
+
import type { PluginLogger, PostToolUseContext } from "../plugin-api/types.js";
|
|
37
|
+
import postToolUse, {
|
|
38
|
+
EXPLORATION_DRIFT_NUDGE_TEXT,
|
|
39
|
+
EXPLORATION_LOOP_REPEAT_THRESHOLD,
|
|
40
|
+
EXPLORATION_NUDGE_THRESHOLD,
|
|
41
|
+
explorationLoopNudgeText,
|
|
42
|
+
resetExplorationDriftStateForTests,
|
|
43
|
+
} from "../plugins/defaults/exploration-drift/hooks/post-tool-use.js";
|
|
44
|
+
import { defaultExplorationDriftPlugin } from "../plugins/defaults/index.js";
|
|
45
|
+
import { runHook } from "../plugins/pipeline.js";
|
|
46
|
+
import {
|
|
47
|
+
registerPlugin,
|
|
48
|
+
resetPluginRegistryForTests,
|
|
49
|
+
} from "../plugins/registry.js";
|
|
50
|
+
import type { Message, ToolResultContent } from "../providers/types.js";
|
|
51
|
+
|
|
52
|
+
const noopLogger: PluginLogger = {
|
|
53
|
+
info: () => {},
|
|
54
|
+
warn: () => {},
|
|
55
|
+
error: () => {},
|
|
56
|
+
debug: () => {},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const BASE_CONTENT = "grep output";
|
|
60
|
+
|
|
61
|
+
/** A model id outside the loop-prone set. */
|
|
62
|
+
const GENERIC_MODEL = "claude-test-model";
|
|
63
|
+
/** Kimi K2.6 as reported by Fireworks. */
|
|
64
|
+
const KIMI_FIREWORKS_MODEL = "accounts/fireworks/models/kimi-k2p6";
|
|
65
|
+
/** Kimi K2.6 as reported by OpenRouter. */
|
|
66
|
+
const KIMI_OPENROUTER_MODEL = "moonshotai/kimi-k2.6";
|
|
67
|
+
/** MiniMax M3 as reported by OpenRouter. */
|
|
68
|
+
const MINIMAX_OPENROUTER_MODEL = "minimax/minimax-m3";
|
|
69
|
+
|
|
70
|
+
let conversationCounter = 0;
|
|
71
|
+
/** Unique conversation id per test so the per-conversation state can't leak. */
|
|
72
|
+
function freshConversationId(): string {
|
|
73
|
+
conversationCounter++;
|
|
74
|
+
return `conv-drift-test-${conversationCounter}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Assistant turn issuing a single `tool_use` block. */
|
|
78
|
+
function toolUseTurn(
|
|
79
|
+
id: string,
|
|
80
|
+
name: string,
|
|
81
|
+
input: Record<string, unknown> = {},
|
|
82
|
+
): Message {
|
|
83
|
+
return {
|
|
84
|
+
role: "assistant",
|
|
85
|
+
content: [{ type: "tool_use", id, name, input }],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** User turn carrying a single `tool_result` for a prior `tool_use`. */
|
|
90
|
+
function toolResultTurn(toolUseId: string): Message {
|
|
91
|
+
return {
|
|
92
|
+
role: "user",
|
|
93
|
+
content: [
|
|
94
|
+
{
|
|
95
|
+
type: "tool_result",
|
|
96
|
+
tool_use_id: toolUseId,
|
|
97
|
+
content: "result",
|
|
98
|
+
is_error: false,
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function currentResponse(toolUseId: string): ToolResultContent {
|
|
105
|
+
return {
|
|
106
|
+
type: "tool_result",
|
|
107
|
+
tool_use_id: toolUseId,
|
|
108
|
+
content: BASE_CONTENT,
|
|
109
|
+
is_error: false,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function makeCtx(
|
|
114
|
+
conversationId: string,
|
|
115
|
+
toolResponse: ToolResultContent,
|
|
116
|
+
messages: Message[],
|
|
117
|
+
model: string = GENERIC_MODEL,
|
|
118
|
+
): PostToolUseContext {
|
|
119
|
+
return {
|
|
120
|
+
conversationId,
|
|
121
|
+
toolResponse,
|
|
122
|
+
messages,
|
|
123
|
+
additionalContext: null,
|
|
124
|
+
model,
|
|
125
|
+
maxInputTokens: 10_000,
|
|
126
|
+
logger: noopLogger,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Build a history of `priorCalls` completed exploration tool calls of
|
|
132
|
+
* `toolName` — each with a distinct input, so the loop trigger never fires on
|
|
133
|
+
* histories meant to exercise the long-dig threshold — followed by the
|
|
134
|
+
* current turn's `tool_use` (whose result is delivered via
|
|
135
|
+
* `ctx.toolResponse`, not history).
|
|
136
|
+
*/
|
|
137
|
+
function explorationHistory(
|
|
138
|
+
priorCalls: number,
|
|
139
|
+
toolName = "bash",
|
|
140
|
+
): { messages: Message[]; currentToolUseId: string } {
|
|
141
|
+
const messages: Message[] = [
|
|
142
|
+
{ role: "user", content: [{ type: "text", text: "investigate the bug" }] },
|
|
143
|
+
];
|
|
144
|
+
for (let i = 0; i < priorCalls; i++) {
|
|
145
|
+
const id = `${toolName}-${i}`;
|
|
146
|
+
messages.push(toolUseTurn(id, toolName, { command: `cmd-${i}` }));
|
|
147
|
+
messages.push(toolResultTurn(id));
|
|
148
|
+
}
|
|
149
|
+
const currentToolUseId = `${toolName}-current`;
|
|
150
|
+
messages.push(
|
|
151
|
+
toolUseTurn(currentToolUseId, toolName, { command: "cmd-current" }),
|
|
152
|
+
);
|
|
153
|
+
return { messages, currentToolUseId };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Build a history where the current call repeats a prior call's exact input
|
|
158
|
+
* `priorIdenticalCalls` times, padded in front with `distinctCalls` calls
|
|
159
|
+
* with unique inputs.
|
|
160
|
+
*/
|
|
161
|
+
function repeatedCallHistory(opts: {
|
|
162
|
+
priorIdenticalCalls: number;
|
|
163
|
+
distinctCalls?: number;
|
|
164
|
+
input?: Record<string, unknown>;
|
|
165
|
+
currentInput?: Record<string, unknown>;
|
|
166
|
+
}): { messages: Message[]; currentToolUseId: string } {
|
|
167
|
+
const repeatedInput = opts.input ?? { command: "grep -r needle src" };
|
|
168
|
+
const messages: Message[] = [
|
|
169
|
+
{ role: "user", content: [{ type: "text", text: "investigate the bug" }] },
|
|
170
|
+
];
|
|
171
|
+
for (let i = 0; i < (opts.distinctCalls ?? 0); i++) {
|
|
172
|
+
const id = `distinct-${i}`;
|
|
173
|
+
messages.push(toolUseTurn(id, "bash", { command: `unique-${i}` }));
|
|
174
|
+
messages.push(toolResultTurn(id));
|
|
175
|
+
}
|
|
176
|
+
for (let i = 0; i < opts.priorIdenticalCalls; i++) {
|
|
177
|
+
const id = `repeat-${i}`;
|
|
178
|
+
messages.push(toolUseTurn(id, "bash", { ...repeatedInput }));
|
|
179
|
+
messages.push(toolResultTurn(id));
|
|
180
|
+
}
|
|
181
|
+
const currentToolUseId = "repeat-current";
|
|
182
|
+
messages.push(
|
|
183
|
+
toolUseTurn(currentToolUseId, "bash", {
|
|
184
|
+
...(opts.currentInput ?? repeatedInput),
|
|
185
|
+
}),
|
|
186
|
+
);
|
|
187
|
+
return { messages, currentToolUseId };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
beforeEach(() => {
|
|
191
|
+
resetExplorationDriftStateForTests();
|
|
192
|
+
mockParentInfo = () => undefined;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("exploration-drift post-tool-use hook — long-dig trigger", () => {
|
|
196
|
+
test("stays silent below the threshold", async () => {
|
|
197
|
+
const { messages, currentToolUseId } = explorationHistory(
|
|
198
|
+
EXPLORATION_NUDGE_THRESHOLD - 2,
|
|
199
|
+
);
|
|
200
|
+
const ctx = makeCtx(
|
|
201
|
+
freshConversationId(),
|
|
202
|
+
currentResponse(currentToolUseId),
|
|
203
|
+
messages,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
await postToolUse(ctx);
|
|
207
|
+
|
|
208
|
+
expect(ctx.additionalContext).toBeNull();
|
|
209
|
+
expect(ctx.toolResponse.content).toBe(BASE_CONTENT);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("nudges once the streak reaches the threshold, leaving the result untouched", async () => {
|
|
213
|
+
const { messages, currentToolUseId } = explorationHistory(
|
|
214
|
+
EXPLORATION_NUDGE_THRESHOLD - 1,
|
|
215
|
+
);
|
|
216
|
+
const ctx = makeCtx(
|
|
217
|
+
freshConversationId(),
|
|
218
|
+
currentResponse(currentToolUseId),
|
|
219
|
+
messages,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
await postToolUse(ctx);
|
|
223
|
+
|
|
224
|
+
expect(ctx.additionalContext).toBe(EXPLORATION_DRIFT_NUDGE_TEXT);
|
|
225
|
+
expect(ctx.toolResponse.content).toBe(BASE_CONTENT);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("counts file_read and file_list as exploration tools", async () => {
|
|
229
|
+
const messages: Message[] = [];
|
|
230
|
+
for (let i = 0; i < EXPLORATION_NUDGE_THRESHOLD - 1; i++) {
|
|
231
|
+
const name = i % 2 === 0 ? "file_read" : "file_list";
|
|
232
|
+
const id = `${name}-${i}`;
|
|
233
|
+
messages.push(toolUseTurn(id, name, { path: `/tmp/file-${i}` }));
|
|
234
|
+
messages.push(toolResultTurn(id));
|
|
235
|
+
}
|
|
236
|
+
messages.push(toolUseTurn("bash-current", "bash", { command: "ls" }));
|
|
237
|
+
const ctx = makeCtx(
|
|
238
|
+
freshConversationId(),
|
|
239
|
+
currentResponse("bash-current"),
|
|
240
|
+
messages,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
await postToolUse(ctx);
|
|
244
|
+
|
|
245
|
+
expect(ctx.additionalContext).toBe(EXPLORATION_DRIFT_NUDGE_TEXT);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("a non-empty assistant text block resets the streak", async () => {
|
|
249
|
+
// GIVEN a long run interrupted by the model speaking to the user, with
|
|
250
|
+
// fewer than threshold calls after the text.
|
|
251
|
+
const { messages, currentToolUseId } = explorationHistory(
|
|
252
|
+
EXPLORATION_NUDGE_THRESHOLD - 1,
|
|
253
|
+
);
|
|
254
|
+
messages.splice(messages.length - 1, 0, {
|
|
255
|
+
role: "assistant",
|
|
256
|
+
content: [{ type: "text", text: "Here is what I found so far…" }],
|
|
257
|
+
});
|
|
258
|
+
const ctx = makeCtx(
|
|
259
|
+
freshConversationId(),
|
|
260
|
+
currentResponse(currentToolUseId),
|
|
261
|
+
messages,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
await postToolUse(ctx);
|
|
265
|
+
|
|
266
|
+
expect(ctx.additionalContext).toBeNull();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("a non-exploration tool result breaks the streak", async () => {
|
|
270
|
+
const { messages, currentToolUseId } = explorationHistory(
|
|
271
|
+
EXPLORATION_NUDGE_THRESHOLD - 1,
|
|
272
|
+
);
|
|
273
|
+
// Replace the second-to-last completed call with a write tool.
|
|
274
|
+
messages.splice(
|
|
275
|
+
messages.length - 1,
|
|
276
|
+
0,
|
|
277
|
+
toolUseTurn("write-1", "file_write"),
|
|
278
|
+
toolResultTurn("write-1"),
|
|
279
|
+
);
|
|
280
|
+
const ctx = makeCtx(
|
|
281
|
+
freshConversationId(),
|
|
282
|
+
currentResponse(currentToolUseId),
|
|
283
|
+
messages,
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
await postToolUse(ctx);
|
|
287
|
+
|
|
288
|
+
expect(ctx.additionalContext).toBeNull();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("is a no-op when the current tool is not an exploration tool", async () => {
|
|
292
|
+
const { messages } = explorationHistory(EXPLORATION_NUDGE_THRESHOLD * 2);
|
|
293
|
+
messages.push(toolUseTurn("write-current", "file_write"));
|
|
294
|
+
const ctx = makeCtx(
|
|
295
|
+
freshConversationId(),
|
|
296
|
+
currentResponse("write-current"),
|
|
297
|
+
messages,
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
await postToolUse(ctx);
|
|
301
|
+
|
|
302
|
+
expect(ctx.additionalContext).toBeNull();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("repeat nudges are spaced one full threshold apart", async () => {
|
|
306
|
+
const conversationId = freshConversationId();
|
|
307
|
+
|
|
308
|
+
// First nudge at the threshold.
|
|
309
|
+
const first = explorationHistory(EXPLORATION_NUDGE_THRESHOLD - 1);
|
|
310
|
+
const firstCtx = makeCtx(
|
|
311
|
+
conversationId,
|
|
312
|
+
currentResponse(first.currentToolUseId),
|
|
313
|
+
first.messages,
|
|
314
|
+
);
|
|
315
|
+
await postToolUse(firstCtx);
|
|
316
|
+
expect(firstCtx.additionalContext).toBe(EXPLORATION_DRIFT_NUDGE_TEXT);
|
|
317
|
+
|
|
318
|
+
// One more call right after — silent.
|
|
319
|
+
const next = explorationHistory(EXPLORATION_NUDGE_THRESHOLD);
|
|
320
|
+
const nextCtx = makeCtx(
|
|
321
|
+
conversationId,
|
|
322
|
+
currentResponse(next.currentToolUseId),
|
|
323
|
+
next.messages,
|
|
324
|
+
);
|
|
325
|
+
await postToolUse(nextCtx);
|
|
326
|
+
expect(nextCtx.additionalContext).toBeNull();
|
|
327
|
+
|
|
328
|
+
// Another full threshold later — nudges again.
|
|
329
|
+
const second = explorationHistory(EXPLORATION_NUDGE_THRESHOLD * 2 - 1);
|
|
330
|
+
const secondCtx = makeCtx(
|
|
331
|
+
conversationId,
|
|
332
|
+
currentResponse(second.currentToolUseId),
|
|
333
|
+
second.messages,
|
|
334
|
+
);
|
|
335
|
+
await postToolUse(secondCtx);
|
|
336
|
+
expect(secondCtx.additionalContext).toBe(EXPLORATION_DRIFT_NUDGE_TEXT);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("a restarted streak (new turn) nudges at the threshold again", async () => {
|
|
340
|
+
const conversationId = freshConversationId();
|
|
341
|
+
|
|
342
|
+
// Fire once deep into a long run.
|
|
343
|
+
const longRun = explorationHistory(EXPLORATION_NUDGE_THRESHOLD * 2 - 1);
|
|
344
|
+
const longCtx = makeCtx(
|
|
345
|
+
conversationId,
|
|
346
|
+
currentResponse(longRun.currentToolUseId),
|
|
347
|
+
longRun.messages,
|
|
348
|
+
);
|
|
349
|
+
await postToolUse(longCtx);
|
|
350
|
+
expect(longCtx.additionalContext).toBe(EXPLORATION_DRIFT_NUDGE_TEXT);
|
|
351
|
+
|
|
352
|
+
// A new turn starts (streak restarts from a fresh history) and runs to
|
|
353
|
+
// the threshold — the stale high-water mark must not suppress this nudge.
|
|
354
|
+
const newTurn = explorationHistory(EXPLORATION_NUDGE_THRESHOLD - 1);
|
|
355
|
+
const newCtx = makeCtx(
|
|
356
|
+
conversationId,
|
|
357
|
+
currentResponse(newTurn.currentToolUseId),
|
|
358
|
+
newTurn.messages,
|
|
359
|
+
);
|
|
360
|
+
await postToolUse(newCtx);
|
|
361
|
+
expect(newCtx.additionalContext).toBe(EXPLORATION_DRIFT_NUDGE_TEXT);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("subagent conversations are exempt", async () => {
|
|
365
|
+
mockParentInfo = () => ({
|
|
366
|
+
parentConversationId: "parent-1",
|
|
367
|
+
subagentId: "sub-1",
|
|
368
|
+
label: "investigate-empty-turns",
|
|
369
|
+
parentSendToClient: () => {},
|
|
370
|
+
});
|
|
371
|
+
const { messages, currentToolUseId } = explorationHistory(
|
|
372
|
+
EXPLORATION_NUDGE_THRESHOLD - 1,
|
|
373
|
+
);
|
|
374
|
+
const ctx = makeCtx(
|
|
375
|
+
freshConversationId(),
|
|
376
|
+
currentResponse(currentToolUseId),
|
|
377
|
+
messages,
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
await postToolUse(ctx);
|
|
381
|
+
|
|
382
|
+
expect(ctx.additionalContext).toBeNull();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("appends to additionalContext set by an earlier hook", async () => {
|
|
386
|
+
const { messages, currentToolUseId } = explorationHistory(
|
|
387
|
+
EXPLORATION_NUDGE_THRESHOLD - 1,
|
|
388
|
+
);
|
|
389
|
+
const ctx = makeCtx(
|
|
390
|
+
freshConversationId(),
|
|
391
|
+
currentResponse(currentToolUseId),
|
|
392
|
+
messages,
|
|
393
|
+
);
|
|
394
|
+
ctx.additionalContext = "<system_notice>earlier coaching</system_notice>";
|
|
395
|
+
|
|
396
|
+
await postToolUse(ctx);
|
|
397
|
+
|
|
398
|
+
expect(ctx.additionalContext).toBe(
|
|
399
|
+
`<system_notice>earlier coaching</system_notice>\n${EXPLORATION_DRIFT_NUDGE_TEXT}`,
|
|
400
|
+
);
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
describe("exploration-drift post-tool-use hook — loop trigger", () => {
|
|
405
|
+
test("fires on a loop-prone model when an identical call repeats to the threshold", async () => {
|
|
406
|
+
const { messages, currentToolUseId } = repeatedCallHistory({
|
|
407
|
+
priorIdenticalCalls: EXPLORATION_LOOP_REPEAT_THRESHOLD - 1,
|
|
408
|
+
});
|
|
409
|
+
const ctx = makeCtx(
|
|
410
|
+
freshConversationId(),
|
|
411
|
+
currentResponse(currentToolUseId),
|
|
412
|
+
messages,
|
|
413
|
+
KIMI_FIREWORKS_MODEL,
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
await postToolUse(ctx);
|
|
417
|
+
|
|
418
|
+
expect(ctx.additionalContext).toBe(
|
|
419
|
+
explorationLoopNudgeText("bash", EXPLORATION_LOOP_REPEAT_THRESHOLD),
|
|
420
|
+
);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("matches the OpenRouter Kimi K2.6 model id", async () => {
|
|
424
|
+
const { messages, currentToolUseId } = repeatedCallHistory({
|
|
425
|
+
priorIdenticalCalls: EXPLORATION_LOOP_REPEAT_THRESHOLD - 1,
|
|
426
|
+
});
|
|
427
|
+
const ctx = makeCtx(
|
|
428
|
+
freshConversationId(),
|
|
429
|
+
currentResponse(currentToolUseId),
|
|
430
|
+
messages,
|
|
431
|
+
KIMI_OPENROUTER_MODEL,
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
await postToolUse(ctx);
|
|
435
|
+
|
|
436
|
+
expect(ctx.additionalContext).toBe(
|
|
437
|
+
explorationLoopNudgeText("bash", EXPLORATION_LOOP_REPEAT_THRESHOLD),
|
|
438
|
+
);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test("matches the MiniMax M3 model id", async () => {
|
|
442
|
+
const { messages, currentToolUseId } = repeatedCallHistory({
|
|
443
|
+
priorIdenticalCalls: EXPLORATION_LOOP_REPEAT_THRESHOLD - 1,
|
|
444
|
+
});
|
|
445
|
+
const ctx = makeCtx(
|
|
446
|
+
freshConversationId(),
|
|
447
|
+
currentResponse(currentToolUseId),
|
|
448
|
+
messages,
|
|
449
|
+
MINIMAX_OPENROUTER_MODEL,
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
await postToolUse(ctx);
|
|
453
|
+
|
|
454
|
+
expect(ctx.additionalContext).toBe(
|
|
455
|
+
explorationLoopNudgeText("bash", EXPLORATION_LOOP_REPEAT_THRESHOLD),
|
|
456
|
+
);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test("stays silent on other models for the same repeated-call history", async () => {
|
|
460
|
+
const { messages, currentToolUseId } = repeatedCallHistory({
|
|
461
|
+
priorIdenticalCalls: EXPLORATION_LOOP_REPEAT_THRESHOLD - 1,
|
|
462
|
+
});
|
|
463
|
+
const ctx = makeCtx(
|
|
464
|
+
freshConversationId(),
|
|
465
|
+
currentResponse(currentToolUseId),
|
|
466
|
+
messages,
|
|
467
|
+
GENERIC_MODEL,
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
await postToolUse(ctx);
|
|
471
|
+
|
|
472
|
+
expect(ctx.additionalContext).toBeNull();
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test("stays silent below the repeat threshold", async () => {
|
|
476
|
+
const { messages, currentToolUseId } = repeatedCallHistory({
|
|
477
|
+
priorIdenticalCalls: EXPLORATION_LOOP_REPEAT_THRESHOLD - 2,
|
|
478
|
+
});
|
|
479
|
+
const ctx = makeCtx(
|
|
480
|
+
freshConversationId(),
|
|
481
|
+
currentResponse(currentToolUseId),
|
|
482
|
+
messages,
|
|
483
|
+
KIMI_FIREWORKS_MODEL,
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
await postToolUse(ctx);
|
|
487
|
+
|
|
488
|
+
expect(ctx.additionalContext).toBeNull();
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test("stays silent on a loop-prone model when calls are distinct", async () => {
|
|
492
|
+
const { messages, currentToolUseId } = explorationHistory(
|
|
493
|
+
EXPLORATION_NUDGE_THRESHOLD - 2,
|
|
494
|
+
);
|
|
495
|
+
const ctx = makeCtx(
|
|
496
|
+
freshConversationId(),
|
|
497
|
+
currentResponse(currentToolUseId),
|
|
498
|
+
messages,
|
|
499
|
+
KIMI_FIREWORKS_MODEL,
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
await postToolUse(ctx);
|
|
503
|
+
|
|
504
|
+
expect(ctx.additionalContext).toBeNull();
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test("input signature comparison is key-order independent", async () => {
|
|
508
|
+
const { messages, currentToolUseId } = repeatedCallHistory({
|
|
509
|
+
priorIdenticalCalls: EXPLORATION_LOOP_REPEAT_THRESHOLD - 1,
|
|
510
|
+
input: { command: "cat /tmp/log", timeout: 5 },
|
|
511
|
+
currentInput: { timeout: 5, command: "cat /tmp/log" },
|
|
512
|
+
});
|
|
513
|
+
const ctx = makeCtx(
|
|
514
|
+
freshConversationId(),
|
|
515
|
+
currentResponse(currentToolUseId),
|
|
516
|
+
messages,
|
|
517
|
+
KIMI_FIREWORKS_MODEL,
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
await postToolUse(ctx);
|
|
521
|
+
|
|
522
|
+
expect(ctx.additionalContext).toBe(
|
|
523
|
+
explorationLoopNudgeText("bash", EXPLORATION_LOOP_REPEAT_THRESHOLD),
|
|
524
|
+
);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test("re-fires on the next duplicate but not on the same streak twice", async () => {
|
|
528
|
+
const conversationId = freshConversationId();
|
|
529
|
+
|
|
530
|
+
// First loop nudge.
|
|
531
|
+
const first = repeatedCallHistory({
|
|
532
|
+
priorIdenticalCalls: EXPLORATION_LOOP_REPEAT_THRESHOLD - 1,
|
|
533
|
+
});
|
|
534
|
+
const firstCtx = makeCtx(
|
|
535
|
+
conversationId,
|
|
536
|
+
currentResponse(first.currentToolUseId),
|
|
537
|
+
first.messages,
|
|
538
|
+
KIMI_FIREWORKS_MODEL,
|
|
539
|
+
);
|
|
540
|
+
await postToolUse(firstCtx);
|
|
541
|
+
expect(firstCtx.additionalContext).toBe(
|
|
542
|
+
explorationLoopNudgeText("bash", EXPLORATION_LOOP_REPEAT_THRESHOLD),
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
// A parallel sibling result observing the same history (same streak)
|
|
546
|
+
// dedupes.
|
|
547
|
+
const siblingCtx = makeCtx(
|
|
548
|
+
conversationId,
|
|
549
|
+
currentResponse(first.currentToolUseId),
|
|
550
|
+
first.messages,
|
|
551
|
+
KIMI_FIREWORKS_MODEL,
|
|
552
|
+
);
|
|
553
|
+
await postToolUse(siblingCtx);
|
|
554
|
+
expect(siblingCtx.additionalContext).toBeNull();
|
|
555
|
+
|
|
556
|
+
// The model ignores the nudge and issues the same call once more — the
|
|
557
|
+
// streak grew by one, so the nudge re-fires with the higher count.
|
|
558
|
+
const second = repeatedCallHistory({
|
|
559
|
+
priorIdenticalCalls: EXPLORATION_LOOP_REPEAT_THRESHOLD,
|
|
560
|
+
});
|
|
561
|
+
const secondCtx = makeCtx(
|
|
562
|
+
conversationId,
|
|
563
|
+
currentResponse(second.currentToolUseId),
|
|
564
|
+
second.messages,
|
|
565
|
+
KIMI_FIREWORKS_MODEL,
|
|
566
|
+
);
|
|
567
|
+
await postToolUse(secondCtx);
|
|
568
|
+
expect(secondCtx.additionalContext).toBe(
|
|
569
|
+
explorationLoopNudgeText("bash", EXPLORATION_LOOP_REPEAT_THRESHOLD + 1),
|
|
570
|
+
);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
test("stops nudging once the model moves on to fresh calls", async () => {
|
|
574
|
+
const conversationId = freshConversationId();
|
|
575
|
+
|
|
576
|
+
const looped = repeatedCallHistory({
|
|
577
|
+
priorIdenticalCalls: EXPLORATION_LOOP_REPEAT_THRESHOLD - 1,
|
|
578
|
+
});
|
|
579
|
+
const loopedCtx = makeCtx(
|
|
580
|
+
conversationId,
|
|
581
|
+
currentResponse(looped.currentToolUseId),
|
|
582
|
+
looped.messages,
|
|
583
|
+
KIMI_FIREWORKS_MODEL,
|
|
584
|
+
);
|
|
585
|
+
await postToolUse(loopedCtx);
|
|
586
|
+
expect(loopedCtx.additionalContext).not.toBeNull();
|
|
587
|
+
|
|
588
|
+
// Next call has a fresh input — even though the duplicates remain in the
|
|
589
|
+
// trailing run, the current call is not a repeat, so no loop nudge.
|
|
590
|
+
const moved = repeatedCallHistory({
|
|
591
|
+
priorIdenticalCalls: EXPLORATION_LOOP_REPEAT_THRESHOLD,
|
|
592
|
+
currentInput: { command: "a brand new command" },
|
|
593
|
+
});
|
|
594
|
+
const movedCtx = makeCtx(
|
|
595
|
+
conversationId,
|
|
596
|
+
currentResponse(moved.currentToolUseId),
|
|
597
|
+
moved.messages,
|
|
598
|
+
KIMI_FIREWORKS_MODEL,
|
|
599
|
+
);
|
|
600
|
+
await postToolUse(movedCtx);
|
|
601
|
+
expect(movedCtx.additionalContext).toBeNull();
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
test("subagent conversations are exempt from the loop trigger", async () => {
|
|
605
|
+
mockParentInfo = () => ({
|
|
606
|
+
parentConversationId: "parent-1",
|
|
607
|
+
subagentId: "sub-1",
|
|
608
|
+
label: "investigate-empty-turns",
|
|
609
|
+
parentSendToClient: () => {},
|
|
610
|
+
});
|
|
611
|
+
const { messages, currentToolUseId } = repeatedCallHistory({
|
|
612
|
+
priorIdenticalCalls: EXPLORATION_LOOP_REPEAT_THRESHOLD - 1,
|
|
613
|
+
});
|
|
614
|
+
const ctx = makeCtx(
|
|
615
|
+
freshConversationId(),
|
|
616
|
+
currentResponse(currentToolUseId),
|
|
617
|
+
messages,
|
|
618
|
+
KIMI_FIREWORKS_MODEL,
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
await postToolUse(ctx);
|
|
622
|
+
|
|
623
|
+
expect(ctx.additionalContext).toBeNull();
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
test("a long-dig-threshold run of identical calls uses the long-dig text once past the threshold", async () => {
|
|
627
|
+
// When both triggers are eligible the long-dig path wins — by then the
|
|
628
|
+
// generic guidance covers the loop case too.
|
|
629
|
+
const { messages, currentToolUseId } = repeatedCallHistory({
|
|
630
|
+
priorIdenticalCalls: EXPLORATION_NUDGE_THRESHOLD - 1,
|
|
631
|
+
});
|
|
632
|
+
const conversationId = freshConversationId();
|
|
633
|
+
const ctx = makeCtx(
|
|
634
|
+
conversationId,
|
|
635
|
+
currentResponse(currentToolUseId),
|
|
636
|
+
messages,
|
|
637
|
+
KIMI_FIREWORKS_MODEL,
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
await postToolUse(ctx);
|
|
641
|
+
|
|
642
|
+
// The loop trigger would have fired far earlier in a live run; with a
|
|
643
|
+
// cold conversation state at threshold length, the long-dig text wins.
|
|
644
|
+
expect(ctx.additionalContext).toBe(EXPLORATION_DRIFT_NUDGE_TEXT);
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
describe("exploration-drift post-tool-use hook — via runHook", () => {
|
|
649
|
+
beforeEach(() => {
|
|
650
|
+
resetPluginRegistryForTests();
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
test("registering the default plugin nudges a threshold-length run", async () => {
|
|
654
|
+
registerPlugin(defaultExplorationDriftPlugin);
|
|
655
|
+
const { messages, currentToolUseId } = explorationHistory(
|
|
656
|
+
EXPLORATION_NUDGE_THRESHOLD - 1,
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
const result = await runHook<PostToolUseContext>(
|
|
660
|
+
HOOKS.POST_TOOL_USE,
|
|
661
|
+
makeCtx(
|
|
662
|
+
freshConversationId(),
|
|
663
|
+
currentResponse(currentToolUseId),
|
|
664
|
+
messages,
|
|
665
|
+
),
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
expect(result.additionalContext).toBe(EXPLORATION_DRIFT_NUDGE_TEXT);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
test("registering the default plugin nudges a repeated call on Kimi K2.6", async () => {
|
|
672
|
+
registerPlugin(defaultExplorationDriftPlugin);
|
|
673
|
+
const { messages, currentToolUseId } = repeatedCallHistory({
|
|
674
|
+
priorIdenticalCalls: EXPLORATION_LOOP_REPEAT_THRESHOLD - 1,
|
|
675
|
+
distinctCalls: 2,
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
const result = await runHook<PostToolUseContext>(
|
|
679
|
+
HOOKS.POST_TOOL_USE,
|
|
680
|
+
makeCtx(
|
|
681
|
+
freshConversationId(),
|
|
682
|
+
currentResponse(currentToolUseId),
|
|
683
|
+
messages,
|
|
684
|
+
KIMI_FIREWORKS_MODEL,
|
|
685
|
+
),
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
expect(result.additionalContext).toBe(
|
|
689
|
+
explorationLoopNudgeText("bash", EXPLORATION_LOOP_REPEAT_THRESHOLD),
|
|
690
|
+
);
|
|
691
|
+
});
|
|
692
|
+
});
|