@vellumai/assistant 0.4.18 → 0.4.20
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/runbook-trusted-contacts.md +5 -3
- package/package.json +1 -1
- package/src/__tests__/channel-approvals.test.ts +7 -1
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
- package/src/__tests__/daemon-server-session-init.test.ts +2 -0
- package/src/__tests__/gmail-integration.test.ts +13 -4
- package/src/__tests__/handle-user-message-secret-resume.test.ts +7 -1
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +2 -0
- package/src/__tests__/ingress-reconcile.test.ts +13 -5
- package/src/__tests__/mcp-cli.test.ts +1 -1
- package/src/__tests__/recording-intent-handler.test.ts +9 -1
- package/src/__tests__/send-endpoint-busy.test.ts +8 -2
- package/src/__tests__/sms-messaging-provider.test.ts +4 -0
- package/src/__tests__/system-prompt.test.ts +18 -2
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
- package/src/agent/loop.ts +324 -163
- package/src/cli/mcp.ts +81 -28
- package/src/config/bundled-skills/app-builder/SKILL.md +7 -5
- package/src/config/bundled-skills/app-builder/TOOLS.json +2 -2
- package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +6 -11
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -2
- package/src/config/bundled-skills/sms-setup/SKILL.md +8 -16
- package/src/config/bundled-skills/telegram-setup/SKILL.md +3 -3
- package/src/config/bundled-skills/trusted-contacts/SKILL.md +13 -25
- package/src/config/bundled-skills/twilio-setup/SKILL.md +13 -23
- package/src/config/system-prompt.ts +574 -518
- package/src/daemon/session-surfaces.ts +28 -0
- package/src/daemon/session.ts +255 -191
- package/src/daemon/tool-side-effects.ts +3 -13
- package/src/mcp/client.ts +2 -7
- package/src/security/secure-keys.ts +43 -3
- package/src/tools/apps/definitions.ts +5 -0
- package/src/tools/apps/executors.ts +18 -22
- package/src/tools/terminal/safe-env.ts +7 -0
- package/src/__tests__/response-tier.test.ts +0 -195
- package/src/daemon/response-tier.ts +0 -250
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Per-turn response tier classification.
|
|
3
|
-
*
|
|
4
|
-
* Classifies each user message into a tier that controls:
|
|
5
|
-
* - maxTokens budget for the LLM response
|
|
6
|
-
* - Which system prompt sections are included
|
|
7
|
-
*
|
|
8
|
-
* Two layers:
|
|
9
|
-
* 1. Deterministic regex/heuristic (zero latency, runs every turn)
|
|
10
|
-
* 2. Background Haiku classification (fire-and-forget, advises future turns)
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { createTimeout, extractText, getConfiguredProvider, userMessage } from '../providers/provider-send-message.js';
|
|
14
|
-
import { getLogger } from '../util/logger.js';
|
|
15
|
-
|
|
16
|
-
const log = getLogger('response-tier');
|
|
17
|
-
|
|
18
|
-
export type ResponseTier = 'low' | 'medium' | 'high';
|
|
19
|
-
|
|
20
|
-
export type TierConfidence = 'high' | 'low';
|
|
21
|
-
|
|
22
|
-
export interface TierClassification {
|
|
23
|
-
tier: ResponseTier;
|
|
24
|
-
reason: string;
|
|
25
|
-
confidence: TierConfidence;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface SessionTierHint {
|
|
29
|
-
tier: ResponseTier;
|
|
30
|
-
turn: number;
|
|
31
|
-
timestamp: number;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// ── Patterns ──────────────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
const GREETING_PATTERNS = /^(hey|hi|hello|yo|sup|hiya|howdy|what'?s up|thanks|thank you|thx|ty|cheers|what can you|who are you|how are you)\b/i;
|
|
37
|
-
|
|
38
|
-
const BUILD_KEYWORDS = /\b(build|implement|create|refactor|debug|deploy|migrate|scaffold|architect|redesign|generate|write|develop|fix|convert|add|remove|update|modify|change|delete|replace|integrate|setup|install|configure|optimize|rewrite)\b/i;
|
|
39
|
-
|
|
40
|
-
const SURFACE_ACTION = /^\[User action on \w+ surface:/;
|
|
41
|
-
const CODE_FENCE = /```/;
|
|
42
|
-
const FILE_PATH = /(?:^|[\s"'(])(?:\/|~\/|\.\/)\S/;
|
|
43
|
-
const MULTI_PARAGRAPH = /\n\s*\n/;
|
|
44
|
-
|
|
45
|
-
// ── Confidence thresholds ─────────────────────────────────────────────
|
|
46
|
-
|
|
47
|
-
const HINT_MAX_TURN_AGE = 4;
|
|
48
|
-
const HINT_MAX_AGE_MS = 5 * 60 * 1000; // 5 minutes
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Classify the complexity tier of a user message (backward-compat wrapper).
|
|
52
|
-
*/
|
|
53
|
-
export function classifyResponseTier(message: string, _turnCount: number): ResponseTier {
|
|
54
|
-
return classifyResponseTierDetailed(message, _turnCount).tier;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Classify with confidence scoring. High confidence means the regex
|
|
59
|
-
* matched an unambiguous signal; low confidence means the message
|
|
60
|
-
* fell through to the default medium bucket.
|
|
61
|
-
*/
|
|
62
|
-
export function classifyResponseTierDetailed(message: string, _turnCount: number): TierClassification {
|
|
63
|
-
const trimmed = message.trim();
|
|
64
|
-
const len = trimmed.length;
|
|
65
|
-
|
|
66
|
-
const isPoliteImperative = /^(can|could|would|will)\s+you\s+/i.test(trimmed) && BUILD_KEYWORDS.test(trimmed);
|
|
67
|
-
|
|
68
|
-
const isQuestion = !isPoliteImperative && (
|
|
69
|
-
/\?$/.test(trimmed) || /^(what|who|where|when|why|how|which|can|could|should|would|is|are|do|does|did|will|has|have)\b/i.test(trimmed)
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
// ── High signals (any match → high tier, high confidence) ──
|
|
73
|
-
if (SURFACE_ACTION.test(trimmed)) return tagged('high', 'surface_action', 'high');
|
|
74
|
-
if (len > 500) return tagged('high', 'length>500', 'high');
|
|
75
|
-
if (CODE_FENCE.test(trimmed)) return tagged('high', 'code_fence', 'high');
|
|
76
|
-
if (FILE_PATH.test(trimmed)) return tagged('high', 'file_path', 'high');
|
|
77
|
-
if (MULTI_PARAGRAPH.test(trimmed)) return tagged('high', 'multi_paragraph', 'high');
|
|
78
|
-
if (!isQuestion && BUILD_KEYWORDS.test(trimmed)) return tagged('high', 'build_keyword', 'high');
|
|
79
|
-
|
|
80
|
-
// ── Low signals (any match → low tier, high confidence) ──
|
|
81
|
-
if (GREETING_PATTERNS.test(trimmed) && len < 40 && !BUILD_KEYWORDS.test(trimmed)) return tagged('low', 'greeting', 'high');
|
|
82
|
-
if (len < 80 && !BUILD_KEYWORDS.test(trimmed)) return tagged('low', 'short_no_keywords', 'high');
|
|
83
|
-
|
|
84
|
-
// ── Default (low confidence — ambiguous) ──
|
|
85
|
-
return tagged('medium', 'default', 'low');
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const TIER_RANK: Record<ResponseTier, number> = { low: 0, medium: 1, high: 2 };
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Resolve the final tier using the regex classification and an optional
|
|
92
|
-
* session hint from a previous background Haiku call.
|
|
93
|
-
*
|
|
94
|
-
* - When confidence is low, defer to a fresh hint (upgrade or downgrade).
|
|
95
|
-
* - When confidence is high, still upgrade if the hint is higher (the
|
|
96
|
-
* conversation trajectory outranks a short-message heuristic), but
|
|
97
|
-
* never downgrade.
|
|
98
|
-
*/
|
|
99
|
-
export function resolveWithHint(
|
|
100
|
-
classification: TierClassification,
|
|
101
|
-
hint: SessionTierHint | null,
|
|
102
|
-
currentTurn: number,
|
|
103
|
-
): ResponseTier {
|
|
104
|
-
if (!hint) {
|
|
105
|
-
return classification.tier;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const turnAge = currentTurn - hint.turn;
|
|
109
|
-
const timeAge = Date.now() - hint.timestamp;
|
|
110
|
-
|
|
111
|
-
if (turnAge > HINT_MAX_TURN_AGE || timeAge > HINT_MAX_AGE_MS) {
|
|
112
|
-
log.debug({ turnAge, timeAge }, 'Session tier hint is stale, ignoring');
|
|
113
|
-
return classification.tier;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (classification.confidence === 'low') {
|
|
117
|
-
// Low confidence: fully defer to hint
|
|
118
|
-
log.info(
|
|
119
|
-
{ regexTier: classification.tier, hintTier: hint.tier, turnAge },
|
|
120
|
-
'Deferring to session tier hint (low confidence)',
|
|
121
|
-
);
|
|
122
|
-
return hint.tier;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// High confidence: only upgrade, never downgrade
|
|
126
|
-
if (TIER_RANK[hint.tier] > TIER_RANK[classification.tier]) {
|
|
127
|
-
log.info(
|
|
128
|
-
{ regexTier: classification.tier, hintTier: hint.tier, turnAge },
|
|
129
|
-
'Upgrading tier via session hint',
|
|
130
|
-
);
|
|
131
|
-
return hint.tier;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return classification.tier;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ── Async Haiku classification ────────────────────────────────────────
|
|
138
|
-
|
|
139
|
-
const ASYNC_CLASSIFICATION_TIMEOUT_MS = 8_000;
|
|
140
|
-
|
|
141
|
-
const TIER_SYSTEM_PROMPT =
|
|
142
|
-
'Classify the overall complexity of this conversation. ' +
|
|
143
|
-
'Output ONLY one word, nothing else.\n' +
|
|
144
|
-
'low — greetings, thanks, short acknowledgements\n' +
|
|
145
|
-
'medium — simple questions, short requests, clarifications\n' +
|
|
146
|
-
'high — build/implement/refactor requests, multi-step tasks, code-heavy work';
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Fire-and-forget Haiku call to classify the conversation trajectory.
|
|
150
|
-
* Returns the classified tier, or undefined when no provider is configured
|
|
151
|
-
* or on any failure.
|
|
152
|
-
*/
|
|
153
|
-
export async function classifyResponseTierAsync(
|
|
154
|
-
recentUserTexts: string[],
|
|
155
|
-
): Promise<ResponseTier | undefined> {
|
|
156
|
-
const provider = getConfiguredProvider();
|
|
157
|
-
if (!provider) {
|
|
158
|
-
log.debug('No provider available for async tier classification');
|
|
159
|
-
return undefined;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const combined = recentUserTexts
|
|
163
|
-
.map((t, i) => `[Message ${i + 1}]: ${t}`)
|
|
164
|
-
.join('\n');
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
const { signal, cleanup } = createTimeout(ASYNC_CLASSIFICATION_TIMEOUT_MS);
|
|
168
|
-
try {
|
|
169
|
-
const response = await provider.sendMessage(
|
|
170
|
-
[userMessage(combined)],
|
|
171
|
-
undefined,
|
|
172
|
-
TIER_SYSTEM_PROMPT,
|
|
173
|
-
{
|
|
174
|
-
config: {
|
|
175
|
-
modelIntent: 'latency-optimized',
|
|
176
|
-
max_tokens: 8,
|
|
177
|
-
},
|
|
178
|
-
signal,
|
|
179
|
-
},
|
|
180
|
-
);
|
|
181
|
-
cleanup();
|
|
182
|
-
|
|
183
|
-
const raw = extractText(response).toLowerCase();
|
|
184
|
-
const match = raw.match(/\b(low|medium|high)\b/);
|
|
185
|
-
if (match) {
|
|
186
|
-
const tier = match[1] as ResponseTier;
|
|
187
|
-
log.debug({ tier, raw }, 'Async tier classification result');
|
|
188
|
-
return tier;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
log.debug({ raw }, 'Async tier classification returned unexpected value');
|
|
192
|
-
return undefined;
|
|
193
|
-
} finally {
|
|
194
|
-
cleanup();
|
|
195
|
-
}
|
|
196
|
-
} catch (err) {
|
|
197
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
198
|
-
log.debug({ err: message }, 'Async tier classification failed');
|
|
199
|
-
return undefined;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function tagged(tier: ResponseTier, reason: string, confidence: TierConfidence): TierClassification {
|
|
204
|
-
log.debug({ tier, reason, confidence }, 'Classified response tier');
|
|
205
|
-
return { tier, reason, confidence };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// ── Token scaling ─────────────────────────────────────────────────────
|
|
209
|
-
|
|
210
|
-
const TIER_SCALE: Record<ResponseTier, number> = {
|
|
211
|
-
low: 0.125,
|
|
212
|
-
medium: 0.375,
|
|
213
|
-
high: 1,
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Scale the configured max tokens ceiling by the tier multiplier.
|
|
218
|
-
*
|
|
219
|
-
* Examples with configuredMax = 16000:
|
|
220
|
-
* low → 2000
|
|
221
|
-
* medium → 6000
|
|
222
|
-
* high → 16000
|
|
223
|
-
*/
|
|
224
|
-
export function tierMaxTokens(tier: ResponseTier, configuredMax: number): number {
|
|
225
|
-
return Math.round(configuredMax * TIER_SCALE[tier]);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// ── Model routing ─────────────────────────────────────────────────────
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Map for Anthropic provider: tier → model.
|
|
232
|
-
* low → sonnet (balanced)
|
|
233
|
-
* medium → sonnet (balanced)
|
|
234
|
-
* high → undefined (use configured default, typically opus)
|
|
235
|
-
*/
|
|
236
|
-
const ANTHROPIC_TIER_MODELS: Record<ResponseTier, string | undefined> = {
|
|
237
|
-
low: 'claude-sonnet-4-6',
|
|
238
|
-
medium: 'claude-sonnet-4-6',
|
|
239
|
-
high: undefined, // use configured default
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Returns a model override for the given tier, or undefined to use the
|
|
244
|
-
* provider's configured default. Only applies model downgrading for
|
|
245
|
-
* the Anthropic provider.
|
|
246
|
-
*/
|
|
247
|
-
export function tierModel(tier: ResponseTier, providerName: string): string | undefined {
|
|
248
|
-
if (providerName !== 'anthropic') return undefined;
|
|
249
|
-
return ANTHROPIC_TIER_MODELS[tier];
|
|
250
|
-
}
|