@vellumai/assistant 0.4.11 → 0.4.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +401 -385
- package/package.json +1 -1
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +75 -61
- package/src/__tests__/registry.test.ts +235 -187
- package/src/__tests__/secure-keys.test.ts +27 -0
- package/src/__tests__/session-agent-loop.test.ts +521 -256
- package/src/__tests__/session-surfaces-task-progress.test.ts +1 -0
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
- package/src/__tests__/skills.test.ts +334 -276
- package/src/__tests__/slack-skill.test.ts +124 -0
- package/src/__tests__/starter-task-flow.test.ts +7 -17
- package/src/agent/loop.ts +10 -3
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +449 -0
- package/src/config/bundled-skills/doordash/SKILL.md +171 -0
- package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +203 -0
- package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +164 -0
- package/src/config/bundled-skills/doordash/doordash-cli.ts +1193 -0
- package/src/config/bundled-skills/doordash/doordash-entry.ts +22 -0
- package/src/config/bundled-skills/doordash/lib/cart-queries.ts +787 -0
- package/src/config/bundled-skills/doordash/lib/client.ts +1071 -0
- package/src/config/bundled-skills/doordash/lib/order-queries.ts +85 -0
- package/src/config/bundled-skills/doordash/lib/queries.ts +28 -0
- package/src/config/bundled-skills/doordash/lib/query-extractor.ts +94 -0
- package/src/config/bundled-skills/doordash/lib/search-queries.ts +203 -0
- package/src/config/bundled-skills/doordash/lib/session.ts +93 -0
- package/src/config/bundled-skills/doordash/lib/shared/errors.ts +61 -0
- package/src/config/bundled-skills/doordash/lib/shared/ipc.ts +32 -0
- package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +380 -0
- package/src/config/bundled-skills/doordash/lib/shared/platform.ts +35 -0
- package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +43 -0
- package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +49 -0
- package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +6 -0
- package/src/config/bundled-skills/doordash/lib/store-queries.ts +246 -0
- package/src/config/bundled-skills/doordash/lib/types.ts +367 -0
- package/src/config/bundled-skills/google-calendar/SKILL.md +4 -5
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +41 -41
- package/src/config/bundled-skills/messaging/SKILL.md +59 -42
- package/src/config/bundled-skills/messaging/TOOLS.json +14 -92
- package/src/config/bundled-skills/messaging/tools/gmail-archive-by-query.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +11 -2
- package/src/config/bundled-skills/messaging/tools/gmail-outreach-scan.ts +8 -1
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +12 -4
- package/src/config/bundled-skills/messaging/tools/gmail-unsubscribe.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +5 -2
- package/src/config/bundled-skills/notion/SKILL.md +240 -0
- package/src/config/bundled-skills/notion-oauth-setup/SKILL.md +127 -0
- package/src/config/bundled-skills/oauth-setup/SKILL.md +144 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +76 -45
- package/src/config/bundled-skills/skills-catalog/SKILL.md +32 -29
- package/src/config/bundled-skills/slack/SKILL.md +49 -0
- package/src/config/bundled-skills/slack/TOOLS.json +167 -0
- package/src/config/bundled-skills/slack/tools/shared.ts +23 -0
- package/src/config/bundled-skills/{messaging → slack}/tools/slack-add-reaction.ts +2 -5
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +33 -0
- package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +75 -0
- package/src/config/bundled-skills/{messaging → slack}/tools/slack-delete-message.ts +2 -5
- package/src/config/bundled-skills/{messaging → slack}/tools/slack-leave-channel.ts +2 -5
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +193 -0
- package/src/config/{vellum-skills → bundled-skills}/sms-setup/SKILL.md +29 -22
- package/src/config/{vellum-skills → bundled-skills}/telegram-setup/SKILL.md +17 -14
- package/src/config/{vellum-skills → bundled-skills}/twilio-setup/SKILL.md +20 -5
- package/src/config/bundled-tool-registry.ts +292 -267
- package/src/config/schema.ts +1 -1
- package/src/daemon/handlers/skills.ts +334 -234
- package/src/daemon/ipc-contract/messages.ts +2 -0
- package/src/daemon/ipc-contract/surfaces.ts +2 -0
- package/src/daemon/lifecycle.ts +358 -221
- package/src/daemon/response-tier.ts +2 -0
- package/src/daemon/server.ts +453 -193
- package/src/daemon/session-agent-loop-handlers.ts +43 -2
- package/src/daemon/session-agent-loop.ts +3 -0
- package/src/daemon/session-lifecycle.ts +3 -0
- package/src/daemon/session-process.ts +1 -0
- package/src/daemon/session-surfaces.ts +22 -20
- package/src/daemon/session-tool-setup.ts +1 -0
- package/src/daemon/session.ts +5 -2
- package/src/messaging/outreach-classifier.ts +12 -5
- package/src/messaging/provider-types.ts +5 -0
- package/src/messaging/provider.ts +1 -1
- package/src/messaging/providers/gmail/adapter.ts +11 -5
- package/src/messaging/providers/gmail/client.ts +2 -0
- package/src/messaging/providers/slack/adapter.ts +1 -0
- package/src/messaging/providers/slack/client.ts +8 -0
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/runtime/http-errors.ts +33 -20
- package/src/runtime/http-server.ts +706 -291
- package/src/runtime/http-types.ts +26 -16
- package/src/runtime/routes/secret-routes.ts +57 -2
- package/src/runtime/routes/surface-action-routes.ts +66 -0
- package/src/runtime/routes/trust-rules-routes.ts +140 -0
- package/src/security/keychain-to-encrypted-migration.ts +59 -0
- package/src/security/secure-keys.ts +17 -0
- package/src/skills/frontmatter.ts +9 -7
- package/src/tools/apps/executors.ts +2 -1
- package/src/tools/tool-manifest.ts +44 -42
- package/src/tools/types.ts +9 -0
- package/src/__tests__/skill-mirror-parity.test.ts +0 -176
- package/src/config/vellum-skills/catalog.json +0 -63
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +0 -295
- package/src/skills/vellum-catalog-remote.ts +0 -166
- package/src/tools/skills/vellum-catalog.ts +0 -168
- /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/TOOLS.json +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/deploy-fullstack-vercel/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/document-writer/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/guardian-verify-setup/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/slack-oauth-setup/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/trusted-contacts/SKILL.md +0 -0
|
@@ -53,6 +53,8 @@ export interface EventHandlerState {
|
|
|
53
53
|
firstTextDeltaEmitted: boolean;
|
|
54
54
|
/** Tracks whether a thinking delta has been emitted this turn for activity state transitions. */
|
|
55
55
|
firstThinkingDeltaEmitted: boolean;
|
|
56
|
+
/** Name of the last completed tool, used to generate contextual statusText. */
|
|
57
|
+
lastCompletedToolName: string | undefined;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
/** Immutable context shared across event handlers within a single agent loop run. */
|
|
@@ -92,6 +94,7 @@ export function createEventHandlerState(): EventHandlerState {
|
|
|
92
94
|
currentTurnToolNames: [],
|
|
93
95
|
firstTextDeltaEmitted: false,
|
|
94
96
|
firstThinkingDeltaEmitted: false,
|
|
97
|
+
lastCompletedToolName: undefined,
|
|
95
98
|
};
|
|
96
99
|
}
|
|
97
100
|
|
|
@@ -130,6 +133,29 @@ function truncateForIpc(value: string, maxChars: number, suffix: string): string
|
|
|
130
133
|
return value.slice(0, maxChars - suffix.length) + suffix;
|
|
131
134
|
}
|
|
132
135
|
|
|
136
|
+
// ── Friendly Tool Names ──────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
const TOOL_FRIENDLY_NAMES: Record<string, string> = {
|
|
139
|
+
bash: 'command',
|
|
140
|
+
web_search: 'web search',
|
|
141
|
+
web_fetch: 'web fetch',
|
|
142
|
+
file_read: 'file read',
|
|
143
|
+
file_write: 'file write',
|
|
144
|
+
file_edit: 'file edit',
|
|
145
|
+
browser_navigate: 'browser',
|
|
146
|
+
browser_click: 'browser',
|
|
147
|
+
browser_type: 'browser',
|
|
148
|
+
browser_screenshot: 'browser',
|
|
149
|
+
browser_scroll: 'browser',
|
|
150
|
+
browser_wait: 'browser',
|
|
151
|
+
app_create: 'app',
|
|
152
|
+
app_update: 'app',
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
function friendlyToolName(name: string): string {
|
|
156
|
+
return TOOL_FRIENDLY_NAMES[name] ?? name.replace(/_/g, ' ');
|
|
157
|
+
}
|
|
158
|
+
|
|
133
159
|
// ── Individual Handlers ──────────────────────────────────────────────
|
|
134
160
|
|
|
135
161
|
export function handleTextDelta(
|
|
@@ -158,7 +184,18 @@ export function handleThinkingDelta(
|
|
|
158
184
|
): void {
|
|
159
185
|
if (!state.firstThinkingDeltaEmitted) {
|
|
160
186
|
state.firstThinkingDeltaEmitted = true;
|
|
161
|
-
|
|
187
|
+
const lastToolName = state.lastCompletedToolName;
|
|
188
|
+
// Only emit an activity state when a tool just completed, so we can
|
|
189
|
+
// show "Processing <tool> results". When no tool has completed yet
|
|
190
|
+
// (e.g. right after confirmation_resolved), skip the emission entirely
|
|
191
|
+
// so the client preserves its current status text (e.g. "Resuming
|
|
192
|
+
// after approval"). Even omitting statusText from the message would
|
|
193
|
+
// cause the client to clear it, since the client overwrites
|
|
194
|
+
// assistantStatusText for every assistant_activity_state event.
|
|
195
|
+
if (lastToolName) {
|
|
196
|
+
const statusText = `Processing ${friendlyToolName(lastToolName)} results`;
|
|
197
|
+
deps.ctx.emitActivityState('thinking', 'thinking_delta', 'assistant_turn', deps.reqId, statusText);
|
|
198
|
+
}
|
|
162
199
|
}
|
|
163
200
|
if (!deps.ctx.streamThinking) return;
|
|
164
201
|
emitLlmCallStartedIfNeeded(state, deps);
|
|
@@ -172,7 +209,8 @@ export function handleToolUse(
|
|
|
172
209
|
): void {
|
|
173
210
|
state.toolUseIdToName.set(event.id, event.name);
|
|
174
211
|
state.currentTurnToolNames.push(event.name);
|
|
175
|
-
|
|
212
|
+
const statusText = `Running ${friendlyToolName(event.name)}`;
|
|
213
|
+
deps.ctx.emitActivityState('tool_running', 'tool_use_start', 'assistant_turn', deps.reqId, statusText);
|
|
176
214
|
deps.onEvent({ type: 'tool_use_start', toolName: event.name, input: event.input, sessionId: deps.ctx.conversationId });
|
|
177
215
|
}
|
|
178
216
|
|
|
@@ -274,6 +312,9 @@ export function handleToolResult(
|
|
|
274
312
|
}
|
|
275
313
|
}
|
|
276
314
|
|
|
315
|
+
// Track last completed tool for contextual statusText on next thinking phase
|
|
316
|
+
state.lastCompletedToolName = state.toolUseIdToName.get(event.toolUseId);
|
|
317
|
+
|
|
277
318
|
// Reset so that the next LLM exchange (think → stream) after this tool
|
|
278
319
|
// call re-emits the activity state transitions.
|
|
279
320
|
state.firstTextDeltaEmitted = false;
|
|
@@ -98,6 +98,7 @@ export interface AgentLoopSessionContext {
|
|
|
98
98
|
currentPage?: string;
|
|
99
99
|
readonly surfaceState: Map<string, { surfaceType: SurfaceType; data: SurfaceData; title?: string }>;
|
|
100
100
|
pendingSurfaceActions: Map<string, { surfaceType: SurfaceType }>;
|
|
101
|
+
surfaceActionRequestIds: Set<string>;
|
|
101
102
|
currentTurnSurfaces: Array<{ surfaceId: string; surfaceType: SurfaceType; title?: string; data: SurfaceData; actions?: Array<{ id: string; label: string; style?: string }>; display?: string }>;
|
|
102
103
|
|
|
103
104
|
workingDir: string;
|
|
@@ -135,6 +136,7 @@ export interface AgentLoopSessionContext {
|
|
|
135
136
|
reason: 'message_dequeued' | 'thinking_delta' | 'first_text_delta' | 'tool_use_start' | 'confirmation_requested' | 'confirmation_resolved' | 'message_complete' | 'generation_cancelled' | 'error_terminal',
|
|
136
137
|
anchor?: 'assistant_turn' | 'user_turn' | 'global',
|
|
137
138
|
requestId?: string,
|
|
139
|
+
statusText?: string,
|
|
138
140
|
): void;
|
|
139
141
|
emitConfirmationStateChanged(params: import('./ipc-contract/messages.js').ConfirmationStateChanged extends { type: infer _ } ? Omit<import('./ipc-contract/messages.js').ConfirmationStateChanged, 'type'> : never): void;
|
|
140
142
|
|
|
@@ -839,6 +841,7 @@ export async function runAgentLoopImpl(
|
|
|
839
841
|
|
|
840
842
|
ctx.abortController = null;
|
|
841
843
|
ctx.processing = false;
|
|
844
|
+
ctx.surfaceActionRequestIds.delete(ctx.currentRequestId ?? '');
|
|
842
845
|
ctx.currentRequestId = undefined;
|
|
843
846
|
ctx.currentActiveSurfaceId = undefined;
|
|
844
847
|
ctx.allowedToolNames = undefined;
|
|
@@ -70,6 +70,7 @@ export interface AbortContext {
|
|
|
70
70
|
prompter: PermissionPrompter;
|
|
71
71
|
secretPrompter: SecretPrompter;
|
|
72
72
|
pendingSurfaceActions: Map<string, { surfaceType: SurfaceType }>;
|
|
73
|
+
surfaceActionRequestIds: Set<string>;
|
|
73
74
|
surfaceState: Map<string, { surfaceType: SurfaceType; data: SurfaceData; title?: string }>;
|
|
74
75
|
readonly queue: MessageQueue;
|
|
75
76
|
}
|
|
@@ -159,6 +160,7 @@ export function abortSession(ctx: AbortContext): void {
|
|
|
159
160
|
ctx.prompter.dispose();
|
|
160
161
|
ctx.secretPrompter.dispose();
|
|
161
162
|
ctx.pendingSurfaceActions.clear();
|
|
163
|
+
ctx.surfaceActionRequestIds.clear();
|
|
162
164
|
ctx.surfaceState.clear();
|
|
163
165
|
unregisterWatchNotifiers(ctx.conversationId);
|
|
164
166
|
for (const queued of ctx.queue) {
|
|
@@ -186,6 +188,7 @@ export function disposeSession(ctx: DisposeContext): void {
|
|
|
186
188
|
ctx.surfaceUndoStacks.clear();
|
|
187
189
|
ctx.currentTurnSurfaces = [];
|
|
188
190
|
ctx.pendingSurfaceActions.clear();
|
|
191
|
+
ctx.surfaceActionRequestIds.clear();
|
|
189
192
|
ctx.surfaceState.clear();
|
|
190
193
|
ctx.lastSurfaceAction.clear();
|
|
191
194
|
ctx.workspaceTopLevelContext = null;
|
|
@@ -92,6 +92,7 @@ export interface ProcessSessionContext {
|
|
|
92
92
|
reason: 'message_dequeued' | 'thinking_delta' | 'first_text_delta' | 'tool_use_start' | 'confirmation_requested' | 'confirmation_resolved' | 'message_complete' | 'generation_cancelled' | 'error_terminal',
|
|
93
93
|
anchor?: 'assistant_turn' | 'user_turn' | 'global',
|
|
94
94
|
requestId?: string,
|
|
95
|
+
statusText?: string,
|
|
95
96
|
): void;
|
|
96
97
|
}
|
|
97
98
|
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
findSeededHomeBaseApp,
|
|
5
5
|
getPrebuiltHomeBasePreview,
|
|
6
6
|
} from '../home-base/prebuilt/seed.js';
|
|
7
|
-
import { getApp, updateApp } from '../memory/app-store.js';
|
|
7
|
+
import { getApp, getAppPreview, updateApp } from '../memory/app-store.js';
|
|
8
8
|
import type { ToolExecutionResult } from '../tools/types.js';
|
|
9
9
|
import { getLogger } from '../util/logger.js';
|
|
10
10
|
import { isPlainObject } from '../util/object.js';
|
|
@@ -133,6 +133,8 @@ export interface SurfaceSessionContext {
|
|
|
133
133
|
lastSurfaceAction: Map<string, { actionId: string; data?: Record<string, unknown> }>;
|
|
134
134
|
surfaceState: Map<string, { surfaceType: SurfaceType; data: SurfaceData; title?: string }>;
|
|
135
135
|
surfaceUndoStacks: Map<string, string[]>;
|
|
136
|
+
/** Request IDs that originated from surface action button clicks (not regular user messages). */
|
|
137
|
+
surfaceActionRequestIds: Set<string>;
|
|
136
138
|
currentTurnSurfaces: Array<{
|
|
137
139
|
surfaceId: string;
|
|
138
140
|
surfaceType: SurfaceType;
|
|
@@ -393,24 +395,19 @@ export function handleSurfaceAction(ctx: SurfaceSessionContext, surfaceId: strin
|
|
|
393
395
|
? data.prompt.trim()
|
|
394
396
|
: '';
|
|
395
397
|
|
|
396
|
-
// Build a human-readable summary
|
|
397
|
-
//
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
fallbackContent
|
|
404
|
-
surfaceAction: true,
|
|
405
|
-
surfaceId,
|
|
406
|
-
surfaceType: pending.surfaceType,
|
|
407
|
-
actionId,
|
|
408
|
-
data: data ?? {},
|
|
409
|
-
});
|
|
398
|
+
// Build a human-readable summary so the LLM clearly understands the
|
|
399
|
+
// user's decision instead of parsing raw JSON.
|
|
400
|
+
const summary = buildCompletionSummary(pending.surfaceType, actionId, data);
|
|
401
|
+
let fallbackContent = `[User action on ${pending.surfaceType} surface: ${summary}]`;
|
|
402
|
+
// Append structured data so the LLM has access to IDs/values it needs
|
|
403
|
+
// to act on (e.g. selectedIds for archiving).
|
|
404
|
+
if (data && Object.keys(data).length > 0) {
|
|
405
|
+
fallbackContent += `\n\nAction data: ${JSON.stringify(data)}`;
|
|
410
406
|
}
|
|
411
407
|
const content = prompt || fallbackContent;
|
|
412
408
|
|
|
413
409
|
const requestId = uuid();
|
|
410
|
+
ctx.surfaceActionRequestIds.add(requestId);
|
|
414
411
|
const onEvent = (msg: ServerMessage) => ctx.sendToClient(msg);
|
|
415
412
|
|
|
416
413
|
// Echo the user's prompt to the client so it appears in the chat UI
|
|
@@ -534,13 +531,15 @@ export function buildCompletionSummary(surfaceType: string | undefined, actionId
|
|
|
534
531
|
}
|
|
535
532
|
if (surfaceType === 'list' && data) {
|
|
536
533
|
const selectedIds = data.selectedIds as string[] | undefined;
|
|
537
|
-
|
|
538
|
-
if (selectedIds?.length) return `Selected ${selectedIds
|
|
534
|
+
const actionSuffix = actionId ? ` (action: ${actionId})` : '';
|
|
535
|
+
if (selectedIds?.length === 1) return `Selected: ${selectedIds[0]}${actionSuffix}`;
|
|
536
|
+
if (selectedIds?.length) return `Selected ${selectedIds.length} items${actionSuffix}`;
|
|
539
537
|
}
|
|
540
538
|
if (surfaceType === 'table' && data) {
|
|
541
539
|
const selectedIds = data.selectedIds as string[] | undefined;
|
|
542
|
-
|
|
543
|
-
if (selectedIds?.length) return `Selected ${
|
|
540
|
+
const actionSuffix = actionId ? ` (action: ${actionId})` : '';
|
|
541
|
+
if (selectedIds?.length === 1) return `Selected 1 row${actionSuffix}`;
|
|
542
|
+
if (selectedIds?.length) return `Selected ${selectedIds.length} rows${actionSuffix}`;
|
|
544
543
|
}
|
|
545
544
|
return actionId.charAt(0).toUpperCase() + actionId.slice(1);
|
|
546
545
|
}
|
|
@@ -608,6 +607,7 @@ export async function surfaceProxyResolver(
|
|
|
608
607
|
message: 'File upload dialog displayed and the user can see it. The uploaded file data will arrive as a follow-up message. Do not output any waiting message — just stop here.',
|
|
609
608
|
}),
|
|
610
609
|
isError: false,
|
|
610
|
+
yieldToUser: true,
|
|
611
611
|
};
|
|
612
612
|
}
|
|
613
613
|
|
|
@@ -668,6 +668,7 @@ export async function surfaceProxyResolver(
|
|
|
668
668
|
message: 'Surface displayed and the user can see it. Their response will arrive as a follow-up message. Do not output any waiting message — just stop here.',
|
|
669
669
|
}),
|
|
670
670
|
isError: false,
|
|
671
|
+
yieldToUser: true,
|
|
671
672
|
};
|
|
672
673
|
}
|
|
673
674
|
return { content: JSON.stringify({ surfaceId }), isError: false };
|
|
@@ -772,11 +773,12 @@ export async function surfaceProxyResolver(
|
|
|
772
773
|
// un-clickable fallback chip) after session restart.
|
|
773
774
|
: { title: app.name, subtitle: app.description };
|
|
774
775
|
|
|
776
|
+
const storedPreview = getAppPreview(app.id);
|
|
775
777
|
const surfaceData: DynamicPageSurfaceData = {
|
|
776
778
|
html: app.htmlDefinition,
|
|
777
779
|
appId: app.id,
|
|
778
780
|
appType: app.appType,
|
|
779
|
-
preview: preview
|
|
781
|
+
preview: { ...defaultPreview, ...preview, ...(storedPreview ? { previewImage: storedPreview } : {}) },
|
|
780
782
|
};
|
|
781
783
|
const surfaceId = uuid();
|
|
782
784
|
ctx.surfaceState.set(surfaceId, {
|
|
@@ -113,6 +113,7 @@ export function createToolExecutor(
|
|
|
113
113
|
guardianTrustClass: ctx.guardianContext?.trustClass ?? 'guardian',
|
|
114
114
|
executionChannel: ctx.guardianContext?.sourceChannel,
|
|
115
115
|
callSessionId: ctx.callSessionId,
|
|
116
|
+
triggeredBySurfaceAction: ctx.surfaceActionRequestIds?.has(ctx.currentRequestId ?? '') ?? false,
|
|
116
117
|
requesterExternalUserId: ctx.guardianContext?.requesterExternalUserId,
|
|
117
118
|
requesterChatId: ctx.guardianContext?.requesterChatId,
|
|
118
119
|
onOutput,
|
package/src/daemon/session.ts
CHANGED
|
@@ -146,6 +146,7 @@ export class Session {
|
|
|
146
146
|
/** @internal */ voiceCallControlPrompt?: string;
|
|
147
147
|
/** @internal */ assistantId?: string;
|
|
148
148
|
/** @internal */ commandIntent?: { type: string; payload?: string; languageCode?: string };
|
|
149
|
+
/** @internal */ surfaceActionRequestIds = new Set<string>();
|
|
149
150
|
/** @internal */ pendingSurfaceActions = new Map<string, { surfaceType: SurfaceType }>();
|
|
150
151
|
/** @internal */ lastSurfaceAction = new Map<string, { actionId: string; data?: Record<string, unknown> }>();
|
|
151
152
|
/** @internal */ surfaceState = new Map<string, { surfaceType: SurfaceType; data: SurfaceData; title?: string }>();
|
|
@@ -205,7 +206,7 @@ export class Session {
|
|
|
205
206
|
if (state === 'pending') {
|
|
206
207
|
this.emitActivityState('awaiting_confirmation', 'confirmation_requested', 'assistant_turn');
|
|
207
208
|
} else if (state === 'timed_out') {
|
|
208
|
-
this.emitActivityState('thinking', 'confirmation_resolved', 'assistant_turn');
|
|
209
|
+
this.emitActivityState('thinking', 'confirmation_resolved', 'assistant_turn', undefined, 'Resuming after timeout');
|
|
209
210
|
}
|
|
210
211
|
});
|
|
211
212
|
this.secretPrompter = new SecretPrompter(sendToClient);
|
|
@@ -520,7 +521,7 @@ export class Session {
|
|
|
520
521
|
...(emissionContext?.causedByRequestId ? { causedByRequestId: emissionContext.causedByRequestId } : {}),
|
|
521
522
|
...(emissionContext?.decisionText ? { decisionText: emissionContext.decisionText } : {}),
|
|
522
523
|
});
|
|
523
|
-
this.emitActivityState('thinking', 'confirmation_resolved', 'assistant_turn');
|
|
524
|
+
this.emitActivityState('thinking', 'confirmation_resolved', 'assistant_turn', undefined, 'Resuming after approval');
|
|
524
525
|
}
|
|
525
526
|
|
|
526
527
|
handleSecretResponse(requestId: string, value?: string, delivery?: 'store' | 'transient_send'): void {
|
|
@@ -540,6 +541,7 @@ export class Session {
|
|
|
540
541
|
reason: AssistantActivityState['reason'],
|
|
541
542
|
anchor: AssistantActivityState['anchor'] = 'assistant_turn',
|
|
542
543
|
requestId?: string,
|
|
544
|
+
statusText?: string,
|
|
543
545
|
): void {
|
|
544
546
|
this.activityVersion++;
|
|
545
547
|
const msg: ServerMessage = {
|
|
@@ -550,6 +552,7 @@ export class Session {
|
|
|
550
552
|
anchor,
|
|
551
553
|
requestId,
|
|
552
554
|
reason,
|
|
555
|
+
...(statusText ? { statusText } : {}),
|
|
553
556
|
} as ServerMessage;
|
|
554
557
|
this.sendToClient(msg);
|
|
555
558
|
this.onStateSignal?.(msg);
|
|
@@ -12,6 +12,7 @@ const log = getLogger('outreach-classifier');
|
|
|
12
12
|
const MODEL_INTENT = 'latency-optimized' as const;
|
|
13
13
|
const TIMEOUT_MS = 30_000;
|
|
14
14
|
const BATCH_SIZE = 100;
|
|
15
|
+
const CLASSIFY_CONCURRENCY = 3;
|
|
15
16
|
|
|
16
17
|
export type OutreachType = 'sales' | 'recruiting' | 'marketing' | 'other';
|
|
17
18
|
|
|
@@ -128,12 +129,18 @@ async function classifyBatch(emails: EmailMetadata[]): Promise<OutreachClassific
|
|
|
128
129
|
export async function classifyOutreach(emails: EmailMetadata[]): Promise<OutreachClassification[]> {
|
|
129
130
|
if (emails.length === 0) return [];
|
|
130
131
|
|
|
131
|
-
|
|
132
|
-
|
|
132
|
+
// Split into batches
|
|
133
|
+
const batches: EmailMetadata[][] = [];
|
|
133
134
|
for (let i = 0; i < emails.length; i += BATCH_SIZE) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
batches.push(emails.slice(i, i + BATCH_SIZE));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Process batches with bounded concurrency
|
|
139
|
+
const results: OutreachClassification[] = [];
|
|
140
|
+
for (let i = 0; i < batches.length; i += CLASSIFY_CONCURRENCY) {
|
|
141
|
+
const concurrentBatches = batches.slice(i, i + CLASSIFY_CONCURRENCY);
|
|
142
|
+
const batchResults = await Promise.all(concurrentBatches.map((batch) => classifyBatch(batch)));
|
|
143
|
+
for (const r of batchResults) results.push(...r);
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
return results;
|
|
@@ -10,6 +10,7 @@ export interface Conversation {
|
|
|
10
10
|
memberCount?: number;
|
|
11
11
|
topic?: string;
|
|
12
12
|
isArchived?: boolean;
|
|
13
|
+
isPrivate?: boolean;
|
|
13
14
|
metadata?: Record<string, unknown>;
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -99,6 +100,10 @@ export interface SenderDigestResult {
|
|
|
99
100
|
senders: SenderDigestEntry[];
|
|
100
101
|
totalScanned: number;
|
|
101
102
|
queryUsed: string;
|
|
103
|
+
/** True when pagination was stopped because the scan cap was reached but more messages exist. */
|
|
104
|
+
truncated?: boolean;
|
|
105
|
+
/** Resume token for sequential scanning — present when truncated is true. */
|
|
106
|
+
nextPageToken?: string;
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
export interface ArchiveResult {
|
|
@@ -41,7 +41,7 @@ export interface MessagingProvider {
|
|
|
41
41
|
markRead?(token: string, conversationId: string, messageId?: string): Promise<void>;
|
|
42
42
|
|
|
43
43
|
/** Scan messages and group by sender for bulk cleanup (e.g. newsletter decluttering). */
|
|
44
|
-
senderDigest?(token: string, query: string, options?: { maxMessages?: number; maxSenders?: number }): Promise<SenderDigestResult>;
|
|
44
|
+
senderDigest?(token: string, query: string, options?: { maxMessages?: number; maxSenders?: number; pageToken?: string }): Promise<SenderDigestResult>;
|
|
45
45
|
/** Archive messages matching a search query. */
|
|
46
46
|
archiveByQuery?(token: string, query: string): Promise<ArchiveResult>;
|
|
47
47
|
|
|
@@ -195,13 +195,14 @@ export const gmailMessagingProvider: MessagingProvider = {
|
|
|
195
195
|
await gmail.modifyMessage(token, messageId, { removeLabelIds: ['UNREAD'] });
|
|
196
196
|
},
|
|
197
197
|
|
|
198
|
-
async senderDigest(token: string, query: string, options?: { maxMessages?: number; maxSenders?: number }): Promise<SenderDigestResult> {
|
|
199
|
-
const maxMessages = Math.min(options?.maxMessages ??
|
|
198
|
+
async senderDigest(token: string, query: string, options?: { maxMessages?: number; maxSenders?: number; pageToken?: string }): Promise<SenderDigestResult> {
|
|
199
|
+
const maxMessages = Math.min(options?.maxMessages ?? 2000, 2000);
|
|
200
200
|
const maxSenders = options?.maxSenders ?? 30;
|
|
201
|
-
const maxIdsPerSender =
|
|
201
|
+
const maxIdsPerSender = 2000;
|
|
202
202
|
|
|
203
203
|
const allMessageIds: string[] = [];
|
|
204
|
-
let pageToken: string | undefined;
|
|
204
|
+
let pageToken: string | undefined = options?.pageToken;
|
|
205
|
+
let truncated = false;
|
|
205
206
|
|
|
206
207
|
while (allMessageIds.length < maxMessages) {
|
|
207
208
|
const pageSize = Math.min(100, maxMessages - allMessageIds.length);
|
|
@@ -213,6 +214,11 @@ export const gmailMessagingProvider: MessagingProvider = {
|
|
|
213
214
|
if (!pageToken) break;
|
|
214
215
|
}
|
|
215
216
|
|
|
217
|
+
// If we stopped because we hit the cap but there were still more pages, flag truncation
|
|
218
|
+
if (allMessageIds.length >= maxMessages && pageToken) {
|
|
219
|
+
truncated = true;
|
|
220
|
+
}
|
|
221
|
+
|
|
216
222
|
if (allMessageIds.length === 0) {
|
|
217
223
|
return { senders: [], totalScanned: 0, queryUsed: query };
|
|
218
224
|
}
|
|
@@ -283,7 +289,7 @@ export const gmailMessagingProvider: MessagingProvider = {
|
|
|
283
289
|
hasMore: s.hasMore,
|
|
284
290
|
}));
|
|
285
291
|
|
|
286
|
-
return { senders, totalScanned: allMessageIds.length, queryUsed: query };
|
|
292
|
+
return { senders, totalScanned: allMessageIds.length, queryUsed: query, ...(truncated ? { truncated, nextPageToken: pageToken } : {}) };
|
|
287
293
|
},
|
|
288
294
|
|
|
289
295
|
async archiveByQuery(token: string, query: string): Promise<ArchiveResult> {
|
|
@@ -33,6 +33,7 @@ export class GmailApiError extends Error {
|
|
|
33
33
|
|
|
34
34
|
const MAX_RETRIES = 3;
|
|
35
35
|
const INITIAL_BACKOFF_MS = 1000;
|
|
36
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
36
37
|
|
|
37
38
|
function isRetryable(status: number): boolean {
|
|
38
39
|
return status === 429 || (status >= 500 && status < 600);
|
|
@@ -57,6 +58,7 @@ async function request<T>(token: string, path: string, options?: GmailRequestOpt
|
|
|
57
58
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
58
59
|
const resp = await fetch(url, {
|
|
59
60
|
...options,
|
|
61
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
60
62
|
headers: {
|
|
61
63
|
Authorization: `Bearer ${token}`,
|
|
62
64
|
'Content-Type': 'application/json',
|
|
@@ -60,6 +60,7 @@ function mapConversation(conv: SlackConversation): Conversation {
|
|
|
60
60
|
memberCount: conv.num_members,
|
|
61
61
|
topic: conv.topic?.value || undefined,
|
|
62
62
|
isArchived: conv.is_archived,
|
|
63
|
+
isPrivate: conv.is_private ?? conv.is_group ?? false,
|
|
63
64
|
metadata: conv.is_im ? { dmUserId: conv.user } : undefined,
|
|
64
65
|
};
|
|
65
66
|
}
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
SlackAuthTestResponse,
|
|
11
11
|
SlackChatDeleteResponse,
|
|
12
12
|
SlackConversationHistoryResponse,
|
|
13
|
+
SlackConversationInfoResponse,
|
|
13
14
|
SlackConversationLeaveResponse,
|
|
14
15
|
SlackConversationMarkResponse,
|
|
15
16
|
SlackConversationRepliesResponse,
|
|
@@ -95,6 +96,13 @@ export async function listConversations(
|
|
|
95
96
|
});
|
|
96
97
|
}
|
|
97
98
|
|
|
99
|
+
export async function conversationInfo(
|
|
100
|
+
token: string,
|
|
101
|
+
channel: string,
|
|
102
|
+
): Promise<SlackConversationInfoResponse> {
|
|
103
|
+
return request<SlackConversationInfoResponse>(token, 'conversations.info', { channel });
|
|
104
|
+
}
|
|
105
|
+
|
|
98
106
|
export async function conversationHistory(
|
|
99
107
|
token: string,
|
|
100
108
|
channel: string,
|
|
@@ -20,6 +20,7 @@ export interface SlackConversation {
|
|
|
20
20
|
is_group?: boolean;
|
|
21
21
|
is_im?: boolean;
|
|
22
22
|
is_mpim?: boolean;
|
|
23
|
+
is_private?: boolean;
|
|
23
24
|
is_archived?: boolean;
|
|
24
25
|
is_member?: boolean;
|
|
25
26
|
topic?: { value: string };
|
|
@@ -108,6 +109,10 @@ export interface SlackSearchMatch {
|
|
|
108
109
|
thread_ts?: string;
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
export interface SlackConversationInfoResponse extends SlackApiResponse {
|
|
113
|
+
channel: SlackConversation;
|
|
114
|
+
}
|
|
115
|
+
|
|
111
116
|
export interface SlackConversationsOpenResponse extends SlackApiResponse {
|
|
112
117
|
channel: { id: string };
|
|
113
118
|
}
|
|
@@ -16,16 +16,17 @@
|
|
|
16
16
|
* internal assistant-layer errors.
|
|
17
17
|
*/
|
|
18
18
|
export type HttpErrorCode =
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
|
19
|
+
| "BAD_REQUEST"
|
|
20
|
+
| "UNAUTHORIZED"
|
|
21
|
+
| "FORBIDDEN"
|
|
22
|
+
| "NOT_FOUND"
|
|
23
|
+
| "CONFLICT"
|
|
24
|
+
| "GONE"
|
|
25
|
+
| "RATE_LIMITED"
|
|
26
|
+
| "UNPROCESSABLE_ENTITY"
|
|
27
|
+
| "INTERNAL_ERROR"
|
|
28
|
+
| "NOT_IMPLEMENTED"
|
|
29
|
+
| "SERVICE_UNAVAILABLE";
|
|
29
30
|
|
|
30
31
|
// ── Response type ────────────────────────────────────────────────────────────
|
|
31
32
|
|
|
@@ -79,15 +80,27 @@ export function httpError(
|
|
|
79
80
|
*/
|
|
80
81
|
export function httpErrorCodeFromStatus(status: number): HttpErrorCode {
|
|
81
82
|
switch (status) {
|
|
82
|
-
case 400:
|
|
83
|
-
|
|
84
|
-
case
|
|
85
|
-
|
|
86
|
-
case
|
|
87
|
-
|
|
88
|
-
case
|
|
89
|
-
|
|
90
|
-
case
|
|
91
|
-
|
|
83
|
+
case 400:
|
|
84
|
+
return "BAD_REQUEST";
|
|
85
|
+
case 401:
|
|
86
|
+
return "UNAUTHORIZED";
|
|
87
|
+
case 403:
|
|
88
|
+
return "FORBIDDEN";
|
|
89
|
+
case 404:
|
|
90
|
+
return "NOT_FOUND";
|
|
91
|
+
case 409:
|
|
92
|
+
return "CONFLICT";
|
|
93
|
+
case 410:
|
|
94
|
+
return "GONE";
|
|
95
|
+
case 422:
|
|
96
|
+
return "UNPROCESSABLE_ENTITY";
|
|
97
|
+
case 429:
|
|
98
|
+
return "RATE_LIMITED";
|
|
99
|
+
case 501:
|
|
100
|
+
return "NOT_IMPLEMENTED";
|
|
101
|
+
case 503:
|
|
102
|
+
return "SERVICE_UNAVAILABLE";
|
|
103
|
+
default:
|
|
104
|
+
return "INTERNAL_ERROR";
|
|
92
105
|
}
|
|
93
106
|
}
|