@vellumai/assistant 0.3.2 → 0.3.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/README.md +82 -21
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +16 -0
- package/src/__tests__/app-git-history.test.ts +22 -27
- package/src/__tests__/app-git-service.test.ts +44 -78
- package/src/__tests__/call-orchestrator.test.ts +321 -0
- package/src/__tests__/channel-approval-routes.test.ts +1267 -93
- package/src/__tests__/channel-approval.test.ts +2 -0
- package/src/__tests__/channel-approvals.test.ts +51 -2
- package/src/__tests__/channel-delivery-store.test.ts +130 -1
- package/src/__tests__/channel-guardian.test.ts +371 -1
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/daemon-lifecycle.test.ts +635 -0
- package/src/__tests__/daemon-server-session-init.test.ts +5 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +106 -21
- package/src/__tests__/handlers-telegram-config.test.ts +82 -0
- package/src/__tests__/handlers-twilio-config.test.ts +738 -5
- package/src/__tests__/ingress-url-consistency.test.ts +64 -0
- package/src/__tests__/ipc-snapshot.test.ts +10 -0
- package/src/__tests__/run-orchestrator.test.ts +1 -1
- package/src/__tests__/secret-scanner.test.ts +223 -0
- package/src/__tests__/session-process-bridge.test.ts +2 -0
- package/src/__tests__/shell-parser-property.test.ts +357 -2
- package/src/__tests__/system-prompt.test.ts +25 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +34 -1
- package/src/__tests__/tool-permission-simulate-handler.test.ts +2 -2
- package/src/__tests__/user-reference.test.ts +68 -0
- package/src/calls/call-orchestrator.ts +63 -11
- package/src/calls/twilio-config.ts +10 -1
- package/src/calls/twilio-rest.ts +70 -0
- package/src/cli/map.ts +6 -0
- package/src/commands/__tests__/cc-command-registry.test.ts +67 -0
- package/src/commands/cc-command-registry.ts +14 -1
- package/src/config/bundled-skills/claude-code/TOOLS.json +10 -3
- package/src/config/bundled-skills/email-setup/SKILL.md +56 -0
- package/src/config/bundled-skills/messaging/SKILL.md +4 -0
- package/src/config/bundled-skills/subagent/SKILL.md +4 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
- package/src/config/defaults.ts +1 -1
- package/src/config/schema.ts +6 -3
- package/src/config/skills.ts +5 -32
- package/src/config/system-prompt.ts +16 -0
- package/src/config/user-reference.ts +29 -0
- package/src/config/vellum-skills/catalog.json +52 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -1
- package/src/config/vellum-skills/twilio-setup/SKILL.md +49 -4
- package/src/daemon/auth-manager.ts +103 -0
- package/src/daemon/computer-use-session.ts +8 -1
- package/src/daemon/config-watcher.ts +253 -0
- package/src/daemon/handlers/config.ts +193 -17
- package/src/daemon/handlers/sessions.ts +5 -3
- package/src/daemon/handlers/skills.ts +60 -17
- package/src/daemon/ipc-contract-inventory.json +4 -0
- package/src/daemon/ipc-contract.ts +16 -0
- package/src/daemon/ipc-handler.ts +87 -0
- package/src/daemon/lifecycle.ts +16 -4
- package/src/daemon/ride-shotgun-handler.ts +11 -1
- package/src/daemon/server.ts +105 -502
- package/src/daemon/session-agent-loop.ts +9 -14
- package/src/daemon/session-process.ts +20 -3
- package/src/daemon/session-runtime-assembly.ts +60 -44
- package/src/daemon/session-slash.ts +50 -2
- package/src/daemon/session-surfaces.ts +17 -1
- package/src/daemon/session.ts +8 -1
- package/src/inbound/public-ingress-urls.ts +20 -3
- package/src/index.ts +1 -23
- package/src/memory/app-git-service.ts +24 -0
- package/src/memory/app-store.ts +0 -21
- package/src/memory/channel-delivery-store.ts +74 -3
- package/src/memory/channel-guardian-store.ts +54 -26
- package/src/memory/conversation-key-store.ts +20 -0
- package/src/memory/conversation-store.ts +14 -2
- package/src/memory/db-connection.ts +28 -0
- package/src/memory/db-init.ts +1019 -0
- package/src/memory/db.ts +2 -1995
- package/src/memory/embedding-backend.ts +79 -11
- package/src/memory/indexer.ts +2 -0
- package/src/memory/job-utils.ts +64 -4
- package/src/memory/jobs-worker.ts +7 -1
- package/src/memory/recall-cache.ts +107 -0
- package/src/memory/retriever.ts +30 -1
- package/src/memory/schema-migration.ts +984 -0
- package/src/memory/schema.ts +6 -0
- package/src/memory/search/types.ts +2 -0
- package/src/permissions/prompter.ts +14 -3
- package/src/permissions/trust-store.ts +7 -0
- package/src/runtime/channel-approvals.ts +17 -3
- package/src/runtime/gateway-client.ts +2 -1
- package/src/runtime/http-server.ts +28 -9
- package/src/runtime/routes/channel-routes.ts +279 -100
- package/src/runtime/routes/run-routes.ts +7 -1
- package/src/runtime/run-orchestrator.ts +8 -1
- package/src/security/secret-scanner.ts +218 -0
- package/src/skills/clawhub.ts +6 -2
- package/src/skills/frontmatter.ts +63 -0
- package/src/skills/slash-commands.ts +23 -0
- package/src/skills/vellum-catalog-remote.ts +107 -0
- package/src/subagent/manager.ts +4 -1
- package/src/subagent/types.ts +2 -0
- package/src/tools/browser/auto-navigate.ts +132 -24
- package/src/tools/browser/browser-manager.ts +67 -61
- package/src/tools/claude-code/claude-code.ts +55 -3
- package/src/tools/executor.ts +10 -2
- package/src/tools/skills/vellum-catalog.ts +75 -127
- package/src/tools/subagent/spawn.ts +2 -0
- package/src/tools/terminal/parser.ts +21 -5
- package/src/util/platform.ts +8 -1
- package/src/util/retry.ts +4 -4
|
@@ -31,10 +31,7 @@ import type { MessageQueue } from './session-queue-manager.js';
|
|
|
31
31
|
import type { QueueDrainReason } from './session-queue-manager.js';
|
|
32
32
|
import {
|
|
33
33
|
applyRuntimeInjections,
|
|
34
|
-
|
|
35
|
-
stripWorkspaceTopLevelContext,
|
|
36
|
-
stripChannelCapabilityContext,
|
|
37
|
-
stripTemporalContext,
|
|
34
|
+
stripInjectedContext,
|
|
38
35
|
} from './session-runtime-assembly.js';
|
|
39
36
|
import { buildTemporalContext } from './date-context.js';
|
|
40
37
|
import type { ActiveSurfaceContext, ChannelCapabilities } from './session-runtime-assembly.js';
|
|
@@ -58,6 +55,7 @@ import { repairHistory, deepRepairHistory } from './history-repair.js';
|
|
|
58
55
|
import { stripMediaPayloadsForRetry, raceWithTimeout } from './session-media-retry.js';
|
|
59
56
|
import { commitTurnChanges } from '../workspace/turn-commit.js';
|
|
60
57
|
import { getWorkspaceGitService } from '../workspace/git-service.js';
|
|
58
|
+
import { commitAppTurnChanges } from '../memory/app-git-service.js';
|
|
61
59
|
import type { UsageActor } from '../usage/actors.js';
|
|
62
60
|
import type { SkillProjectionCache } from './session-skill-tools.js';
|
|
63
61
|
|
|
@@ -739,16 +737,10 @@ export async function runAgentLoopImpl(
|
|
|
739
737
|
}
|
|
740
738
|
|
|
741
739
|
const restoredHistory = [...preRepairMessages, ...newMessages];
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
stripActiveSurfaceContext(
|
|
747
|
-
stripDynamicProfileMessages(recallStripped, dynamicProfile.text),
|
|
748
|
-
),
|
|
749
|
-
),
|
|
750
|
-
),
|
|
751
|
-
);
|
|
740
|
+
ctx.messages = stripInjectedContext(restoredHistory, {
|
|
741
|
+
stripRecall: (msgs) => stripMemoryRecallMessages(msgs, recall.injectedText, recallInjectionStrategy),
|
|
742
|
+
stripDynamicProfile: (msgs) => stripDynamicProfileMessages(msgs, dynamicProfile.text),
|
|
743
|
+
});
|
|
752
744
|
|
|
753
745
|
emitUsage(ctx, exchangeInputTokens, exchangeOutputTokens, model, onEvent, 'main_agent', reqId);
|
|
754
746
|
|
|
@@ -859,6 +851,9 @@ export async function runAgentLoopImpl(
|
|
|
859
851
|
'Turn-boundary commit timed out — continuing without waiting (commit still runs in background)',
|
|
860
852
|
);
|
|
861
853
|
}
|
|
854
|
+
|
|
855
|
+
// Commit app changes (fire-and-forget — apps repo is separate from workspace)
|
|
856
|
+
void commitAppTurnChanges(ctx.conversationId, ctx.turnCount);
|
|
862
857
|
}
|
|
863
858
|
|
|
864
859
|
ctx.profiler.emitSummary(ctx.traceEmitter, reqId);
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
|
|
9
9
|
import type { Message } from '../providers/types.js';
|
|
10
10
|
import type { ServerMessage, UserMessageAttachment } from './ipc-protocol.js';
|
|
11
|
+
import type { UsageStats } from './ipc-contract.js';
|
|
11
12
|
import type { MessageQueue } from './session-queue-manager.js';
|
|
12
13
|
import type { QueueDrainReason } from './session-queue-manager.js';
|
|
13
14
|
import type { TraceEmitter } from './trace-emitter.js';
|
|
14
15
|
import { createUserMessage, createAssistantMessage } from '../agent/message-types.js';
|
|
15
16
|
import * as conversationStore from '../memory/conversation-store.js';
|
|
16
|
-
import { resolveSlash } from './session-slash.js';
|
|
17
|
+
import { resolveSlash, type SlashContext } from './session-slash.js';
|
|
17
18
|
import { getConfig } from '../config/loader.js';
|
|
18
19
|
import { getLogger } from '../util/logger.js';
|
|
19
20
|
import { tryRouteCallMessage } from '../calls/call-bridge.js';
|
|
@@ -56,6 +57,8 @@ export interface ProcessSessionContext {
|
|
|
56
57
|
readonly traceEmitter: TraceEmitter;
|
|
57
58
|
currentActiveSurfaceId?: string;
|
|
58
59
|
currentPage?: string;
|
|
60
|
+
/** Cumulative token usage stats for the session. */
|
|
61
|
+
readonly usageStats: UsageStats;
|
|
59
62
|
/** Request-scoped skill IDs preactivated via slash resolution. */
|
|
60
63
|
preactivatedSkillIds?: string[];
|
|
61
64
|
persistUserMessage(content: string, attachments: UserMessageAttachment[], requestId?: string, metadata?: Record<string, unknown>): string;
|
|
@@ -67,6 +70,20 @@ export interface ProcessSessionContext {
|
|
|
67
70
|
): Promise<void>;
|
|
68
71
|
}
|
|
69
72
|
|
|
73
|
+
/** Build a SlashContext from the current session state and config. */
|
|
74
|
+
function buildSlashContext(session: ProcessSessionContext): SlashContext {
|
|
75
|
+
const config = getConfig();
|
|
76
|
+
return {
|
|
77
|
+
messageCount: session.messages.length,
|
|
78
|
+
inputTokens: session.usageStats.inputTokens,
|
|
79
|
+
outputTokens: session.usageStats.outputTokens,
|
|
80
|
+
maxInputTokens: config.contextWindow.maxInputTokens,
|
|
81
|
+
model: config.model,
|
|
82
|
+
provider: config.provider,
|
|
83
|
+
estimatedCost: session.usageStats.estimatedCost,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
70
87
|
// ── drainQueue ───────────────────────────────────────────────────────
|
|
71
88
|
|
|
72
89
|
/**
|
|
@@ -96,7 +113,7 @@ export function drainQueue(session: ProcessSessionContext, reason: QueueDrainRea
|
|
|
96
113
|
});
|
|
97
114
|
|
|
98
115
|
// Resolve slash commands for queued messages
|
|
99
|
-
const slashResult = resolveSlash(next.content);
|
|
116
|
+
const slashResult = resolveSlash(next.content, buildSlashContext(session));
|
|
100
117
|
|
|
101
118
|
// Unknown slash — persist the exchange and continue draining.
|
|
102
119
|
// Persist each message before pushing to session.messages so that a
|
|
@@ -242,7 +259,7 @@ export async function processMessage(
|
|
|
242
259
|
session.currentPage = currentPage;
|
|
243
260
|
|
|
244
261
|
// Resolve slash commands before persistence
|
|
245
|
-
const slashResult = resolveSlash(content);
|
|
262
|
+
const slashResult = resolveSlash(content, buildSlashContext(session));
|
|
246
263
|
|
|
247
264
|
// Unknown slash command — persist the exchange (user + assistant) so the
|
|
248
265
|
// messageId is real. Persist each message before pushing to session.messages
|
|
@@ -264,16 +264,24 @@ export function injectChannelCapabilityContext(message: Message, caps: ChannelCa
|
|
|
264
264
|
};
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// Prefix-based stripping primitive
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
|
|
267
271
|
/**
|
|
268
|
-
*
|
|
269
|
-
*
|
|
272
|
+
* Remove text blocks from user messages whose text starts with any of the
|
|
273
|
+
* given prefixes. If stripping removes all content blocks from a message,
|
|
274
|
+
* the message itself is dropped.
|
|
275
|
+
*
|
|
276
|
+
* This is the shared primitive behind the individual strip* functions and
|
|
277
|
+
* the `stripInjectedContext` pipeline.
|
|
270
278
|
*/
|
|
271
|
-
export function
|
|
279
|
+
export function stripUserTextBlocksByPrefix(messages: Message[], prefixes: string[]): Message[] {
|
|
272
280
|
return messages.map((message) => {
|
|
273
281
|
if (message.role !== 'user') return message;
|
|
274
282
|
const nextContent = message.content.filter((block) => {
|
|
275
283
|
if (block.type !== 'text') return true;
|
|
276
|
-
return !block.text.startsWith(
|
|
284
|
+
return !prefixes.some((p) => block.text.startsWith(p));
|
|
277
285
|
});
|
|
278
286
|
if (nextContent.length === message.content.length) return message;
|
|
279
287
|
if (nextContent.length === 0) return null;
|
|
@@ -281,6 +289,15 @@ export function stripChannelCapabilityContext(messages: Message[]): Message[] {
|
|
|
281
289
|
}).filter((message): message is NonNullable<typeof message> => message !== null);
|
|
282
290
|
}
|
|
283
291
|
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// Individual strip functions (thin wrappers around the primitive)
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
/** Strip `<channel_capabilities>` blocks injected by `injectChannelCapabilityContext`. */
|
|
297
|
+
export function stripChannelCapabilityContext(messages: Message[]): Message[] {
|
|
298
|
+
return stripUserTextBlocksByPrefix(messages, ['<channel_capabilities>']);
|
|
299
|
+
}
|
|
300
|
+
|
|
284
301
|
/**
|
|
285
302
|
* Prepend workspace top-level directory context to a user message.
|
|
286
303
|
*/
|
|
@@ -294,22 +311,9 @@ export function injectWorkspaceTopLevelContext(message: Message, contextText: st
|
|
|
294
311
|
};
|
|
295
312
|
}
|
|
296
313
|
|
|
297
|
-
/**
|
|
298
|
-
* Strip `<workspace_top_level>` blocks injected by
|
|
299
|
-
* `injectWorkspaceTopLevelContext`. Called after the agent run to prevent
|
|
300
|
-
* workspace context from persisting in session history.
|
|
301
|
-
*/
|
|
314
|
+
/** Strip `<workspace_top_level>` blocks injected by `injectWorkspaceTopLevelContext`. */
|
|
302
315
|
export function stripWorkspaceTopLevelContext(messages: Message[]): Message[] {
|
|
303
|
-
return messages
|
|
304
|
-
if (message.role !== 'user') return message;
|
|
305
|
-
const nextContent = message.content.filter((block) => {
|
|
306
|
-
if (block.type !== 'text') return true;
|
|
307
|
-
return !block.text.startsWith('<workspace_top_level>');
|
|
308
|
-
});
|
|
309
|
-
if (nextContent.length === message.content.length) return message;
|
|
310
|
-
if (nextContent.length === 0) return null;
|
|
311
|
-
return { ...message, content: nextContent };
|
|
312
|
-
}).filter((message): message is NonNullable<typeof message> => message !== null);
|
|
316
|
+
return stripUserTextBlocksByPrefix(messages, ['<workspace_top_level>']);
|
|
313
317
|
}
|
|
314
318
|
|
|
315
319
|
/**
|
|
@@ -328,8 +332,6 @@ export function injectTemporalContext(message: Message, temporalContext: string)
|
|
|
328
332
|
|
|
329
333
|
/**
|
|
330
334
|
* Strip `<temporal_context>` blocks injected by `injectTemporalContext`.
|
|
331
|
-
* Called after the agent run to prevent temporal context from persisting
|
|
332
|
-
* in session history.
|
|
333
335
|
*
|
|
334
336
|
* Uses a specific prefix (`<temporal_context>\nToday:`) so that
|
|
335
337
|
* user-authored text that happens to start with `<temporal_context>`
|
|
@@ -338,35 +340,49 @@ export function injectTemporalContext(message: Message, temporalContext: string)
|
|
|
338
340
|
const TEMPORAL_INJECTED_PREFIX = '<temporal_context>\nToday:';
|
|
339
341
|
|
|
340
342
|
export function stripTemporalContext(messages: Message[]): Message[] {
|
|
341
|
-
return messages
|
|
342
|
-
if (message.role !== 'user') return message;
|
|
343
|
-
const nextContent = message.content.filter((block) => {
|
|
344
|
-
if (block.type !== 'text') return true;
|
|
345
|
-
return !block.text.startsWith(TEMPORAL_INJECTED_PREFIX);
|
|
346
|
-
});
|
|
347
|
-
if (nextContent.length === message.content.length) return message;
|
|
348
|
-
if (nextContent.length === 0) return null;
|
|
349
|
-
return { ...message, content: nextContent };
|
|
350
|
-
}).filter((message): message is NonNullable<typeof message> => message !== null);
|
|
343
|
+
return stripUserTextBlocksByPrefix(messages, [TEMPORAL_INJECTED_PREFIX]);
|
|
351
344
|
}
|
|
352
345
|
|
|
353
346
|
/**
|
|
354
347
|
* Strip `<active_workspace>` (and legacy `<active_dynamic_page>`) blocks
|
|
355
|
-
* injected by `injectActiveSurfaceContext`.
|
|
356
|
-
* prevent the (potentially 100 KB) surface HTML from persisting in session
|
|
357
|
-
* history.
|
|
348
|
+
* injected by `injectActiveSurfaceContext`.
|
|
358
349
|
*/
|
|
359
350
|
export function stripActiveSurfaceContext(messages: Message[]): Message[] {
|
|
360
|
-
return messages
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
351
|
+
return stripUserTextBlocksByPrefix(messages, ['<active_workspace>', '<active_dynamic_page>']);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
// Declarative strip pipeline
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
|
|
358
|
+
/** Prefixes stripped by the pipeline (order doesn't matter — single pass). */
|
|
359
|
+
const RUNTIME_INJECTION_PREFIXES = [
|
|
360
|
+
'<channel_capabilities>',
|
|
361
|
+
'<workspace_top_level>',
|
|
362
|
+
TEMPORAL_INJECTED_PREFIX,
|
|
363
|
+
'<active_workspace>',
|
|
364
|
+
'<active_dynamic_page>',
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Strip all runtime-injected context from message history in a single pass.
|
|
369
|
+
*
|
|
370
|
+
* Composes:
|
|
371
|
+
* 1. `stripMemoryRecallMessages` (caller-supplied, handles its own logic)
|
|
372
|
+
* 2. `stripDynamicProfileMessages` (caller-supplied, handles its own logic)
|
|
373
|
+
* 3. Prefix-based stripping for channel capabilities, workspace top-level,
|
|
374
|
+
* temporal context, and active surface context (single pass).
|
|
375
|
+
*/
|
|
376
|
+
export function stripInjectedContext(
|
|
377
|
+
messages: Message[],
|
|
378
|
+
options: {
|
|
379
|
+
stripRecall: (msgs: Message[]) => Message[];
|
|
380
|
+
stripDynamicProfile: (msgs: Message[]) => Message[];
|
|
381
|
+
},
|
|
382
|
+
): Message[] {
|
|
383
|
+
const afterRecall = options.stripRecall(messages);
|
|
384
|
+
const afterProfile = options.stripDynamicProfile(afterRecall);
|
|
385
|
+
return stripUserTextBlocksByPrefix(afterProfile, RUNTIME_INJECTION_PREFIXES);
|
|
370
386
|
}
|
|
371
387
|
|
|
372
388
|
/**
|
|
@@ -15,6 +15,18 @@ export type SlashResolution =
|
|
|
15
15
|
| { kind: 'rewritten'; content: string; skillId: string }
|
|
16
16
|
| { kind: 'unknown'; message: string };
|
|
17
17
|
|
|
18
|
+
// ── /status command ──────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export interface SlashContext {
|
|
21
|
+
messageCount: number;
|
|
22
|
+
inputTokens: number;
|
|
23
|
+
outputTokens: number;
|
|
24
|
+
maxInputTokens: number;
|
|
25
|
+
model: string;
|
|
26
|
+
provider: string;
|
|
27
|
+
estimatedCost: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
// ── /model command ───────────────────────────────────────────────────
|
|
19
31
|
|
|
20
32
|
const AVAILABLE_MODELS = [
|
|
@@ -242,11 +254,31 @@ function resolveModelCommand(content: string): SlashResolution | null {
|
|
|
242
254
|
};
|
|
243
255
|
}
|
|
244
256
|
|
|
257
|
+
function resolveStatusCommand(context: SlashContext): SlashResolution {
|
|
258
|
+
const { inputTokens, maxInputTokens, model, provider, messageCount, outputTokens, estimatedCost } = context;
|
|
259
|
+
const pct = maxInputTokens > 0 ? Math.min(Math.round((inputTokens / maxInputTokens) * 100), 100) : 0;
|
|
260
|
+
const filled = Math.round(pct / 5);
|
|
261
|
+
const bar = '█'.repeat(filled) + '░'.repeat(20 - filled);
|
|
262
|
+
const fmt = (n: number) => n.toLocaleString('en-US');
|
|
263
|
+
const displayName = MODEL_DISPLAY_NAMES[model] ?? model;
|
|
264
|
+
|
|
265
|
+
const lines = [
|
|
266
|
+
'Session Status\n',
|
|
267
|
+
`Context: ${bar} ${pct}% (${fmt(inputTokens)} / ${fmt(maxInputTokens)} tokens)`,
|
|
268
|
+
`Model: ${displayName} (${provider})`,
|
|
269
|
+
`Messages: ${fmt(messageCount)}`,
|
|
270
|
+
`Tokens: ${fmt(inputTokens)} in / ${fmt(outputTokens)} out`,
|
|
271
|
+
`Cost: $${estimatedCost.toFixed(2)} (estimated)`,
|
|
272
|
+
];
|
|
273
|
+
|
|
274
|
+
return { kind: 'unknown', message: lines.join('\n') };
|
|
275
|
+
}
|
|
276
|
+
|
|
245
277
|
/**
|
|
246
278
|
* Resolve slash commands against the current skill catalog.
|
|
247
279
|
* Returns `unknown` with a deterministic message, or the (possibly rewritten) content.
|
|
248
280
|
*/
|
|
249
|
-
export function resolveSlash(content: string): SlashResolution {
|
|
281
|
+
export function resolveSlash(content: string, context?: SlashContext): SlashResolution {
|
|
250
282
|
// Check provider shortcuts first (/gpt4, /opus, etc.)
|
|
251
283
|
const providerResult = resolveProviderModelCommand(content);
|
|
252
284
|
if (providerResult) return providerResult;
|
|
@@ -255,11 +287,27 @@ export function resolveSlash(content: string): SlashResolution {
|
|
|
255
287
|
const modelResult = resolveModelCommand(content);
|
|
256
288
|
if (modelResult) return modelResult;
|
|
257
289
|
|
|
290
|
+
// Handle /status command
|
|
291
|
+
if (content.trim() === '/status') {
|
|
292
|
+
if (!context) {
|
|
293
|
+
return { kind: 'unknown', message: 'Status information is not available in this context.' };
|
|
294
|
+
}
|
|
295
|
+
return resolveStatusCommand(context);
|
|
296
|
+
}
|
|
297
|
+
|
|
258
298
|
// Handle /commands command
|
|
259
299
|
if (content.trim() === '/commands') {
|
|
300
|
+
const lines = [
|
|
301
|
+
'/commands — List all available commands',
|
|
302
|
+
'/model — Show or switch the current model',
|
|
303
|
+
'/models — List all available models',
|
|
304
|
+
];
|
|
305
|
+
if (context) {
|
|
306
|
+
lines.push('/status — Show session status and context usage');
|
|
307
|
+
}
|
|
260
308
|
return {
|
|
261
309
|
kind: 'unknown',
|
|
262
|
-
message: '
|
|
310
|
+
message: lines.join('\n'),
|
|
263
311
|
};
|
|
264
312
|
}
|
|
265
313
|
|
|
@@ -429,6 +429,12 @@ export function refreshSurfacesForApp(ctx: SurfaceSessionContext, appId: string,
|
|
|
429
429
|
};
|
|
430
430
|
stored.data = updatedData;
|
|
431
431
|
|
|
432
|
+
// Keep the persisted snapshot in sync so updates survive session restart.
|
|
433
|
+
const idx = ctx.currentTurnSurfaces.findIndex(s => s.surfaceId === surfaceId);
|
|
434
|
+
if (idx !== -1) {
|
|
435
|
+
ctx.currentTurnSurfaces[idx].data = updatedData;
|
|
436
|
+
}
|
|
437
|
+
|
|
432
438
|
// Push the update to the client
|
|
433
439
|
ctx.sendToClient({
|
|
434
440
|
type: 'ui_surface_update',
|
|
@@ -603,6 +609,13 @@ export async function surfaceProxyResolver(
|
|
|
603
609
|
surfaceId,
|
|
604
610
|
data: mergedData,
|
|
605
611
|
});
|
|
612
|
+
|
|
613
|
+
// Keep the persisted snapshot in sync so updates survive session restart.
|
|
614
|
+
const idx = ctx.currentTurnSurfaces.findIndex(s => s.surfaceId === surfaceId);
|
|
615
|
+
if (idx !== -1) {
|
|
616
|
+
ctx.currentTurnSurfaces[idx].data = mergedData;
|
|
617
|
+
}
|
|
618
|
+
|
|
606
619
|
return { content: 'Surface updated', isError: false };
|
|
607
620
|
}
|
|
608
621
|
|
|
@@ -662,7 +675,10 @@ export async function surfaceProxyResolver(
|
|
|
662
675
|
const seededHomeBase = findSeededHomeBaseApp();
|
|
663
676
|
const defaultPreview = seededHomeBase && seededHomeBase.id === app.id
|
|
664
677
|
? getPrebuiltHomeBasePreview()
|
|
665
|
-
|
|
678
|
+
// Generate a minimal fallback preview from app metadata so that the
|
|
679
|
+
// surface is always rendered as a clickable preview card (not an
|
|
680
|
+
// un-clickable fallback chip) after session restart.
|
|
681
|
+
: { title: app.name, subtitle: app.description };
|
|
666
682
|
|
|
667
683
|
const surfaceData: DynamicPageSurfaceData = {
|
|
668
684
|
html: app.htmlDefinition,
|
package/src/daemon/session.ts
CHANGED
|
@@ -308,8 +308,15 @@ export class Session {
|
|
|
308
308
|
decision: UserDecision,
|
|
309
309
|
selectedPattern?: string,
|
|
310
310
|
selectedScope?: string,
|
|
311
|
+
decisionContext?: string,
|
|
311
312
|
): void {
|
|
312
|
-
this.prompter.resolveConfirmation(
|
|
313
|
+
this.prompter.resolveConfirmation(
|
|
314
|
+
requestId,
|
|
315
|
+
decision,
|
|
316
|
+
selectedPattern,
|
|
317
|
+
selectedScope,
|
|
318
|
+
decisionContext,
|
|
319
|
+
);
|
|
313
320
|
}
|
|
314
321
|
|
|
315
322
|
handleSecretResponse(requestId: string, value?: string, delivery?: 'store' | 'transient_send'): void {
|
|
@@ -73,11 +73,20 @@ export function getPublicBaseUrl(config: IngressConfig): string {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
|
-
* Build the Twilio voice webhook URL
|
|
76
|
+
* Build the Twilio voice webhook URL.
|
|
77
|
+
*
|
|
78
|
+
* When `callSessionId` is provided (outbound calls), it is included as a
|
|
79
|
+
* query parameter so the gateway can correlate the webhook to an existing
|
|
80
|
+
* session. When omitted (phone-number-level webhook configuration for
|
|
81
|
+
* inbound calls), the URL is returned without the query parameter — the
|
|
82
|
+
* gateway will create a new session for inbound calls.
|
|
77
83
|
*/
|
|
78
|
-
export function getTwilioVoiceWebhookUrl(config: IngressConfig, callSessionId
|
|
84
|
+
export function getTwilioVoiceWebhookUrl(config: IngressConfig, callSessionId?: string): string {
|
|
79
85
|
const base = getPublicBaseUrl(config);
|
|
80
|
-
|
|
86
|
+
if (callSessionId) {
|
|
87
|
+
return `${base}/webhooks/twilio/voice?callSessionId=${callSessionId}`;
|
|
88
|
+
}
|
|
89
|
+
return `${base}/webhooks/twilio/voice`;
|
|
81
90
|
}
|
|
82
91
|
|
|
83
92
|
/**
|
|
@@ -114,6 +123,14 @@ export function getOAuthCallbackUrl(config: IngressConfig): string {
|
|
|
114
123
|
return `${base}/webhooks/oauth/callback`;
|
|
115
124
|
}
|
|
116
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Build the Twilio SMS webhook URL.
|
|
128
|
+
*/
|
|
129
|
+
export function getTwilioSmsWebhookUrl(config: IngressConfig): string {
|
|
130
|
+
const base = getPublicBaseUrl(config);
|
|
131
|
+
return `${base}/webhooks/twilio/sms`;
|
|
132
|
+
}
|
|
133
|
+
|
|
117
134
|
/**
|
|
118
135
|
* Build the Telegram webhook URL.
|
|
119
136
|
*/
|
package/src/index.ts
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
6
|
-
import { spawn } from 'node:child_process';
|
|
7
5
|
|
|
8
6
|
const require = createRequire(import.meta.url);
|
|
9
7
|
const { version } = require('../package.json') as { version: string };
|
|
@@ -58,24 +56,4 @@ registerCompletionsCommand(program);
|
|
|
58
56
|
registerTwitterCommand(program);
|
|
59
57
|
registerMapCommand(program);
|
|
60
58
|
|
|
61
|
-
|
|
62
|
-
const firstArg = process.argv[2];
|
|
63
|
-
|
|
64
|
-
if (firstArg && !firstArg.startsWith('-') && !knownCommands.has(firstArg)) {
|
|
65
|
-
try {
|
|
66
|
-
const cliPkgPath = require.resolve('@vellumai/cli/package.json');
|
|
67
|
-
const cliEntry = join(dirname(cliPkgPath), 'src', 'index.ts');
|
|
68
|
-
const child = spawn('bun', ['run', cliEntry, ...process.argv.slice(2)], {
|
|
69
|
-
stdio: 'inherit',
|
|
70
|
-
});
|
|
71
|
-
child.on('exit', (code) => {
|
|
72
|
-
process.exit(code ?? 1);
|
|
73
|
-
});
|
|
74
|
-
} catch {
|
|
75
|
-
console.error(`Unknown command: ${firstArg}`);
|
|
76
|
-
console.error('Install the full stack with: bun install -g vellum');
|
|
77
|
-
process.exit(1);
|
|
78
|
-
}
|
|
79
|
-
} else {
|
|
80
|
-
program.parse();
|
|
81
|
-
}
|
|
59
|
+
program.parse();
|
|
@@ -141,6 +141,30 @@ export async function commitAppChange(message: string): Promise<void> {
|
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Commit app changes at turn boundaries.
|
|
146
|
+
*
|
|
147
|
+
* Called once per agent turn (after all tool calls complete). Only creates
|
|
148
|
+
* a commit if there are actual changes in the apps directory, so multiple
|
|
149
|
+
* mutations within a single turn are batched into one version.
|
|
150
|
+
*
|
|
151
|
+
* Fire-and-forget safe: errors are logged but never thrown.
|
|
152
|
+
*/
|
|
153
|
+
export async function commitAppTurnChanges(sessionId: string, turnNumber: number): Promise<void> {
|
|
154
|
+
try {
|
|
155
|
+
const appsDir = getAppsDir();
|
|
156
|
+
ensureAppGitignoreRules(appsDir);
|
|
157
|
+
|
|
158
|
+
const gitService = getWorkspaceGitService(appsDir);
|
|
159
|
+
await gitService.commitIfDirty(() => ({
|
|
160
|
+
message: `Turn ${turnNumber}: app changes`,
|
|
161
|
+
metadata: { sessionId, turnNumber },
|
|
162
|
+
}));
|
|
163
|
+
} catch (err) {
|
|
164
|
+
log.error({ err, sessionId, turnNumber }, 'Failed to commit app turn changes');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
144
168
|
// ---------------------------------------------------------------------------
|
|
145
169
|
// Query methods
|
|
146
170
|
// ---------------------------------------------------------------------------
|
package/src/memory/app-store.ts
CHANGED
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
isPrebuiltHomeBaseApp,
|
|
30
30
|
validatePrebuiltHomeBaseHtml,
|
|
31
31
|
} from '../home-base/prebuilt-home-base-updater.js';
|
|
32
|
-
import { commitAppChange } from './app-git-service.js';
|
|
33
32
|
|
|
34
33
|
export interface AppDefinition {
|
|
35
34
|
id: string;
|
|
@@ -211,8 +210,6 @@ export function createApp(params: {
|
|
|
211
210
|
app.pages = params.pages;
|
|
212
211
|
}
|
|
213
212
|
|
|
214
|
-
void commitAppChange(`Create app: ${params.name}`);
|
|
215
|
-
|
|
216
213
|
return app;
|
|
217
214
|
}
|
|
218
215
|
|
|
@@ -375,27 +372,14 @@ export function updateApp(
|
|
|
375
372
|
updated.pages = loadedPages;
|
|
376
373
|
}
|
|
377
374
|
|
|
378
|
-
const changedFields = Object.keys(updates).filter(k => updates[k as keyof typeof updates] !== undefined);
|
|
379
|
-
void commitAppChange(`Update app: ${updated.name}\n\nChanged: ${changedFields.join(', ')}`);
|
|
380
|
-
|
|
381
375
|
return updated;
|
|
382
376
|
}
|
|
383
377
|
|
|
384
378
|
export function deleteApp(id: string): void {
|
|
385
379
|
validateId(id);
|
|
386
380
|
const dir = getAppsDir();
|
|
387
|
-
|
|
388
|
-
// Read app name before deleting for the commit message
|
|
389
|
-
let appName = id;
|
|
390
381
|
const filePath = join(dir, `${id}.json`);
|
|
391
382
|
if (existsSync(filePath)) {
|
|
392
|
-
try {
|
|
393
|
-
const raw = readFileSync(filePath, 'utf-8');
|
|
394
|
-
const app = JSON.parse(raw);
|
|
395
|
-
if (app.name) appName = app.name;
|
|
396
|
-
} catch {
|
|
397
|
-
// fall back to id
|
|
398
|
-
}
|
|
399
383
|
unlinkSync(filePath);
|
|
400
384
|
}
|
|
401
385
|
const previewPath = join(dir, `${id}.preview`);
|
|
@@ -404,8 +388,6 @@ export function deleteApp(id: string): void {
|
|
|
404
388
|
}
|
|
405
389
|
const appDir = join(dir, id);
|
|
406
390
|
rmSync(appDir, { recursive: true, force: true });
|
|
407
|
-
|
|
408
|
-
void commitAppChange(`Delete app: ${appName}`);
|
|
409
391
|
}
|
|
410
392
|
|
|
411
393
|
export function createAppRecord(appId: string, data: Record<string, unknown>): AppRecord {
|
|
@@ -545,8 +527,6 @@ export function writeAppFile(appId: string, path: string, content: string): void
|
|
|
545
527
|
const dir = join(resolved, '..');
|
|
546
528
|
mkdirSync(dir, { recursive: true });
|
|
547
529
|
writeFileSync(resolved, content, 'utf-8');
|
|
548
|
-
|
|
549
|
-
void commitAppChange(`Write ${path} in app ${appId}`);
|
|
550
530
|
}
|
|
551
531
|
|
|
552
532
|
/**
|
|
@@ -569,7 +549,6 @@ export function editAppFile(
|
|
|
569
549
|
const result = applyEdit(content, oldString, newString, replaceAll ?? false);
|
|
570
550
|
if (result.ok) {
|
|
571
551
|
writeFileSync(resolved, result.updatedContent, 'utf-8');
|
|
572
|
-
void commitAppChange(`Edit ${path} in app ${appId}`);
|
|
573
552
|
}
|
|
574
553
|
return result;
|
|
575
554
|
}
|