@vellumai/assistant 0.8.9-staging.2 → 0.8.9-staging.3
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/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__/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 +1 -1
- 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-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__/cu-unified-flow.test.ts +36 -0
- package/src/__tests__/history-repair-hook.test.ts +2 -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__/post-turn-tool-result-truncation.test.ts +69 -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/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/feature-flag-registry.json +3 -18
- 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 +78 -78
- 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/persist-unsendable-image.ts +62 -25
- package/src/daemon/process-message.ts +1 -1
- 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/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 +12 -12
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +12 -18
- package/src/prompts/system-prompt.ts +61 -10
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +37 -2
- package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
- package/src/providers/openai/chat-completions-provider.ts +25 -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/conversation-routes.ts +2 -0
- 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/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/ui-surface/definitions.ts +7 -0
- package/src/acp/feature-gate.test.ts +0 -48
- package/src/acp/feature-gate.ts +0 -34
|
@@ -2,6 +2,12 @@ import { and, asc, eq, gt, or } from "drizzle-orm";
|
|
|
2
2
|
import { v4 as uuid } from "uuid";
|
|
3
3
|
|
|
4
4
|
import { getConfig } from "../config/loader.js";
|
|
5
|
+
import {
|
|
6
|
+
ACTIVATION_AB_VARIANT,
|
|
7
|
+
ACTIVATION_FUNNEL_VERSION,
|
|
8
|
+
activationStepIndex,
|
|
9
|
+
type ActivationStepName,
|
|
10
|
+
} from "../telemetry/activation-funnel.js";
|
|
5
11
|
import { getDb } from "./db-connection.js";
|
|
6
12
|
import { onboardingEvents } from "./schema.js";
|
|
7
13
|
|
|
@@ -16,6 +22,11 @@ export interface OnboardingEvent {
|
|
|
16
22
|
googleScopesJson: string | null;
|
|
17
23
|
priorAssistantsJson: string | null;
|
|
18
24
|
abVariant: string | null;
|
|
25
|
+
sessionId: string | null;
|
|
26
|
+
stepName: string | null;
|
|
27
|
+
stepIndex: number | null;
|
|
28
|
+
completedAt: string | null;
|
|
29
|
+
funnelVersion: string | null;
|
|
19
30
|
}
|
|
20
31
|
|
|
21
32
|
export interface RecordOnboardingEventParams {
|
|
@@ -27,6 +38,17 @@ export interface RecordOnboardingEventParams {
|
|
|
27
38
|
googleScopes?: string[];
|
|
28
39
|
priorAssistants?: string[];
|
|
29
40
|
abVariant?: string;
|
|
41
|
+
sessionId?: string | null;
|
|
42
|
+
stepName?: string | null;
|
|
43
|
+
stepIndex?: number | null;
|
|
44
|
+
completedAt?: string | null;
|
|
45
|
+
funnelVersion?: string | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Insert a fully-built event row. Shared by all record* entry points. */
|
|
49
|
+
function insertOnboardingEvent(event: OnboardingEvent): OnboardingEvent {
|
|
50
|
+
getDb().insert(onboardingEvents).values(event).run();
|
|
51
|
+
return event;
|
|
30
52
|
}
|
|
31
53
|
|
|
32
54
|
/**
|
|
@@ -37,8 +59,7 @@ export function recordOnboardingEvent(
|
|
|
37
59
|
params: RecordOnboardingEventParams,
|
|
38
60
|
): OnboardingEvent | null {
|
|
39
61
|
if (!getConfig().collectUsageData) return null;
|
|
40
|
-
|
|
41
|
-
const event: OnboardingEvent = {
|
|
62
|
+
return insertOnboardingEvent({
|
|
42
63
|
id: uuid(),
|
|
43
64
|
createdAt: Date.now(),
|
|
44
65
|
screen: params.screen,
|
|
@@ -53,22 +74,44 @@ export function recordOnboardingEvent(
|
|
|
53
74
|
? JSON.stringify(params.priorAssistants)
|
|
54
75
|
: null,
|
|
55
76
|
abVariant: params.abVariant ?? null,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
sessionId: params.sessionId ?? null,
|
|
78
|
+
stepName: params.stepName ?? null,
|
|
79
|
+
stepIndex: params.stepIndex ?? null,
|
|
80
|
+
completedAt: params.completedAt ?? null,
|
|
81
|
+
funnelVersion: params.funnelVersion ?? null,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Record an activation-funnel milestone event. Reuses the onboarding telemetry
|
|
87
|
+
* substrate (`screen` carries the step name to satisfy the NOT NULL column).
|
|
88
|
+
* Returns null when usage data collection is disabled.
|
|
89
|
+
*/
|
|
90
|
+
export function recordActivationEvent(params: {
|
|
91
|
+
stepName: ActivationStepName;
|
|
92
|
+
sessionId: string;
|
|
93
|
+
userId?: string | null;
|
|
94
|
+
abVariant?: string;
|
|
95
|
+
}): OnboardingEvent | null {
|
|
96
|
+
if (!getConfig().collectUsageData) return null;
|
|
97
|
+
const createdAt = Date.now();
|
|
98
|
+
return insertOnboardingEvent({
|
|
99
|
+
id: uuid(),
|
|
100
|
+
createdAt,
|
|
101
|
+
screen: params.stepName,
|
|
102
|
+
toolsJson: null,
|
|
103
|
+
tasksJson: null,
|
|
104
|
+
tone: null,
|
|
105
|
+
googleConnected: null,
|
|
106
|
+
googleScopesJson: null,
|
|
107
|
+
priorAssistantsJson: null,
|
|
108
|
+
abVariant: params.abVariant ?? ACTIVATION_AB_VARIANT,
|
|
109
|
+
sessionId: params.sessionId,
|
|
110
|
+
stepName: params.stepName,
|
|
111
|
+
stepIndex: activationStepIndex(params.stepName),
|
|
112
|
+
completedAt: new Date(createdAt).toISOString(),
|
|
113
|
+
funnelVersion: ACTIVATION_FUNNEL_VERSION,
|
|
114
|
+
});
|
|
72
115
|
}
|
|
73
116
|
|
|
74
117
|
/**
|
|
@@ -93,6 +136,11 @@ export function queryUnreportedOnboardingEvents(
|
|
|
93
136
|
googleScopesJson: onboardingEvents.googleScopesJson,
|
|
94
137
|
priorAssistantsJson: onboardingEvents.priorAssistantsJson,
|
|
95
138
|
abVariant: onboardingEvents.abVariant,
|
|
139
|
+
sessionId: onboardingEvents.sessionId,
|
|
140
|
+
stepName: onboardingEvents.stepName,
|
|
141
|
+
stepIndex: onboardingEvents.stepIndex,
|
|
142
|
+
completedAt: onboardingEvents.completedAt,
|
|
143
|
+
funnelVersion: onboardingEvents.funnelVersion,
|
|
96
144
|
})
|
|
97
145
|
.from(onboardingEvents)
|
|
98
146
|
.where(
|
|
@@ -277,6 +277,11 @@ export const onboardingEvents = sqliteTable("onboarding_events", {
|
|
|
277
277
|
googleScopesJson: text("google_scopes_json"),
|
|
278
278
|
priorAssistantsJson: text("prior_assistants_json"),
|
|
279
279
|
abVariant: text("ab_variant"),
|
|
280
|
+
sessionId: text("session_id"),
|
|
281
|
+
stepName: text("step_name"),
|
|
282
|
+
stepIndex: integer("step_index"),
|
|
283
|
+
completedAt: text("completed_at"),
|
|
284
|
+
funnelVersion: text("funnel_version"),
|
|
280
285
|
});
|
|
281
286
|
|
|
282
287
|
// Aggregated legacy-loopback auth-fallback counts forwarded by the gateway.
|
|
@@ -294,6 +299,14 @@ export const authFallbackEvents = sqliteTable("auth_fallback_events", {
|
|
|
294
299
|
windowEnd: integer("window_end").notNull(),
|
|
295
300
|
});
|
|
296
301
|
|
|
302
|
+
// One row per conversation started on the activation-rail bootstrap template.
|
|
303
|
+
// Lets the activation funnel telemetry scope its events to activation
|
|
304
|
+
// conversations without inspecting the bootstrap template at emit time.
|
|
305
|
+
export const activationSessions = sqliteTable("activation_sessions", {
|
|
306
|
+
conversationId: text("conversation_id").primaryKey(),
|
|
307
|
+
createdAt: integer("created_at").notNull(),
|
|
308
|
+
});
|
|
309
|
+
|
|
297
310
|
export const traceEvents = sqliteTable(
|
|
298
311
|
"trace_events",
|
|
299
312
|
{
|
|
@@ -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,23 @@
|
|
|
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
|
+
runAssistantDrivenCompaction,
|
|
24
|
+
} from "../../../context/compactor.js";
|
|
25
|
+
import { estimatePromptTokens } from "../../../context/token-estimator.js";
|
|
21
26
|
import type {
|
|
22
27
|
ContentBlock,
|
|
23
28
|
Message,
|
|
24
29
|
Provider,
|
|
25
30
|
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";
|
|
31
|
+
} from "../../../providers/types.js";
|
|
32
|
+
import type { TrustClass } from "../../../runtime/actor-trust-resolver.js";
|
|
33
|
+
import { getLogger } from "../../../util/logger.js";
|
|
34
34
|
|
|
35
35
|
const log = getLogger("context-window");
|
|
36
36
|
|
|
@@ -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
|
});
|
|
@@ -11,6 +11,8 @@ import { join } from "node:path";
|
|
|
11
11
|
import { getIsContainerized } from "../config/env-registry.js";
|
|
12
12
|
import type { ChannelCapabilities } from "../daemon/conversation-runtime-assembly.js";
|
|
13
13
|
import type { TrustContext } from "../daemon/trust-context.js";
|
|
14
|
+
import { markActivationSession } from "../memory/activation-session-store.js";
|
|
15
|
+
import { ACTIVATION_RAIL_BOOTSTRAP_TEMPLATE } from "../telemetry/activation-funnel.js";
|
|
14
16
|
import type { OnboardingContext } from "../types/onboarding-context.js";
|
|
15
17
|
import { resolveBundledDir } from "../util/bundled-asset.js";
|
|
16
18
|
import { getLogger } from "../util/logger.js";
|
|
@@ -206,7 +208,7 @@ export function ensurePromptFiles(): void {
|
|
|
206
208
|
* overwrite the workspace file with the specified variant. No-op when
|
|
207
209
|
* BOOTSTRAP.md has been deleted, modified, or the template file is missing.
|
|
208
210
|
*/
|
|
209
|
-
export function maybeReseedBootstrap(templateFileName: string):
|
|
211
|
+
export function maybeReseedBootstrap(templateFileName: string): boolean {
|
|
210
212
|
// Path traversal guard: reject filenames containing directory separators or
|
|
211
213
|
// parent-directory references, and require a `.md` extension.
|
|
212
214
|
if (
|
|
@@ -218,11 +220,11 @@ export function maybeReseedBootstrap(templateFileName: string): void {
|
|
|
218
220
|
{ templateFileName },
|
|
219
221
|
"Rejected bootstrap template filename: invalid characters or extension",
|
|
220
222
|
);
|
|
221
|
-
return;
|
|
223
|
+
return false;
|
|
222
224
|
}
|
|
223
225
|
|
|
224
226
|
const bootstrapPath = getWorkspacePromptPath("BOOTSTRAP.md");
|
|
225
|
-
if (!existsSync(bootstrapPath)) return;
|
|
227
|
+
if (!existsSync(bootstrapPath)) return false;
|
|
226
228
|
|
|
227
229
|
const currentContent = readPromptFile(bootstrapPath);
|
|
228
230
|
// Compare against the GENERIC "BOOTSTRAP.md" template, not the specified
|
|
@@ -230,7 +232,7 @@ export function maybeReseedBootstrap(templateFileName: string): void {
|
|
|
230
232
|
// template, so this guard returns false on subsequent calls — making the
|
|
231
233
|
// swap idempotent. Do NOT change the comparison target to the provided
|
|
232
234
|
// template filename; that would re-swap on every prompt build.
|
|
233
|
-
if (!isTemplateContent(currentContent, "BOOTSTRAP.md")) return;
|
|
235
|
+
if (!isTemplateContent(currentContent, "BOOTSTRAP.md")) return false;
|
|
234
236
|
|
|
235
237
|
const templatesDir = resolveBundledDir(
|
|
236
238
|
import.meta.dirname ?? __dirname,
|
|
@@ -243,7 +245,7 @@ export function maybeReseedBootstrap(templateFileName: string): void {
|
|
|
243
245
|
{ templateFileName },
|
|
244
246
|
"Bootstrap template not found, keeping generic BOOTSTRAP.md",
|
|
245
247
|
);
|
|
246
|
-
return;
|
|
248
|
+
return false;
|
|
247
249
|
}
|
|
248
250
|
|
|
249
251
|
try {
|
|
@@ -253,11 +255,51 @@ export function maybeReseedBootstrap(templateFileName: string): void {
|
|
|
253
255
|
{ templateFileName },
|
|
254
256
|
"Replaced generic BOOTSTRAP.md with specified template",
|
|
255
257
|
);
|
|
258
|
+
return true;
|
|
256
259
|
} catch (err) {
|
|
257
260
|
log.warn(
|
|
258
261
|
{ err, templateFileName },
|
|
259
262
|
"Failed to reseed BOOTSTRAP.md with template",
|
|
260
263
|
);
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Reseed BOOTSTRAP.md from the requested onboarding template (one-shot,
|
|
270
|
+
* idempotent) and, when that template is the activation rail AND the rail is
|
|
271
|
+
* now the active bootstrap, mark the conversation as an activation session.
|
|
272
|
+
*
|
|
273
|
+
* Marking happens here — at the single point where the bootstrap selection is
|
|
274
|
+
* known — so it lands BEFORE the agent loop resolves tools and BEFORE the model
|
|
275
|
+
* can call the emit tool on the first activation-rail turn. (`resolveTools`
|
|
276
|
+
* runs before `resolveSystemPrompt` in the loop, so the system-prompt build is
|
|
277
|
+
* too late to be the *only* marking site.) The marker write is best-effort and
|
|
278
|
+
* idempotent (`markActivationSession` swallows errors and dedups on the PK), so
|
|
279
|
+
* calling this from both `setOnboardingContext` and `buildSystemPrompt` is safe.
|
|
280
|
+
*
|
|
281
|
+
* The activation mark is gated on the rail actually being the active bootstrap —
|
|
282
|
+
* either this call just installed it, OR BOOTSTRAP.md already holds the
|
|
283
|
+
* activation-rail template. When the reseed no-ops because BOOTSTRAP.md is
|
|
284
|
+
* missing or customized to something else, the rail is NOT active, so we must
|
|
285
|
+
* NOT mark (otherwise non-rail conversations would pollute activation
|
|
286
|
+
* telemetry).
|
|
287
|
+
*/
|
|
288
|
+
export function applyBootstrapTemplate(
|
|
289
|
+
bootstrapTemplate: string,
|
|
290
|
+
conversationId?: string,
|
|
291
|
+
): void {
|
|
292
|
+
const installedActivationRail = maybeReseedBootstrap(bootstrapTemplate);
|
|
293
|
+
if (
|
|
294
|
+
bootstrapTemplate === ACTIVATION_RAIL_BOOTSTRAP_TEMPLATE &&
|
|
295
|
+
conversationId &&
|
|
296
|
+
(installedActivationRail ||
|
|
297
|
+
isTemplateContent(
|
|
298
|
+
readPromptFile(getWorkspacePromptPath("BOOTSTRAP.md")),
|
|
299
|
+
ACTIVATION_RAIL_BOOTSTRAP_TEMPLATE,
|
|
300
|
+
))
|
|
301
|
+
) {
|
|
302
|
+
markActivationSession(conversationId);
|
|
261
303
|
}
|
|
262
304
|
}
|
|
263
305
|
|
|
@@ -268,6 +310,13 @@ export interface BuildSystemPromptOptions {
|
|
|
268
310
|
trustContext?: TrustContext;
|
|
269
311
|
channelCapabilities?: ChannelCapabilities;
|
|
270
312
|
onboardingContext?: OnboardingContext;
|
|
313
|
+
/**
|
|
314
|
+
* Conversation this prompt is being built for. Optional because several
|
|
315
|
+
* callers build a prompt outside a conversation (e.g. home greeting,
|
|
316
|
+
* suggested prompts). When present and the activation-rail bootstrap template
|
|
317
|
+
* is selected, the conversation is marked as an activation session.
|
|
318
|
+
*/
|
|
319
|
+
conversationId?: string;
|
|
271
320
|
}
|
|
272
321
|
|
|
273
322
|
/**
|
|
@@ -278,11 +327,13 @@ export interface BuildSystemPromptOptions {
|
|
|
278
327
|
* file-backed bodies, and runtime-computed transforms.
|
|
279
328
|
*/
|
|
280
329
|
export function buildSystemPrompt(options?: BuildSystemPromptOptions): string {
|
|
281
|
-
// One-shot bootstrap
|
|
282
|
-
//
|
|
283
|
-
// the
|
|
284
|
-
|
|
285
|
-
|
|
330
|
+
// One-shot bootstrap reseed + activation-rail marking. The marking also runs
|
|
331
|
+
// earlier, at `setOnboardingContext`, so the activation session is recorded
|
|
332
|
+
// before the agent loop resolves tools on the first turn; this call is a
|
|
333
|
+
// harmless idempotent backstop for prompt builds outside that path.
|
|
334
|
+
const bootstrapTemplate = options?.onboardingContext?.bootstrapTemplate;
|
|
335
|
+
if (bootstrapTemplate) {
|
|
336
|
+
applyBootstrapTemplate(bootstrapTemplate, options?.conversationId);
|
|
286
337
|
}
|
|
287
338
|
|
|
288
339
|
// Slugs used by the persona sections (`10-user-persona`,
|