@vellumai/assistant 0.8.9-staging.2 → 0.8.9-staging.4
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/docs/activation-funnel-telemetry.md +310 -0
- package/openapi.yaml +16 -115
- package/package.json +1 -1
- package/src/__tests__/activation-early-marking.test.ts +120 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +13 -13
- package/src/__tests__/anthropic-provider.test.ts +23 -8
- package/src/__tests__/approval-cascade.test.ts +1 -1
- package/src/__tests__/compaction-direct.test.ts +32 -18
- package/src/__tests__/compaction-events.test.ts +2 -2
- package/src/__tests__/compaction.benchmark.test.ts +1 -1
- package/src/__tests__/context-overflow-reducer.test.ts +5 -5
- package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-history-web-search.test.ts +5 -0
- package/src/__tests__/conversation-media-retry.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +40 -0
- package/src/__tests__/conversation-process-callsite.test.ts +1 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +71 -0
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
- package/src/__tests__/conversation-speed-override.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +44 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
- package/src/__tests__/conversation-undo.test.ts +2 -2
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/cu-unified-flow.test.ts +36 -0
- package/src/__tests__/history-repair-hook.test.ts +2 -0
- package/src/__tests__/llm-resolver.test.ts +73 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +1 -2
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
- package/src/__tests__/persist-unsendable-image.test.ts +97 -1
- package/src/__tests__/plugin-external-api.test.ts +68 -0
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
- package/src/__tests__/published-app-updater.test.ts +138 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +5 -7
- package/src/__tests__/title-generate-hook.test.ts +2 -0
- package/src/__tests__/web-fetch.test.ts +45 -0
- package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
- package/src/acp/resolve-agent.test.ts +0 -56
- package/src/acp/resolve-agent.ts +10 -38
- package/src/agent/loop.ts +13 -27
- package/src/api/responses/memory-v3-selection-log.ts +19 -10
- package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
- package/src/cli/commands/memory-v3.ts +57 -199
- package/src/cli/lib/__tests__/install-from-github.test.ts +232 -29
- package/src/cli/lib/__tests__/plugin-details.test.ts +28 -19
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +57 -7
- package/src/cli/lib/__tests__/search-plugins.test.ts +17 -10
- package/src/cli/lib/install-from-github.ts +258 -41
- package/src/cli/lib/plugin-details.ts +20 -13
- package/src/cli/lib/plugin-marketplace.ts +23 -5
- package/src/cli/lib/search-plugins.ts +14 -8
- package/src/config/acp-defaults.ts +3 -3
- package/src/config/acp-schema.ts +1 -7
- package/src/config/bundled-skills/acp/SKILL.md +4 -17
- package/src/config/bundled-skills/acp/TOOLS.json +2 -2
- package/src/config/call-site-defaults.ts +0 -1
- package/src/config/feature-flag-registry.json +3 -18
- package/src/config/llm-resolver.ts +39 -7
- package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
- package/src/config/schemas/call-site-catalog.ts +0 -7
- package/src/config/schemas/llm.ts +17 -1
- package/src/config/schemas/memory-v3.ts +58 -8
- package/src/config/seed-inference-profiles.ts +18 -0
- package/src/context/post-turn-tool-result-truncation.ts +39 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +8 -1
- package/src/daemon/conversation-agent-loop.ts +87 -95
- package/src/daemon/conversation-error.ts +31 -4
- package/src/daemon/conversation-history.ts +1 -1
- package/src/daemon/conversation-media-retry.ts +19 -6
- package/src/daemon/conversation-messaging.ts +17 -0
- package/src/daemon/conversation-process.ts +14 -5
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +37 -1
- package/src/daemon/conversation-surfaces.ts +141 -3
- package/src/daemon/conversation.ts +48 -13
- package/src/daemon/external-plugins-bootstrap.ts +8 -3
- package/src/daemon/persist-unsendable-image.ts +62 -25
- package/src/daemon/process-message.ts +1 -1
- package/src/daemon/tool-side-effects.ts +15 -0
- package/src/memory/__tests__/activation-session-store.test.ts +41 -0
- package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
- package/src/memory/activation-session-store.ts +43 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
- package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/onboarding-events-store.ts +66 -18
- package/src/memory/schema/infrastructure.ts +13 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
- package/src/memory/v2/__tests__/page-store.test.ts +22 -0
- package/src/memory/v2/consolidation-job.ts +2 -72
- package/src/memory/v2/types.ts +5 -0
- package/src/messaging/providers/telegram-bot/api.ts +14 -5
- package/src/notifications/adapters/telegram.ts +7 -1
- package/src/plugin-api/constants.ts +2 -2
- package/src/plugin-api/index.ts +2 -2
- package/src/plugin-api/types.ts +19 -5
- package/src/plugins/defaults/compaction/compact.ts +24 -15
- package/src/plugins/defaults/compaction/context-overflow-reducer.ts +4 -4
- package/src/plugins/defaults/compaction/manager-store.ts +1 -1
- package/src/{context → plugins/defaults/compaction}/window-manager.ts +68 -12
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +12 -18
- package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +19 -66
- package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +139 -129
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +332 -164
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +516 -293
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +51 -21
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +66 -11
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +271 -110
- package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +0 -22
- package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +26 -62
- package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
- package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +3 -2
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +415 -181
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +196 -56
- package/src/plugins/defaults/memory-v3-shadow/page-content.ts +39 -2
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
- package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +15 -7
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
- package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
- package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +75 -3
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +111 -78
- package/src/plugins/defaults/memory-v3-shadow/types.ts +52 -19
- package/src/plugins/defaults/memory-v3-shadow/working-set.ts +4 -1
- package/src/plugins/external-api.ts +114 -0
- package/src/prompts/system-prompt.ts +61 -10
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +37 -2
- package/src/providers/anthropic/client.ts +9 -10
- package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
- package/src/providers/inference/logit-bias.ts +55 -0
- package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -1
- package/src/providers/retry.ts +22 -0
- package/src/providers/types.ts +6 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
- package/src/runtime/routes/acp-routes.test.ts +3 -20
- package/src/runtime/routes/app-management-routes.ts +3 -0
- package/src/runtime/routes/conversation-routes.ts +2 -0
- package/src/runtime/routes/memory-v3-routes.ts +64 -314
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
- package/src/runtime/routes/stt-routes.ts +45 -12
- package/src/runtime/routes/workspace-routes.ts +50 -15
- package/src/services/published-app-updater.ts +30 -8
- package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
- package/src/telemetry/activation-funnel.ts +167 -0
- package/src/telemetry/types.ts +13 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
- package/src/telemetry/usage-telemetry-reporter.ts +26 -1
- package/src/tools/acp/list-agents.test.ts +2 -18
- package/src/tools/acp/list-agents.ts +3 -15
- package/src/tools/acp/spawn.test.ts +0 -10
- package/src/tools/browser/browser-execution.ts +12 -2
- package/src/tools/network/web-fetch.ts +65 -24
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/ui-surface/definitions.ts +7 -0
- package/src/acp/feature-gate.test.ts +0 -48
- package/src/acp/feature-gate.ts +0 -34
- package/src/plugins/defaults/memory-v3-shadow/__tests__/assign.test.ts +0 -242
- package/src/plugins/defaults/memory-v3-shadow/__tests__/core.test.ts +0 -39
- package/src/plugins/defaults/memory-v3-shadow/__tests__/health.test.ts +0 -219
- package/src/plugins/defaults/memory-v3-shadow/__tests__/needle.test.ts +0 -107
- package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +0 -13
- package/src/plugins/defaults/memory-v3-shadow/__tests__/reconcile.test.ts +0 -274
- package/src/plugins/defaults/memory-v3-shadow/__tests__/router.test.ts +0 -337
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selector.test.ts +0 -470
- package/src/plugins/defaults/memory-v3-shadow/__tests__/snapshot.test.ts +0 -168
- package/src/plugins/defaults/memory-v3-shadow/__tests__/tree.test.ts +0 -192
- package/src/plugins/defaults/memory-v3-shadow/assign.ts +0 -272
- package/src/plugins/defaults/memory-v3-shadow/core.ts +0 -26
- package/src/plugins/defaults/memory-v3-shadow/health.ts +0 -0
- package/src/plugins/defaults/memory-v3-shadow/needle.ts +0 -115
- package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +0 -26
- package/src/plugins/defaults/memory-v3-shadow/reconcile.ts +0 -527
- package/src/plugins/defaults/memory-v3-shadow/router.ts +0 -190
- package/src/plugins/defaults/memory-v3-shadow/selector.ts +0 -226
- package/src/plugins/defaults/memory-v3-shadow/snapshot.ts +0 -209
- package/src/plugins/defaults/memory-v3-shadow/tree.ts +0 -174
- package/src/util/map-limit.ts +0 -27
|
@@ -63,19 +63,6 @@ import { dirname, join } from "node:path";
|
|
|
63
63
|
|
|
64
64
|
import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
|
|
65
65
|
import type { AssistantConfig } from "../../config/types.js";
|
|
66
|
-
import { loadCore } from "../../plugins/defaults/memory-v3-shadow/core.js";
|
|
67
|
-
import {
|
|
68
|
-
computeV3Health,
|
|
69
|
-
renderV3Health,
|
|
70
|
-
} from "../../plugins/defaults/memory-v3-shadow/health.js";
|
|
71
|
-
import {
|
|
72
|
-
loadLeafTree,
|
|
73
|
-
resolveDataDir,
|
|
74
|
-
} from "../../plugins/defaults/memory-v3-shadow/tree.js";
|
|
75
|
-
import type {
|
|
76
|
-
LeafPath,
|
|
77
|
-
Slug,
|
|
78
|
-
} from "../../plugins/defaults/memory-v3-shadow/types.js";
|
|
79
66
|
import { runBackgroundJob } from "../../runtime/background-job-runner.js";
|
|
80
67
|
import { getLogger } from "../../util/logger.js";
|
|
81
68
|
import { getWorkspaceDir } from "../../util/platform.js";
|
|
@@ -86,7 +73,6 @@ import {
|
|
|
86
73
|
type MemoryJob,
|
|
87
74
|
type MemoryJobType,
|
|
88
75
|
} from "../jobs-store.js";
|
|
89
|
-
import { getPageIndex } from "../v2/page-index.js";
|
|
90
76
|
import { MEMORY_V2_CONSOLIDATION_SOURCE } from "./constants.js";
|
|
91
77
|
import { resolveConsolidationPrompt } from "./prompts/consolidation.js";
|
|
92
78
|
|
|
@@ -96,8 +82,7 @@ const log = getLogger("memory-v2-consolidate");
|
|
|
96
82
|
const JOB_NAME = "memory.consolidate";
|
|
97
83
|
|
|
98
84
|
/**
|
|
99
|
-
* v3 plugin flags. Either being on
|
|
100
|
-
* consolidation prompt and (b) enqueues `memory_v3_maintain` as a
|
|
85
|
+
* v3 plugin flags. Either being on enqueues `memory_v3_maintain` as a
|
|
101
86
|
* post-consolidation follow-up. These gate the v3 plugin itself.
|
|
102
87
|
*/
|
|
103
88
|
const MEMORY_V3_SHADOW = "memory-v3-shadow" as const;
|
|
@@ -191,15 +176,10 @@ export async function memoryV2ConsolidateJob(
|
|
|
191
176
|
// it to a regular file under 1 MiB before substitution so a stray path
|
|
192
177
|
// (or a `/dev/zero`-style pseudo-file) cannot exfiltrate megabytes of
|
|
193
178
|
// bytes through the wake hint.
|
|
194
|
-
|
|
195
|
-
// That resolved prompt is the base; `maybePrependV3Health` prepends a
|
|
196
|
-
// freshly computed v3 health block when a v3 flag is on and the tree has
|
|
197
|
-
// actionable drift, leaving it untouched otherwise.
|
|
198
|
-
const basePrompt = resolveConsolidationPrompt(
|
|
179
|
+
const prompt = resolveConsolidationPrompt(
|
|
199
180
|
config.memory.v2.consolidation_prompt_path,
|
|
200
181
|
cutoff,
|
|
201
182
|
);
|
|
202
|
-
const prompt = await maybePrependV3Health(basePrompt, config);
|
|
203
183
|
|
|
204
184
|
const runResult = await runBackgroundJob({
|
|
205
185
|
jobName: JOB_NAME,
|
|
@@ -270,56 +250,6 @@ export async function memoryV2ConsolidateJob(
|
|
|
270
250
|
}
|
|
271
251
|
}
|
|
272
252
|
|
|
273
|
-
/**
|
|
274
|
-
* When a v3 flag is enabled and the v3 tree has actionable structural drift,
|
|
275
|
-
* prepend a rendered health block to `basePrompt` so the consolidation run can
|
|
276
|
-
* fold tree maintenance into its pass. Returns `basePrompt` UNCHANGED when:
|
|
277
|
-
* - neither `memory-v3-shadow` nor `memory-v3-live` is enabled,
|
|
278
|
-
* - the health report is all-green (`renderV3Health` returns ""), or
|
|
279
|
-
* - computing the report throws for any reason.
|
|
280
|
-
*
|
|
281
|
-
* The block is computed here (not baked into the prompt template) so an
|
|
282
|
-
* operator's custom consolidation prompt stays untouched and the block reflects
|
|
283
|
-
* the tree state at run time. Health computation is wrapped so a load/compute
|
|
284
|
-
* failure can never break consolidation — the run proceeds on the base prompt.
|
|
285
|
-
*/
|
|
286
|
-
async function maybePrependV3Health(
|
|
287
|
-
basePrompt: string,
|
|
288
|
-
config: AssistantConfig,
|
|
289
|
-
): Promise<string> {
|
|
290
|
-
const v3Enabled =
|
|
291
|
-
isAssistantFeatureFlagEnabled(MEMORY_V3_SHADOW, config) ||
|
|
292
|
-
isAssistantFeatureFlagEnabled(MEMORY_V3_LIVE, config);
|
|
293
|
-
if (!v3Enabled) return basePrompt;
|
|
294
|
-
|
|
295
|
-
try {
|
|
296
|
-
const pageIndex = await getPageIndex(getWorkspaceDir());
|
|
297
|
-
const pageLeaves = new Map<Slug, LeafPath[]>();
|
|
298
|
-
const allSlugs: Slug[] = [];
|
|
299
|
-
for (const entry of pageIndex.entries) {
|
|
300
|
-
pageLeaves.set(entry.slug, entry.leaves);
|
|
301
|
-
allSlugs.push(entry.slug);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const dataDir = resolveDataDir();
|
|
305
|
-
const [tree, core] = await Promise.all([
|
|
306
|
-
loadLeafTree(dataDir, pageLeaves),
|
|
307
|
-
loadCore(dataDir),
|
|
308
|
-
]);
|
|
309
|
-
|
|
310
|
-
const report = computeV3Health({ tree, allSlugs, core });
|
|
311
|
-
const healthBlock = renderV3Health(report);
|
|
312
|
-
if (healthBlock.length === 0) return basePrompt;
|
|
313
|
-
return `${healthBlock}\n\n${basePrompt}`;
|
|
314
|
-
} catch (err) {
|
|
315
|
-
log.warn(
|
|
316
|
-
{ err: err instanceof Error ? err.message : String(err) },
|
|
317
|
-
"consolidation: v3 health computation failed; using base prompt",
|
|
318
|
-
);
|
|
319
|
-
return basePrompt;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
253
|
/**
|
|
324
254
|
* Read `memory/buffer.md`. Missing file → empty string so the skip-on-empty
|
|
325
255
|
* branch doesn't have to distinguish "no file" from "blank file".
|
package/src/memory/v2/types.ts
CHANGED
|
@@ -41,6 +41,11 @@ export const ConceptPageFrontmatterSchema = z
|
|
|
41
41
|
ref_urls: z.array(z.string().url()).default([]),
|
|
42
42
|
summary: z.string().optional(),
|
|
43
43
|
leaves: z.array(z.string()).optional(),
|
|
44
|
+
// Optional authored `"<target-slug> — <why>"` cross-links. Curated, first-class
|
|
45
|
+
// edges for the memory-v3 edge lane. Declared here so `.strict()` does not
|
|
46
|
+
// reject a page that uses `links:`, and so `renderPageContent` round-trips the
|
|
47
|
+
// field (the edge graph reads it back from the rendered frontmatter).
|
|
48
|
+
links: z.array(z.string()).optional(),
|
|
44
49
|
})
|
|
45
50
|
.strict();
|
|
46
51
|
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { credentialKey } from "../../../security/credential-key.js";
|
|
11
|
-
import {
|
|
11
|
+
import { getSecureKeyResultAsync } from "../../../security/secure-keys.js";
|
|
12
|
+
import { BackendUnavailableError, ConfigError } from "../../../util/errors.js";
|
|
12
13
|
import { getLogger } from "../../../util/logger.js";
|
|
13
14
|
|
|
14
15
|
const log = getLogger("telegram-api");
|
|
@@ -197,13 +198,21 @@ async function retryableFetch<T>(
|
|
|
197
198
|
// ---------------------------------------------------------------------------
|
|
198
199
|
|
|
199
200
|
async function resolveBotToken(): Promise<string> {
|
|
200
|
-
const
|
|
201
|
+
const result = await getSecureKeyResultAsync(
|
|
201
202
|
credentialKey("telegram", "bot_token"),
|
|
202
203
|
);
|
|
203
|
-
if (
|
|
204
|
-
|
|
204
|
+
if (result.value) {
|
|
205
|
+
return result.value;
|
|
205
206
|
}
|
|
206
|
-
|
|
207
|
+
// Distinguish a transient credential-store outage from a token that was
|
|
208
|
+
// genuinely never set. Both surface as an absent value, but only the
|
|
209
|
+
// latter is an operator misconfiguration; the former is retryable
|
|
210
|
+
// infrastructure unavailability that callers should not treat as
|
|
211
|
+
// "not configured".
|
|
212
|
+
if (result.unreachable) {
|
|
213
|
+
throw new BackendUnavailableError("Telegram credential store unreachable");
|
|
214
|
+
}
|
|
215
|
+
throw new ConfigError("Telegram bot token not configured");
|
|
207
216
|
}
|
|
208
217
|
|
|
209
218
|
// ---------------------------------------------------------------------------
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { sendTelegramReply } from "../../messaging/providers/telegram-bot/send.js";
|
|
12
12
|
import type { ApprovalUIMetadata } from "../../runtime/channel-approval-types.js";
|
|
13
|
+
import { ConfigError } from "../../util/errors.js";
|
|
13
14
|
import { getLogger } from "../../util/logger.js";
|
|
14
15
|
import { isConversationSeedSane } from "../conversation-seed-composer.js";
|
|
15
16
|
import { buildAccessRequestContractText, nonEmpty } from "../copy-composer.js";
|
|
@@ -136,7 +137,12 @@ export class TelegramAdapter implements ChannelAdapter {
|
|
|
136
137
|
return { success: true };
|
|
137
138
|
} catch (err) {
|
|
138
139
|
const message = err instanceof Error ? err.message : String(err);
|
|
139
|
-
|
|
140
|
+
// A missing bot token means the operator simply hasn't configured
|
|
141
|
+
// Telegram; it is not a code fault, so log it at warn to keep it out
|
|
142
|
+
// of Sentry. Genuine and transient failures (e.g. an unreachable
|
|
143
|
+
// credential store) stay at error so they remain visible.
|
|
144
|
+
const logFn = err instanceof ConfigError ? log.warn : log.error;
|
|
145
|
+
logFn(
|
|
140
146
|
{ err, sourceEventName: payload.sourceEventName, chatId },
|
|
141
147
|
"Failed to deliver Telegram notification",
|
|
142
148
|
);
|
|
@@ -26,8 +26,8 @@ export const HOOKS = {
|
|
|
26
26
|
POST_TOOL_USE: "post-tool-use",
|
|
27
27
|
/** Fires when the model yields a response with no tool calls — the run's stop boundary. Decides whether to stop or continue with a follow-up turn. */
|
|
28
28
|
STOP: "stop",
|
|
29
|
-
/** Fires for each finalized assistant message, before it is persisted/streamed-final. A hook may transform the message content. */
|
|
30
|
-
|
|
29
|
+
/** Fires for each finalized assistant message, once per model call, before it is persisted/streamed-final. A hook may transform the message content. */
|
|
30
|
+
POST_MODEL_CALL: "post-model-call",
|
|
31
31
|
} as const;
|
|
32
32
|
|
|
33
33
|
/** Union of every hook name declared in {@link HOOKS}. */
|
package/src/plugin-api/index.ts
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* per tool result before it joins the provider-bound history
|
|
31
31
|
* - {@link StopContext} — passed to `stop` hook, fired when the model yields
|
|
32
32
|
* a response with no tool calls
|
|
33
|
-
* - {@link
|
|
33
|
+
* - {@link PostModelCallContext} — passed to `post-model-call` hook,
|
|
34
34
|
* fired for each finalized assistant message to transform its content
|
|
35
35
|
* - {@link PluginHookFn} — signature every lifecycle hook implements
|
|
36
36
|
* - {@link PluginLogger} — pino-compatible logger shape on the contexts
|
|
@@ -43,11 +43,11 @@
|
|
|
43
43
|
export type { HookName } from "./constants.js";
|
|
44
44
|
export { HOOKS } from "./constants.js";
|
|
45
45
|
export type {
|
|
46
|
-
AssistantMessageContext,
|
|
47
46
|
PluginHookFn,
|
|
48
47
|
PluginInitContext,
|
|
49
48
|
PluginLogger,
|
|
50
49
|
PluginShutdownContext,
|
|
50
|
+
PostModelCallContext,
|
|
51
51
|
PostToolUseContext,
|
|
52
52
|
PreModelCallContext,
|
|
53
53
|
StopContext,
|
package/src/plugin-api/types.ts
CHANGED
|
@@ -55,7 +55,7 @@ export interface PluginLogger {
|
|
|
55
55
|
* - `pre-model-call` — {@link PreModelCallContext}
|
|
56
56
|
* - `post-tool-use` — {@link PostToolUseContext}
|
|
57
57
|
* - `stop` — {@link StopContext}
|
|
58
|
-
* - `
|
|
58
|
+
* - `post-model-call` — {@link PostModelCallContext}
|
|
59
59
|
*/
|
|
60
60
|
export type PluginHookFn<TCtx = unknown> = (ctx: TCtx) => Promise<TCtx | void>;
|
|
61
61
|
|
|
@@ -130,6 +130,20 @@ export interface PluginShutdownContext {
|
|
|
130
130
|
export interface UserPromptSubmitContext {
|
|
131
131
|
/** Conversation ID the user prompt was submitted on. */
|
|
132
132
|
readonly conversationId: string;
|
|
133
|
+
/**
|
|
134
|
+
* Persisted ID of the user message that triggered this turn. Hooks that
|
|
135
|
+
* attach turn-scoped metadata to the originating message (e.g. recording an
|
|
136
|
+
* injected memory block so it survives a conversation reload) key off this
|
|
137
|
+
* row id rather than the in-memory message arrays, whose entries carry no id.
|
|
138
|
+
*/
|
|
139
|
+
readonly userMessageId: string;
|
|
140
|
+
/**
|
|
141
|
+
* Stable ID for the request that drives this turn. Hooks that perform
|
|
142
|
+
* runtime injection forward it onto the injector turn context so the
|
|
143
|
+
* assembled blocks are attributed to the originating request; it is fixed
|
|
144
|
+
* for the turn and cannot be recovered from the message arrays.
|
|
145
|
+
*/
|
|
146
|
+
readonly requestId: string;
|
|
133
147
|
/**
|
|
134
148
|
* The text of the user prompt that triggered this turn — the resolved
|
|
135
149
|
* user message (after slash-command expansion), independent of any
|
|
@@ -321,7 +335,7 @@ export interface PreModelCallContext {
|
|
|
321
335
|
systemPrompt: string | undefined;
|
|
322
336
|
/**
|
|
323
337
|
* Seeded `false`. When a hook sets it `true`, the loop suppresses this turn's
|
|
324
|
-
* live assistant `text_delta` stream;
|
|
338
|
+
* live assistant `text_delta` stream; a `post-model-call` hook is then
|
|
325
339
|
* expected to produce the text the client sees (emitted once, after the reply
|
|
326
340
|
* is finalized). Lets a plugin replace streamed output wholesale — e.g.
|
|
327
341
|
* redaction that needs the full message — instead of leaking the raw stream.
|
|
@@ -331,10 +345,10 @@ export interface PreModelCallContext {
|
|
|
331
345
|
readonly logger: PluginLogger;
|
|
332
346
|
}
|
|
333
347
|
|
|
334
|
-
// ───
|
|
348
|
+
// ─── Post-model-call hook context ────────────────────────────────────────────
|
|
335
349
|
|
|
336
350
|
/**
|
|
337
|
-
* Context passed to the `
|
|
351
|
+
* Context passed to the `post-model-call` hook. Fires for each finalized
|
|
338
352
|
* assistant message — once per model call, at the message-complete boundary —
|
|
339
353
|
* before the message is persisted and (if deferred) streamed-final. Unlike
|
|
340
354
|
* {@link StopContext}'s read-only `responseContent` (which exists for the stop
|
|
@@ -348,7 +362,7 @@ export interface PreModelCallContext {
|
|
|
348
362
|
* place or return a new context; throwing is contained by the loop (the original
|
|
349
363
|
* content is kept).
|
|
350
364
|
*/
|
|
351
|
-
export interface
|
|
365
|
+
export interface PostModelCallContext {
|
|
352
366
|
/** Conversation ID the message belongs to. */
|
|
353
367
|
readonly conversationId: string;
|
|
354
368
|
/** The call site this message serves — `"mainAgent"` for the user-facing reply. */
|
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
* Default compaction implementation.
|
|
3
3
|
*
|
|
4
4
|
* Summarizes conversation history when the context window fills up by
|
|
5
|
-
* delegating to
|
|
6
|
-
* {@link defaultCompact} directly with a {@link CompactionContext}; the
|
|
7
|
-
* carries the conversational inputs while the
|
|
5
|
+
* delegating to the conversation's {@link ContextWindowManager}. The agent loop
|
|
6
|
+
* calls {@link defaultCompact} directly with a {@link CompactionContext}; the
|
|
7
|
+
* request carries the conversational inputs while the compaction module resolves
|
|
8
|
+
* (and, if needed, builds) the manager from its per-conversation store.
|
|
8
9
|
*
|
|
9
10
|
* This module is side-effect free: importing it does not register any plugin.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
|
-
import type {
|
|
13
|
-
ContextWindowManager,
|
|
14
|
-
ContextWindowResult,
|
|
15
|
-
} from "../../../context/window-manager.js";
|
|
16
13
|
import type { Message } from "../../../providers/types.js";
|
|
17
14
|
import type { TrustClass } from "../../../runtime/actor-trust-resolver.js";
|
|
15
|
+
import { PluginExecutionError } from "../../types.js";
|
|
16
|
+
import { getContextWindowManager } from "./manager-store.js";
|
|
17
|
+
import type { ContextWindowResult } from "./window-manager.js";
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Name under which the default compaction plugin registers. Exposed so call
|
|
@@ -24,13 +24,14 @@ export const DEFAULT_COMPACTION_PLUGIN_NAME = "default-compaction";
|
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Self-describing compaction request handed to {@link defaultCompact}. Carries
|
|
27
|
-
* the conversational inputs plus the
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* the conversational inputs plus the `conversationId` the compaction module
|
|
28
|
+
* resolves its manager from, so callers never hold or pass a manager handle and
|
|
29
|
+
* the request can grow toward a model-facing compaction-method shape without
|
|
30
|
+
* coupling callers to the manager's option bag.
|
|
30
31
|
*/
|
|
31
32
|
export interface CompactionContext {
|
|
32
|
-
/**
|
|
33
|
-
|
|
33
|
+
/** Conversation whose manager performs the compaction. */
|
|
34
|
+
conversationId: string;
|
|
34
35
|
/** Message history to consider for compaction. */
|
|
35
36
|
messages: Message[];
|
|
36
37
|
/** Abort signal forwarded to the compaction summary call. */
|
|
@@ -48,12 +49,20 @@ export interface CompactionContext {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
/**
|
|
51
|
-
* Run compaction for the turn:
|
|
52
|
-
* manager
|
|
52
|
+
* Run compaction for the turn: resolves the conversation's context window
|
|
53
|
+
* manager from the compaction store (building it on first access) and returns
|
|
54
|
+
* the (possibly summarized) message history.
|
|
53
55
|
*/
|
|
54
56
|
export async function defaultCompact(
|
|
55
57
|
context: CompactionContext,
|
|
56
58
|
): Promise<ContextWindowResult> {
|
|
57
|
-
const {
|
|
59
|
+
const { conversationId, messages, signal, ...options } = context;
|
|
60
|
+
const manager = getContextWindowManager(conversationId);
|
|
61
|
+
if (manager == null) {
|
|
62
|
+
throw new PluginExecutionError(
|
|
63
|
+
`default-compaction: no ContextWindowManager registered for conversation ${conversationId} — the conversation must register one before compaction runs`,
|
|
64
|
+
DEFAULT_COMPACTION_PLUGIN_NAME,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
58
67
|
return manager.maybeCompact(messages, signal, options);
|
|
59
68
|
}
|
|
@@ -22,10 +22,6 @@ import {
|
|
|
22
22
|
estimatePromptTokens,
|
|
23
23
|
} from "../../../context/token-estimator.js";
|
|
24
24
|
import { truncateToolResultsAcrossHistory } from "../../../context/tool-result-truncation.js";
|
|
25
|
-
import type {
|
|
26
|
-
ContextWindowCompactOptions,
|
|
27
|
-
ContextWindowResult,
|
|
28
|
-
} from "../../../context/window-manager.js";
|
|
29
25
|
import {
|
|
30
26
|
countMediaBlocks,
|
|
31
27
|
estimateUnconditionalStubTokens,
|
|
@@ -33,6 +29,10 @@ import {
|
|
|
33
29
|
} from "../../../daemon/conversation-media-retry.js";
|
|
34
30
|
import type { InjectionMode } from "../../../daemon/conversation-runtime-assembly.js";
|
|
35
31
|
import type { Message } from "../../../providers/types.js";
|
|
32
|
+
import type {
|
|
33
|
+
ContextWindowCompactOptions,
|
|
34
|
+
ContextWindowResult,
|
|
35
|
+
} from "./window-manager.js";
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Identifies which reduction tier was applied in a given step.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import {
|
|
16
16
|
ContextWindowManager,
|
|
17
17
|
type ContextWindowManagerOptions,
|
|
18
|
-
} from "
|
|
18
|
+
} from "./window-manager.js";
|
|
19
19
|
|
|
20
20
|
/** Live managers keyed by conversation id. */
|
|
21
21
|
const managersByConversation = new Map<string, ContextWindowManager>();
|
|
@@ -14,23 +14,25 @@
|
|
|
14
14
|
* marker is wrapped around the assistant-role memory message we emit on
|
|
15
15
|
* successful compaction so those code paths keep working unchanged.
|
|
16
16
|
*/
|
|
17
|
-
import { getConfig } from "
|
|
18
|
-
import type { CompactionConfig } from "
|
|
19
|
-
import type { LLMCallSite } from "
|
|
20
|
-
import type { ContextWindowConfig } from "
|
|
17
|
+
import { getConfig } from "../../../config/loader.js";
|
|
18
|
+
import type { CompactionConfig } from "../../../config/schemas/compaction.js";
|
|
19
|
+
import type { LLMCallSite } from "../../../config/schemas/llm.js";
|
|
20
|
+
import type { ContextWindowConfig } from "../../../config/types.js";
|
|
21
|
+
import {
|
|
22
|
+
type CompactionRunArgs,
|
|
23
|
+
type CompactionRunResult,
|
|
24
|
+
runAssistantDrivenCompaction,
|
|
25
|
+
runEmergencyCompaction,
|
|
26
|
+
} from "../../../context/compactor.js";
|
|
27
|
+
import { estimatePromptTokens } from "../../../context/token-estimator.js";
|
|
21
28
|
import type {
|
|
22
29
|
ContentBlock,
|
|
23
30
|
Message,
|
|
24
31
|
Provider,
|
|
25
32
|
ToolDefinition,
|
|
26
|
-
} from "
|
|
27
|
-
import type { TrustClass } from "
|
|
28
|
-
import { getLogger } from "
|
|
29
|
-
import {
|
|
30
|
-
type CompactionRunArgs,
|
|
31
|
-
runAssistantDrivenCompaction,
|
|
32
|
-
} from "./compactor.js";
|
|
33
|
-
import { estimatePromptTokens } from "./token-estimator.js";
|
|
33
|
+
} from "../../../providers/types.js";
|
|
34
|
+
import type { TrustClass } from "../../../runtime/actor-trust-resolver.js";
|
|
35
|
+
import { getLogger } from "../../../util/logger.js";
|
|
34
36
|
|
|
35
37
|
const log = getLogger("context-window");
|
|
36
38
|
|
|
@@ -118,6 +120,21 @@ export interface ContextWindowCompactOptions {
|
|
|
118
120
|
actorTrustClass?: TrustClass;
|
|
119
121
|
}
|
|
120
122
|
|
|
123
|
+
export interface EmergencyCompactOptions {
|
|
124
|
+
/**
|
|
125
|
+
* The provider-reported estimate at the overflow that triggered recovery.
|
|
126
|
+
* Sizing the emergency summary needs the actual rejected token count, not
|
|
127
|
+
* the manager's pre-send estimate (which under-counted, hence the
|
|
128
|
+
* rejection).
|
|
129
|
+
*/
|
|
130
|
+
previousEstimatedInputTokens: number;
|
|
131
|
+
/**
|
|
132
|
+
* Per-conversation inference-profile override forwarded to the summary
|
|
133
|
+
* call.
|
|
134
|
+
*/
|
|
135
|
+
overrideProfile?: string | null;
|
|
136
|
+
}
|
|
137
|
+
|
|
121
138
|
export interface ContextWindowManagerOptions {
|
|
122
139
|
provider: Provider;
|
|
123
140
|
systemPrompt: string | (() => string);
|
|
@@ -428,6 +445,45 @@ export class ContextWindowManager {
|
|
|
428
445
|
return { ...result, estimatedInputTokens, exhausted: true };
|
|
429
446
|
}
|
|
430
447
|
|
|
448
|
+
/**
|
|
449
|
+
* Emergency mid-turn compaction — summarize everything before the last
|
|
450
|
+
* tool_use/tool_result pair and let the agent continue with
|
|
451
|
+
* `[summary, last_tool_call, last_tool_result]`. Used as a recovery rung
|
|
452
|
+
* after the provider rejects a turn for context overflow. The manager
|
|
453
|
+
* supplies the provider, system prompt, token budget, conversation id, and
|
|
454
|
+
* non-persisted prefix count it already owns; the caller provides only the
|
|
455
|
+
* overflow-specific inputs.
|
|
456
|
+
*/
|
|
457
|
+
async emergencyCompact(
|
|
458
|
+
messages: Message[],
|
|
459
|
+
options: EmergencyCompactOptions,
|
|
460
|
+
signal?: AbortSignal,
|
|
461
|
+
): Promise<CompactionRunResult> {
|
|
462
|
+
if (this.conversationId == null) {
|
|
463
|
+
throw new Error(
|
|
464
|
+
"ContextWindowManager has no conversationId — cannot run emergency compaction",
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
try {
|
|
468
|
+
return await runEmergencyCompaction({
|
|
469
|
+
conversationId: this.conversationId,
|
|
470
|
+
messages,
|
|
471
|
+
provider: this.provider,
|
|
472
|
+
systemPrompt: this.systemPrompt,
|
|
473
|
+
tools: undefined,
|
|
474
|
+
compaction: this.resolveCompactionConfig(),
|
|
475
|
+
maxInputTokens: this.config.maxInputTokens,
|
|
476
|
+
previousEstimatedInputTokens: options.previousEstimatedInputTokens,
|
|
477
|
+
force: true,
|
|
478
|
+
signal,
|
|
479
|
+
overrideProfile: options.overrideProfile ?? null,
|
|
480
|
+
nonPersistedPrefixCount: this._nonPersistedPrefixCount,
|
|
481
|
+
});
|
|
482
|
+
} finally {
|
|
483
|
+
this.clearSystemPromptCache();
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
431
487
|
/**
|
|
432
488
|
* Recompute the prompt-token estimate against the post-compaction
|
|
433
489
|
* message array. The compactor returns a conservative placeholder; the
|
|
@@ -37,8 +37,7 @@ import { getConfig } from "../../../../config/loader.js";
|
|
|
37
37
|
import { findConversationOrSubagent } from "../../../../daemon/conversation-registry.js";
|
|
38
38
|
import {
|
|
39
39
|
applyRuntimeInjections,
|
|
40
|
-
|
|
41
|
-
type InjectionMode,
|
|
40
|
+
resolveTurnInboundActorContext,
|
|
42
41
|
type RuntimeInjectionResult,
|
|
43
42
|
} from "../../../../daemon/conversation-runtime-assembly.js";
|
|
44
43
|
import type { ServerMessage } from "../../../../daemon/message-protocol.js";
|
|
@@ -78,12 +77,6 @@ export interface MemoryRetrievalHookContext {
|
|
|
78
77
|
* context. The one turn-identity field the live conversation can't recover.
|
|
79
78
|
*/
|
|
80
79
|
readonly requestId: string | undefined;
|
|
81
|
-
/**
|
|
82
|
-
* Injection volume for the assembled blocks. `"full"` on the first call;
|
|
83
|
-
* overflow reduction downgrades subsequent re-injection, but the first-call
|
|
84
|
-
* assembly this hook performs runs at the loop's committed mode.
|
|
85
|
-
*/
|
|
86
|
-
readonly mode: InjectionMode;
|
|
87
80
|
/**
|
|
88
81
|
* Whether the in-flight turn has no human present to answer clarification
|
|
89
82
|
* questions. Resolved once at turn start and threaded in so injection uses
|
|
@@ -96,12 +89,6 @@ export interface MemoryRetrievalHookContext {
|
|
|
96
89
|
* at turn start, so it is threaded in rather than re-derived.
|
|
97
90
|
*/
|
|
98
91
|
readonly modelProfile: string | null;
|
|
99
|
-
/**
|
|
100
|
-
* Inbound actor identity and trust fields for the unified `<turn_context>`
|
|
101
|
-
* block, or `null` on guardian turns (which suppress the actor section).
|
|
102
|
-
* Resolved once at turn start from the actor-trust resolver and threaded in.
|
|
103
|
-
*/
|
|
104
|
-
readonly actorContext: InboundActorContext | null;
|
|
105
92
|
}
|
|
106
93
|
|
|
107
94
|
/**
|
|
@@ -266,6 +253,10 @@ const userPromptSubmitMemoryRetrieval: PluginHookFn<
|
|
|
266
253
|
const abortSignal = conversation?.abortController?.signal;
|
|
267
254
|
const isTrustedActor =
|
|
268
255
|
resolveTrustClass(conversation?.trustContext) === "guardian";
|
|
256
|
+
const actorContext = resolveTurnInboundActorContext(
|
|
257
|
+
conversation?.trustContext,
|
|
258
|
+
conversation?.assistantId,
|
|
259
|
+
);
|
|
269
260
|
|
|
270
261
|
if (conversation && isTrustedActor && abortSignal) {
|
|
271
262
|
const graphResult = await conversation.graphMemory.prepareMemory(
|
|
@@ -309,13 +300,16 @@ const userPromptSubmitMemoryRetrieval: PluginHookFn<
|
|
|
309
300
|
// input — the Slack chronological transcript, the unified `<turn_context>`
|
|
310
301
|
// block, channel/voice/transport hints, and the turn's trust/index/call-site
|
|
311
302
|
// — from the live conversation, so we hand in only the request id and
|
|
312
|
-
// conversation id plus the fields resolved once at turn start
|
|
313
|
-
// `isNonInteractive`, `modelProfile
|
|
303
|
+
// conversation id plus the fields resolved once at turn start
|
|
304
|
+
// (`isNonInteractive`, `modelProfile`). The unified `<turn_context>` actor
|
|
305
|
+
// input is self-resolved from the live conversation's trust context. This
|
|
306
|
+
// first-call assembly always runs at `"full"` volume; overflow reduction only
|
|
307
|
+
// downgrades the mode on later re-injection.
|
|
314
308
|
const injection = await applyRuntimeInjections(ctx.latestMessages, {
|
|
315
309
|
isNonInteractive: ctx.isNonInteractive,
|
|
316
310
|
modelProfile: ctx.modelProfile,
|
|
317
|
-
actorContext
|
|
318
|
-
mode:
|
|
311
|
+
actorContext,
|
|
312
|
+
mode: "full",
|
|
319
313
|
requestId: ctx.requestId,
|
|
320
314
|
conversationId: ctx.conversationId,
|
|
321
315
|
});
|
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
capabilityOrDiskBody,
|
|
5
5
|
type CapabilityResolvers,
|
|
6
|
-
injectCapabilitiesLeaf,
|
|
7
6
|
isCapabilitySlug,
|
|
8
7
|
renderCapabilityContent,
|
|
9
8
|
} from "../capabilities.js";
|
|
10
|
-
import { buildNeedleIndex } from "../needle.js";
|
|
11
|
-
import type { LeafPath, LeafTree, Slug } from "../types.js";
|
|
12
|
-
|
|
13
|
-
function emptyTree(): LeafTree {
|
|
14
|
-
return { leaves: new Map(), byPage: new Map<Slug, LeafPath[]>() };
|
|
15
|
-
}
|
|
16
9
|
|
|
17
10
|
describe("isCapabilitySlug", () => {
|
|
18
11
|
test("true for skill and CLI-command slugs, false otherwise", () => {
|
|
@@ -23,52 +16,6 @@ describe("isCapabilitySlug", () => {
|
|
|
23
16
|
});
|
|
24
17
|
});
|
|
25
18
|
|
|
26
|
-
describe("injectCapabilitiesLeaf", () => {
|
|
27
|
-
test("registers an always-on leaf with the synthetic slugs as members", () => {
|
|
28
|
-
const tree = emptyTree();
|
|
29
|
-
const core = new Set<LeafPath>();
|
|
30
|
-
injectCapabilitiesLeaf(tree, core, ["skills/foo", "cli-commands/bar"]);
|
|
31
|
-
|
|
32
|
-
const leaf = tree.leaves.get(CAPABILITIES_LEAF_PATH);
|
|
33
|
-
expect(leaf?.frontmatter.in_core).toBe(true);
|
|
34
|
-
expect(leaf?.members).toEqual(["skills/foo", "cli-commands/bar"]);
|
|
35
|
-
expect(core.has(CAPABILITIES_LEAF_PATH)).toBe(true);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("unions the leaf into each member's byPage entry without dropping existing leaves", () => {
|
|
39
|
-
const tree = emptyTree();
|
|
40
|
-
tree.byPage.set("skills/foo", ["other/leaf"]);
|
|
41
|
-
injectCapabilitiesLeaf(tree, new Set<LeafPath>(), [
|
|
42
|
-
"skills/foo",
|
|
43
|
-
"cli-commands/bar",
|
|
44
|
-
]);
|
|
45
|
-
|
|
46
|
-
expect(tree.byPage.get("skills/foo")).toEqual([
|
|
47
|
-
"other/leaf",
|
|
48
|
-
CAPABILITIES_LEAF_PATH,
|
|
49
|
-
]);
|
|
50
|
-
expect(tree.byPage.get("cli-commands/bar")).toEqual([
|
|
51
|
-
CAPABILITIES_LEAF_PATH,
|
|
52
|
-
]);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("is idempotent — re-injecting does not duplicate the byPage entry", () => {
|
|
56
|
-
const tree = emptyTree();
|
|
57
|
-
const core = new Set<LeafPath>();
|
|
58
|
-
injectCapabilitiesLeaf(tree, core, ["skills/foo"]);
|
|
59
|
-
injectCapabilitiesLeaf(tree, core, ["skills/foo"]);
|
|
60
|
-
expect(tree.byPage.get("skills/foo")).toEqual([CAPABILITIES_LEAF_PATH]);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("stays always-on even with no installed skills/commands", () => {
|
|
64
|
-
const tree = emptyTree();
|
|
65
|
-
const core = new Set<LeafPath>();
|
|
66
|
-
injectCapabilitiesLeaf(tree, core, []);
|
|
67
|
-
expect(tree.leaves.get(CAPABILITIES_LEAF_PATH)?.members).toEqual([]);
|
|
68
|
-
expect(core.has(CAPABILITIES_LEAF_PATH)).toBe(true);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
19
|
describe("renderCapabilityContent", () => {
|
|
73
20
|
const resolvers: CapabilityResolvers = {
|
|
74
21
|
skill: (slug) =>
|
|
@@ -100,19 +47,25 @@ describe("renderCapabilityContent", () => {
|
|
|
100
47
|
});
|
|
101
48
|
});
|
|
102
49
|
|
|
103
|
-
describe("
|
|
104
|
-
test("a
|
|
105
|
-
|
|
106
|
-
|
|
50
|
+
describe("capabilityOrDiskBody", () => {
|
|
51
|
+
test("a capability slug routes through renderCapabilityContent and never reads disk", async () => {
|
|
52
|
+
let diskReads = 0;
|
|
53
|
+
const readDiskBody = async (): Promise<string> => {
|
|
54
|
+
diskReads += 1;
|
|
55
|
+
return "disk body — should not be read";
|
|
56
|
+
};
|
|
57
|
+
// A capability slug whose production cache has no entry degrades to "" (the
|
|
58
|
+
// `renderCapabilityContent` contract) rather than falling through to the
|
|
59
|
+
// injected disk reader, which must stay untouched.
|
|
60
|
+
expect(await capabilityOrDiskBody("skills/example", readDiskBody)).toBe("");
|
|
61
|
+
expect(diskReads).toBe(0);
|
|
62
|
+
});
|
|
107
63
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
64
|
+
test("a non-capability slug returns the injected disk reader's result", async () => {
|
|
65
|
+
const readDiskBody = async (slug: string): Promise<string> =>
|
|
66
|
+
`disk body for ${slug}`;
|
|
67
|
+
expect(await capabilityOrDiskBody("relationship/vows", readDiskBody)).toBe(
|
|
68
|
+
"disk body for relationship/vows",
|
|
111
69
|
);
|
|
112
|
-
|
|
113
|
-
// By slug-derived title token.
|
|
114
|
-
expect(needle.query("meet", 5)).toContain("skills/meet-join");
|
|
115
|
-
// By summary token.
|
|
116
|
-
expect(needle.query("transcribe", 5)).toContain("skills/meet-join");
|
|
117
70
|
});
|
|
118
71
|
});
|