@vellumai/assistant 0.4.35 → 0.4.37
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/AGENTS.md +1 -1
- package/ARCHITECTURE.md +44 -49
- package/README.md +32 -20
- package/docs/architecture/keychain-broker.md +186 -0
- package/docs/architecture/security.md +110 -116
- package/docs/runbook-trusted-contacts.md +2 -2
- package/docs/skills.md +25 -25
- package/package.json +5 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +11 -2
- package/src/__tests__/actor-token-service.test.ts +1 -0
- package/src/__tests__/amazon-cdp-integration.test.ts +74 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +38 -9
- package/src/__tests__/assistant-id-boundary-guard.test.ts +29 -0
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/bundle-scanner.test.ts +1 -1
- package/src/__tests__/channel-guardian.test.ts +102 -102
- package/src/__tests__/channel-invite-transport.test.ts +155 -256
- package/src/__tests__/channel-readiness-routes.test.ts +336 -0
- package/src/__tests__/checker.test.ts +6 -6
- package/src/__tests__/chrome-cdp.test.ts +350 -0
- package/src/__tests__/computer-use-session-lifecycle.test.ts +3 -3
- package/src/__tests__/computer-use-session-working-dir.test.ts +86 -52
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +1 -1
- package/src/__tests__/config-loader-migration.test.ts +85 -0
- package/src/__tests__/conversation-pairing.test.ts +370 -5
- package/src/__tests__/credential-broker-browser-fill.test.ts +1 -10
- package/src/__tests__/credential-broker-server-use.test.ts +1 -10
- package/src/__tests__/credential-security-e2e.test.ts +7 -1
- package/src/__tests__/credential-security-invariants.test.ts +14 -20
- package/src/__tests__/credential-vault-unit.test.ts +1 -11
- package/src/__tests__/credential-vault.test.ts +5 -19
- package/src/__tests__/credentials-cli.test.ts +814 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +23 -4
- package/src/__tests__/email-invite-adapter.test.ts +78 -0
- package/src/__tests__/email-service-config-fallback.test.ts +102 -0
- package/src/__tests__/encrypted-store.test.ts +6 -6
- package/src/__tests__/ephemeral-permissions.test.ts +3 -3
- package/src/__tests__/gateway-only-enforcement.test.ts +5 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +70 -12
- package/src/__tests__/guardian-outbound-http.test.ts +53 -47
- package/src/__tests__/handle-user-message-secret-resume.test.ts +23 -0
- package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +32 -23
- package/src/__tests__/handlers-telegram-config.test.ts +8 -2
- package/src/__tests__/handlers-twitter-config.test.ts +2 -2
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +108 -7
- package/src/__tests__/ingress-reconcile.test.ts +6 -0
- package/src/__tests__/intent-routing.test.ts +23 -4
- package/src/__tests__/invite-routes-http.test.ts +12 -0
- package/src/__tests__/ipc-snapshot.test.ts +8 -2
- package/src/__tests__/keychain-broker-client.test.ts +543 -0
- package/src/__tests__/llm-usage-store.test.ts +344 -0
- package/src/__tests__/mcp-client-auth.test.ts +2 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +1 -1
- package/src/__tests__/migration-transport.test.ts +49 -0
- package/src/__tests__/notification-broadcaster.test.ts +205 -5
- package/src/__tests__/notification-deep-link.test.ts +365 -1
- package/src/__tests__/oauth-connect-handler.test.ts +2 -2
- package/src/__tests__/onboarding-starter-tasks.test.ts +17 -4
- package/src/__tests__/proxy-approval-callback.test.ts +1 -1
- package/src/__tests__/recording-handler.test.ts +1 -1
- package/src/__tests__/recording-intent-handler.test.ts +6 -1
- package/src/__tests__/recording-state-machine.test.ts +1 -1
- package/src/__tests__/relay-server.test.ts +9 -1
- package/src/__tests__/ride-shotgun-handler.test.ts +499 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +160 -1
- package/src/__tests__/script-proxy-injection-runtime.test.ts +299 -2
- package/src/__tests__/script-proxy-profile-template-fallback.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +8 -2
- package/src/__tests__/secure-keys.test.ts +175 -216
- package/src/__tests__/session-confirmation-signals.test.ts +1 -1
- package/src/__tests__/session-messaging-secret-redirect.test.ts +1 -1
- package/src/__tests__/session-queue.test.ts +2 -1
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +2 -2
- package/src/__tests__/skill-feature-flags-integration.test.ts +29 -4
- package/src/__tests__/skill-feature-flags.test.ts +12 -9
- package/src/__tests__/skill-load-feature-flag.test.ts +26 -5
- package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
- package/src/__tests__/skills.test.ts +34 -4
- package/src/__tests__/slack-channel-config.test.ts +2 -2
- package/src/__tests__/system-prompt.test.ts +26 -4
- package/src/__tests__/telegram-bot-username-resolution.test.ts +212 -0
- package/src/__tests__/telegram-invite-adapter.test.ts +164 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
- package/src/__tests__/tool-permission-simulate-handler.test.ts +8 -2
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +9 -1
- package/src/__tests__/twitter-auth-handler.test.ts +2 -2
- package/src/__tests__/twitter-oauth-client.test.ts +1 -1
- package/src/__tests__/usage-routes.test.ts +339 -0
- package/src/__tests__/whatsapp-invite-adapter.test.ts +94 -0
- package/src/agent/loop.ts +3 -0
- package/src/amazon/checkout.ts +0 -1
- package/src/approvals/guardian-request-resolvers.ts +9 -1
- package/src/bundler/app-bundler.ts +28 -12
- package/src/bundler/bundle-scanner.ts +1 -1
- package/src/bundler/bundle-signer.ts +3 -3
- package/src/bundler/manifest.ts +1 -1
- package/src/bundler/signature-verifier.ts +3 -3
- package/src/channels/config.ts +1 -1
- package/src/cli/AGENTS.md +63 -0
- package/src/cli/__tests__/notifications.test.ts +470 -0
- package/src/cli/amazon.ts +344 -167
- package/src/cli/audit.ts +85 -0
- package/src/cli/autonomy.ts +369 -0
- package/src/cli/channels.ts +51 -0
- package/src/cli/completions.ts +208 -0
- package/src/cli/config.ts +220 -0
- package/src/cli/contacts.ts +471 -0
- package/src/cli/credentials.ts +564 -0
- package/src/cli/default-action.ts +14 -0
- package/src/cli/dev.ts +131 -0
- package/src/cli/doctor.ts +398 -0
- package/src/cli/email.ts +494 -0
- package/src/cli/influencer.ts +72 -0
- package/src/cli/integrations.ts +248 -57
- package/src/cli/keys.ts +114 -0
- package/src/cli/map.ts +46 -54
- package/src/cli/mcp.ts +111 -3
- package/src/cli/{config-commands.ts → memory.ts} +134 -245
- package/src/cli/notifications.ts +407 -0
- package/src/cli/program.ts +65 -0
- package/src/cli/reference.ts +48 -0
- package/src/cli/sequence.ts +154 -0
- package/src/cli/sessions.ts +262 -0
- package/src/cli/trust.ts +175 -0
- package/src/cli/twitter.ts +323 -106
- package/src/config/__tests__/build-cli-reference-section.test.ts +49 -0
- package/src/config/bundled-skills/amazon/SKILL.md +2 -2
- package/src/config/bundled-skills/app-builder/TOOLS.json +26 -0
- package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +13 -0
- package/src/config/bundled-skills/contacts/SKILL.md +178 -10
- package/src/config/bundled-skills/doordash/doordash-cli.ts +23 -168
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +135 -34
- package/src/config/bundled-skills/messaging/tools/shared.ts +4 -1
- package/src/config/bundled-skills/twilio-setup/SKILL.md +70 -17
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/core-schema.ts +7 -0
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/loader.ts +26 -0
- package/src/config/schema.ts +4 -0
- package/src/config/skill-state.ts +0 -13
- package/src/config/system-prompt.ts +27 -0
- package/src/contacts/contact-store.ts +25 -0
- package/src/daemon/computer-use-session.ts +1 -1
- package/src/daemon/handlers/apps.ts +1 -0
- package/src/daemon/handlers/config-channels.ts +3 -3
- package/src/daemon/handlers/config-dispatch.ts +29 -0
- package/src/daemon/handlers/config-inbox.ts +4 -3
- package/src/daemon/handlers/config.ts +3 -43
- package/src/daemon/handlers/contacts.ts +34 -0
- package/src/daemon/handlers/index.ts +17 -3
- package/src/daemon/handlers/session-user-message.ts +7 -0
- package/src/daemon/handlers/sessions.ts +21 -2
- package/src/daemon/handlers/shared.ts +17 -0
- package/src/daemon/ipc-contract/apps.ts +2 -0
- package/src/daemon/ipc-contract/computer-use.ts +9 -0
- package/src/daemon/ipc-contract/contacts.ts +3 -3
- package/src/daemon/ipc-contract/inbox.ts +2 -0
- package/src/daemon/ipc-contract/messages.ts +4 -0
- package/src/daemon/ipc-contract/sessions.ts +8 -0
- package/src/daemon/ipc-contract-inventory.json +1 -0
- package/src/daemon/lifecycle.ts +0 -5
- package/src/daemon/ride-shotgun-handler.ts +139 -25
- package/src/daemon/session-agent-loop-handlers.ts +100 -0
- package/src/daemon/session-agent-loop.ts +72 -0
- package/src/daemon/session-tool-setup.ts +7 -0
- package/src/daemon/session.ts +23 -1
- package/src/daemon/tool-side-effects.ts +39 -1
- package/src/email/service.ts +59 -2
- package/src/index.ts +2 -60
- package/src/mcp/mcp-oauth-provider.ts +90 -8
- package/src/media/app-icon-generator.ts +86 -0
- package/src/memory/db-init.ts +11 -0
- package/src/memory/llm-usage-store.ts +186 -0
- package/src/memory/migrations/137-usage-dashboard-indexes.ts +26 -0
- package/src/memory/migrations/139-drop-usage-composite-indexes.ts +30 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/schema-migration.ts +1 -0
- package/src/memory/shared-app-links-store.ts +1 -1
- package/src/messaging/registry.ts +27 -0
- package/src/notifications/README.md +79 -70
- package/src/notifications/broadcaster.ts +2 -1
- package/src/notifications/conversation-pairing.ts +147 -13
- package/src/notifications/copy-composer.ts +7 -3
- package/src/notifications/destination-resolver.ts +14 -1
- package/src/notifications/emit-signal.ts +3 -2
- package/src/notifications/signal.ts +105 -1
- package/src/notifications/types.ts +16 -0
- package/src/permissions/checker.ts +29 -3
- package/src/permissions/prompter.ts +11 -3
- package/src/runtime/access-request-helper.ts +2 -1
- package/src/runtime/auth/route-policy.ts +7 -1
- package/src/runtime/channel-invite-transport.ts +40 -63
- package/src/runtime/channel-invite-transports/email.ts +13 -39
- package/src/runtime/channel-invite-transports/slack.ts +5 -34
- package/src/runtime/channel-invite-transports/sms.ts +8 -29
- package/src/runtime/channel-invite-transports/telegram.ts +69 -28
- package/src/runtime/channel-invite-transports/voice.ts +0 -7
- package/src/runtime/channel-invite-transports/whatsapp.ts +43 -0
- package/src/runtime/channel-readiness-service.ts +202 -45
- package/src/runtime/confirmation-request-guardian-bridge.ts +2 -1
- package/src/runtime/guardian-outbound-actions.ts +8 -5
- package/src/runtime/http-server.ts +2 -0
- package/src/runtime/invite-instruction-generator.ts +178 -0
- package/src/runtime/invite-service.ts +22 -25
- package/src/runtime/migrations/migration-transport.ts +13 -0
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +8 -7
- package/src/runtime/routes/channel-readiness-routes.ts +30 -11
- package/src/runtime/routes/contact-routes.ts +54 -26
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -1
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +2 -1
- package/src/runtime/routes/inbound-stages/verification-intercept.ts +2 -1
- package/src/runtime/routes/integration-routes.ts +1 -1
- package/src/runtime/routes/invite-routes.ts +1 -1
- package/src/runtime/routes/secret-routes.ts +31 -7
- package/src/runtime/routes/twilio-routes.ts +32 -1
- package/src/runtime/routes/usage-routes.ts +114 -0
- package/src/runtime/tool-grant-request-helper.ts +2 -1
- package/src/security/encrypted-store.ts +9 -5
- package/src/security/keychain-broker-client.ts +393 -0
- package/src/security/secure-keys.ts +106 -321
- package/src/tools/apps/executors.ts +73 -0
- package/src/tools/browser/auto-navigate.ts +15 -6
- package/src/tools/browser/chrome-cdp.ts +211 -0
- package/src/tools/browser/network-recorder.test.ts +83 -0
- package/src/tools/browser/network-recorder.ts +8 -7
- package/src/tools/browser/x-auto-navigate.ts +12 -6
- package/src/tools/credentials/policy-types.ts +24 -0
- package/src/tools/credentials/vault.ts +22 -27
- package/src/tools/network/script-proxy/session-manager.ts +47 -3
- package/src/tools/permission-checker.ts +1 -0
- package/src/tools/types.ts +2 -0
- package/src/tools/ui-surface/definitions.ts +1 -2
- package/src/tools/watch/watch-state.ts +2 -0
- package/src/__tests__/key-migration.test.ts +0 -240
- package/src/__tests__/keychain.test.ts +0 -286
- package/src/cli/core-commands.ts +0 -899
- package/src/security/keychain-to-encrypted-migration.ts +0 -66
- package/src/security/keychain.ts +0 -490
|
@@ -67,6 +67,22 @@ export interface EventHandlerState {
|
|
|
67
67
|
firstThinkingDeltaEmitted: boolean;
|
|
68
68
|
/** Name of the last completed tool, used to generate contextual statusText. */
|
|
69
69
|
lastCompletedToolName: string | undefined;
|
|
70
|
+
/** Tracks tool_use_id → timing data for persisting on content blocks. */
|
|
71
|
+
readonly toolCallTimestamps: Map<
|
|
72
|
+
string,
|
|
73
|
+
{ startedAt: number; completedAt?: number }
|
|
74
|
+
>;
|
|
75
|
+
/** The tool_use_id of the currently executing tool (set in handleToolUse, cleared in handleToolResult). */
|
|
76
|
+
currentToolUseId: string | undefined;
|
|
77
|
+
/** Maps confirmation requestId → tool_use_id for linking decisions to tools. */
|
|
78
|
+
readonly requestIdToToolUseId: Map<string, string>;
|
|
79
|
+
/** Stores confirmation outcomes keyed by tool_use_id. */
|
|
80
|
+
readonly toolConfirmationOutcomes: Map<
|
|
81
|
+
string,
|
|
82
|
+
{ decision: string; label: string }
|
|
83
|
+
>;
|
|
84
|
+
/** tool_use_ids emitted in the current turn (populated in handleToolUse, cleared after annotation). */
|
|
85
|
+
currentTurnToolUseIds: string[];
|
|
70
86
|
}
|
|
71
87
|
|
|
72
88
|
/** Immutable context shared across event handlers within a single agent loop run. */
|
|
@@ -108,6 +124,11 @@ export function createEventHandlerState(): EventHandlerState {
|
|
|
108
124
|
firstTextDeltaEmitted: false,
|
|
109
125
|
firstThinkingDeltaEmitted: false,
|
|
110
126
|
lastCompletedToolName: undefined,
|
|
127
|
+
toolCallTimestamps: new Map(),
|
|
128
|
+
currentToolUseId: undefined,
|
|
129
|
+
requestIdToToolUseId: new Map(),
|
|
130
|
+
toolConfirmationOutcomes: new Map(),
|
|
131
|
+
currentTurnToolUseIds: [],
|
|
111
132
|
};
|
|
112
133
|
}
|
|
113
134
|
|
|
@@ -253,6 +274,9 @@ export function handleToolUse(
|
|
|
253
274
|
): void {
|
|
254
275
|
state.toolUseIdToName.set(event.id, event.name);
|
|
255
276
|
state.currentTurnToolNames.push(event.name);
|
|
277
|
+
state.toolCallTimestamps.set(event.id, { startedAt: Date.now() });
|
|
278
|
+
state.currentToolUseId = event.id;
|
|
279
|
+
state.currentTurnToolUseIds.push(event.id);
|
|
256
280
|
const statusText = `Running ${friendlyToolName(event.name)}`;
|
|
257
281
|
deps.ctx.emitActivityState(
|
|
258
282
|
"tool_running",
|
|
@@ -266,6 +290,7 @@ export function handleToolUse(
|
|
|
266
290
|
toolName: event.name,
|
|
267
291
|
input: event.input,
|
|
268
292
|
sessionId: deps.ctx.conversationId,
|
|
293
|
+
toolUseId: event.id,
|
|
269
294
|
});
|
|
270
295
|
}
|
|
271
296
|
|
|
@@ -392,6 +417,11 @@ export function handleToolResult(
|
|
|
392
417
|
contentBlocks: event.contentBlocks,
|
|
393
418
|
});
|
|
394
419
|
|
|
420
|
+
// Record tool completion timestamp
|
|
421
|
+
const ts = state.toolCallTimestamps.get(event.toolUseId);
|
|
422
|
+
if (ts) ts.completedAt = Date.now();
|
|
423
|
+
state.currentToolUseId = undefined;
|
|
424
|
+
|
|
395
425
|
const toolName = state.toolUseIdToName.get(event.toolUseId);
|
|
396
426
|
if (toolName === "file_write" || toolName === "bash") {
|
|
397
427
|
deps.ctx.markWorkspaceTopLevelDirty();
|
|
@@ -433,6 +463,68 @@ export function handleToolResult(
|
|
|
433
463
|
deps.reqId,
|
|
434
464
|
statusText,
|
|
435
465
|
);
|
|
466
|
+
|
|
467
|
+
// Once all tools for this turn have completed, annotate the persisted
|
|
468
|
+
// assistant message with timing and confirmation metadata.
|
|
469
|
+
const allToolsDone = state.currentTurnToolUseIds.every((id) => {
|
|
470
|
+
const ts = state.toolCallTimestamps.get(id);
|
|
471
|
+
return ts && ts.completedAt != null;
|
|
472
|
+
});
|
|
473
|
+
if (allToolsDone && state.currentTurnToolUseIds.length > 0) {
|
|
474
|
+
annotatePersistedAssistantMessage(state);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* After all tools for the current turn complete, fetch the persisted assistant
|
|
480
|
+
* message, annotate its tool_use blocks with timing and confirmation metadata,
|
|
481
|
+
* and update the DB. This runs post-tool-execution so the metadata maps are
|
|
482
|
+
* fully populated (unlike message_complete which fires before tools run).
|
|
483
|
+
*/
|
|
484
|
+
function annotatePersistedAssistantMessage(state: EventHandlerState): void {
|
|
485
|
+
const messageId = state.lastAssistantMessageId;
|
|
486
|
+
if (!messageId) return;
|
|
487
|
+
|
|
488
|
+
const row = conversationStore.getMessageById(messageId);
|
|
489
|
+
if (!row) return;
|
|
490
|
+
|
|
491
|
+
let content: ContentBlock[];
|
|
492
|
+
try {
|
|
493
|
+
content = JSON.parse(row.content) as ContentBlock[];
|
|
494
|
+
} catch {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
let modified = false;
|
|
499
|
+
for (const block of content) {
|
|
500
|
+
if (block.type === "tool_use") {
|
|
501
|
+
const rec = block as unknown as Record<string, unknown>;
|
|
502
|
+
const id = rec.id as string | undefined;
|
|
503
|
+
if (!id) continue;
|
|
504
|
+
|
|
505
|
+
const ts = state.toolCallTimestamps.get(id);
|
|
506
|
+
if (ts) {
|
|
507
|
+
rec._startedAt = ts.startedAt;
|
|
508
|
+
if (ts.completedAt != null) {
|
|
509
|
+
rec._completedAt = ts.completedAt;
|
|
510
|
+
}
|
|
511
|
+
modified = true;
|
|
512
|
+
}
|
|
513
|
+
const confirmation = state.toolConfirmationOutcomes.get(id);
|
|
514
|
+
if (confirmation) {
|
|
515
|
+
rec._confirmationDecision = confirmation.decision;
|
|
516
|
+
rec._confirmationLabel = confirmation.label;
|
|
517
|
+
modified = true;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (modified) {
|
|
523
|
+
conversationStore.updateMessageContent(messageId, JSON.stringify(content));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Clear for the next turn
|
|
527
|
+
state.currentTurnToolUseIds = [];
|
|
436
528
|
}
|
|
437
529
|
|
|
438
530
|
export function handleError(
|
|
@@ -465,6 +557,9 @@ export async function handleMessageComplete(
|
|
|
465
557
|
deps: EventHandlerDeps,
|
|
466
558
|
event: Extract<AgentEvent, { type: "message_complete" }>,
|
|
467
559
|
): Promise<void> {
|
|
560
|
+
// Reset per-turn tool tracking for the new turn.
|
|
561
|
+
state.currentTurnToolUseIds = [];
|
|
562
|
+
|
|
468
563
|
// Flush any remaining directive display buffer
|
|
469
564
|
if (state.pendingDirectiveDisplayBuffer.length > 0) {
|
|
470
565
|
deps.onEvent({
|
|
@@ -533,6 +628,11 @@ export async function handleMessageComplete(
|
|
|
533
628
|
);
|
|
534
629
|
}
|
|
535
630
|
|
|
631
|
+
// NOTE: Tool timing/confirmation annotations are NOT applied here because
|
|
632
|
+
// message_complete fires BEFORE tool_use/tool_result events. The annotations
|
|
633
|
+
// are applied in handleToolResult after all tools for the turn complete,
|
|
634
|
+
// then the persisted message is updated via updateMessageContent.
|
|
635
|
+
|
|
536
636
|
// Build content with UI surfaces
|
|
537
637
|
const contentWithSurfaces: ContentBlock[] = [...cleanedBlocks];
|
|
538
638
|
for (const surface of deps.ctx.currentTurnSurfaces) {
|
|
@@ -118,6 +118,27 @@ import type { TraceEmitter } from "./trace-emitter.js";
|
|
|
118
118
|
|
|
119
119
|
const log = getLogger("session-agent-loop");
|
|
120
120
|
|
|
121
|
+
/** Title-cased friendly labels for tool names, used in confirmation chips. */
|
|
122
|
+
const TOOL_FRIENDLY_LABEL: Record<string, string> = {
|
|
123
|
+
bash: "Run Command",
|
|
124
|
+
web_search: "Web Search",
|
|
125
|
+
web_fetch: "Web Fetch",
|
|
126
|
+
file_read: "Read File",
|
|
127
|
+
file_write: "Write File",
|
|
128
|
+
file_edit: "Edit File",
|
|
129
|
+
browser_navigate: "Browser",
|
|
130
|
+
browser_click: "Browser",
|
|
131
|
+
browser_type: "Browser",
|
|
132
|
+
browser_screenshot: "Browser",
|
|
133
|
+
browser_scroll: "Browser",
|
|
134
|
+
browser_wait: "Browser",
|
|
135
|
+
app_create: "Create App",
|
|
136
|
+
app_update: "Update App",
|
|
137
|
+
skill_load: "Load Skill",
|
|
138
|
+
app_file_edit: "Edit App File",
|
|
139
|
+
app_file_write: "Write App File",
|
|
140
|
+
};
|
|
141
|
+
|
|
121
142
|
type GitServiceInitializer = {
|
|
122
143
|
ensureInitialized(): Promise<void>;
|
|
123
144
|
};
|
|
@@ -222,6 +243,18 @@ export interface AgentLoopSessionContext {
|
|
|
222
243
|
: never,
|
|
223
244
|
): void;
|
|
224
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Optional callback invoked by the Session when a confirmation state changes.
|
|
248
|
+
* The agent loop registers this to track requestId → toolUseId mappings
|
|
249
|
+
* and record confirmation outcomes for persistence.
|
|
250
|
+
*/
|
|
251
|
+
onConfirmationOutcome?: (
|
|
252
|
+
requestId: string,
|
|
253
|
+
state: string,
|
|
254
|
+
toolName?: string,
|
|
255
|
+
toolUseId?: string,
|
|
256
|
+
) => void;
|
|
257
|
+
|
|
225
258
|
getWorkspaceGitService?: (workspaceDir: string) => GitServiceInitializer;
|
|
226
259
|
commitTurnChanges?: typeof commitTurnChanges;
|
|
227
260
|
|
|
@@ -432,6 +465,44 @@ export async function runAgentLoopImpl(
|
|
|
432
465
|
}
|
|
433
466
|
|
|
434
467
|
const state = createEventHandlerState();
|
|
468
|
+
|
|
469
|
+
// Register confirmation outcome tracker so the agent loop can link
|
|
470
|
+
// confirmation decisions to tool_use_ids for persistence.
|
|
471
|
+
ctx.onConfirmationOutcome = (
|
|
472
|
+
requestId,
|
|
473
|
+
confirmationState,
|
|
474
|
+
toolName,
|
|
475
|
+
toolUseId,
|
|
476
|
+
) => {
|
|
477
|
+
if (confirmationState === "pending") {
|
|
478
|
+
// Use the toolUseId passed from the prompter (which knows which tool
|
|
479
|
+
// requested confirmation) instead of the ambient state.currentToolUseId,
|
|
480
|
+
// which is unreliable when multiple tools execute in parallel.
|
|
481
|
+
const resolvedToolUseId = toolUseId ?? state.currentToolUseId;
|
|
482
|
+
if (resolvedToolUseId) {
|
|
483
|
+
state.requestIdToToolUseId.set(requestId, resolvedToolUseId);
|
|
484
|
+
}
|
|
485
|
+
} else if (
|
|
486
|
+
confirmationState === "approved" ||
|
|
487
|
+
confirmationState === "denied" ||
|
|
488
|
+
confirmationState === "timed_out"
|
|
489
|
+
) {
|
|
490
|
+
const resolvedId =
|
|
491
|
+
state.requestIdToToolUseId.get(requestId) ?? toolUseId;
|
|
492
|
+
if (resolvedId) {
|
|
493
|
+
const name = state.toolUseIdToName.get(resolvedId) ?? toolName ?? "";
|
|
494
|
+
// Build a friendly label from the tool name
|
|
495
|
+
const label =
|
|
496
|
+
TOOL_FRIENDLY_LABEL[name] ??
|
|
497
|
+
name.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
498
|
+
state.toolConfirmationOutcomes.set(resolvedId, {
|
|
499
|
+
decision: confirmationState,
|
|
500
|
+
label,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
|
|
435
506
|
let runMessages = ctx.messages;
|
|
436
507
|
|
|
437
508
|
const memoryResult = await prepareMemoryContext(
|
|
@@ -1347,6 +1418,7 @@ export async function runAgentLoopImpl(
|
|
|
1347
1418
|
|
|
1348
1419
|
ctx.abortController = null;
|
|
1349
1420
|
ctx.processing = false;
|
|
1421
|
+
ctx.onConfirmationOutcome = undefined;
|
|
1350
1422
|
ctx.surfaceActionRequestIds.delete(ctx.currentRequestId ?? "");
|
|
1351
1423
|
ctx.currentRequestId = undefined;
|
|
1352
1424
|
ctx.currentActiveSurfaceId = undefined;
|
|
@@ -135,6 +135,7 @@ export function createToolExecutor(
|
|
|
135
135
|
name: string,
|
|
136
136
|
input: Record<string, unknown>,
|
|
137
137
|
onOutput?: (chunk: string) => void,
|
|
138
|
+
toolUseId?: string,
|
|
138
139
|
) => Promise<ToolExecutionResult> {
|
|
139
140
|
// Register the session's sendToClient for browser screencast surface messages
|
|
140
141
|
registerSessionSender(ctx.conversationId, (msg) => ctx.sendToClient(msg));
|
|
@@ -143,6 +144,7 @@ export function createToolExecutor(
|
|
|
143
144
|
name: string,
|
|
144
145
|
input: Record<string, unknown>,
|
|
145
146
|
onOutput?: (chunk: string) => void,
|
|
147
|
+
toolUseId?: string,
|
|
146
148
|
) => {
|
|
147
149
|
if (isDoordashCommand(name, input)) {
|
|
148
150
|
markDoordashStepInProgress(ctx, input);
|
|
@@ -172,6 +174,7 @@ export function createToolExecutor(
|
|
|
172
174
|
allowedToolNames: ctx.allowedToolNames,
|
|
173
175
|
memoryScopeId: ctx.memoryPolicy.scopeId,
|
|
174
176
|
forcePromptSideEffects: ctx.memoryPolicy.strictSideEffects,
|
|
177
|
+
toolUseId,
|
|
175
178
|
onToolLifecycleEvent: handleToolLifecycleEvent,
|
|
176
179
|
sendToClient: (msg) => {
|
|
177
180
|
// Tool context's sendToClient uses a loose { type: string; [key: string]: unknown }
|
|
@@ -255,6 +258,10 @@ export function createToolExecutor(
|
|
|
255
258
|
undefined,
|
|
256
259
|
ctx.conversationId,
|
|
257
260
|
req.executionTarget,
|
|
261
|
+
undefined,
|
|
262
|
+
undefined,
|
|
263
|
+
undefined,
|
|
264
|
+
toolUseId,
|
|
258
265
|
);
|
|
259
266
|
if (
|
|
260
267
|
(response.decision === "always_allow" ||
|
package/src/daemon/session.ts
CHANGED
|
@@ -222,6 +222,13 @@ export class Session {
|
|
|
222
222
|
* no-op for socketless sessions.
|
|
223
223
|
*/
|
|
224
224
|
private onStateSignal?: (msg: ServerMessage) => void;
|
|
225
|
+
/** Set by the agent loop to track confirmation outcomes for persistence. */
|
|
226
|
+
onConfirmationOutcome?: (
|
|
227
|
+
requestId: string,
|
|
228
|
+
state: string,
|
|
229
|
+
toolName?: string,
|
|
230
|
+
toolUseId?: string,
|
|
231
|
+
) => void;
|
|
225
232
|
|
|
226
233
|
constructor(
|
|
227
234
|
conversationId: string,
|
|
@@ -243,7 +250,7 @@ export class Session {
|
|
|
243
250
|
: { ...DEFAULT_MEMORY_POLICY };
|
|
244
251
|
this.traceEmitter = new TraceEmitter(conversationId, sendToClient);
|
|
245
252
|
this.prompter = new PermissionPrompter(sendToClient);
|
|
246
|
-
this.prompter.setOnStateChanged((requestId, state, source) => {
|
|
253
|
+
this.prompter.setOnStateChanged((requestId, state, source, toolUseId) => {
|
|
247
254
|
// Route through emitConfirmationStateChanged so the onStateSignal
|
|
248
255
|
// listener publishes to the SSE hub for HTTP/SSE consumers.
|
|
249
256
|
this.emitConfirmationStateChanged({
|
|
@@ -251,7 +258,11 @@ export class Session {
|
|
|
251
258
|
requestId,
|
|
252
259
|
state,
|
|
253
260
|
source,
|
|
261
|
+
toolUseId,
|
|
254
262
|
});
|
|
263
|
+
// Notify the agent loop so it can track requestId → toolUseId mappings
|
|
264
|
+
// and record confirmation outcomes for persistence.
|
|
265
|
+
this.onConfirmationOutcome?.(requestId, state, undefined, toolUseId);
|
|
255
266
|
// Emit activity state transitions for confirmation lifecycle
|
|
256
267
|
if (state === "pending") {
|
|
257
268
|
this.emitActivityState(
|
|
@@ -523,6 +534,9 @@ export class Session {
|
|
|
523
534
|
return;
|
|
524
535
|
}
|
|
525
536
|
|
|
537
|
+
// Capture toolUseId before resolving (resolution deletes the pending entry)
|
|
538
|
+
const toolUseId = this.prompter.getToolUseId(requestId);
|
|
539
|
+
|
|
526
540
|
this.prompter.resolveConfirmation(
|
|
527
541
|
requestId,
|
|
528
542
|
decision,
|
|
@@ -547,6 +561,7 @@ export class Session {
|
|
|
547
561
|
requestId,
|
|
548
562
|
state: resolvedState,
|
|
549
563
|
source: emissionContext?.source ?? "button",
|
|
564
|
+
toolUseId,
|
|
550
565
|
...(emissionContext?.causedByRequestId
|
|
551
566
|
? { causedByRequestId: emissionContext.causedByRequestId }
|
|
552
567
|
: {}),
|
|
@@ -554,6 +569,13 @@ export class Session {
|
|
|
554
569
|
? { decisionText: emissionContext.decisionText }
|
|
555
570
|
: {}),
|
|
556
571
|
});
|
|
572
|
+
// Notify the agent loop of the confirmation outcome for persistence
|
|
573
|
+
this.onConfirmationOutcome?.(
|
|
574
|
+
requestId,
|
|
575
|
+
resolvedState,
|
|
576
|
+
undefined,
|
|
577
|
+
toolUseId,
|
|
578
|
+
);
|
|
557
579
|
this.emitActivityState(
|
|
558
580
|
"thinking",
|
|
559
581
|
"confirmation_resolved",
|
|
@@ -9,14 +9,18 @@
|
|
|
9
9
|
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
|
|
12
|
+
import { generateAppIcon } from "../media/app-icon-generator.js";
|
|
12
13
|
import { updatePublishedAppDeployment } from "../services/published-app-updater.js";
|
|
13
14
|
import type { ToolExecutionResult } from "../tools/types.js";
|
|
15
|
+
import { getLogger } from "../util/logger.js";
|
|
14
16
|
import { getWorkspaceDir } from "../util/platform.js";
|
|
15
17
|
import { isDoordashCommand, updateDoordashProgress } from "./doordash-steps.js";
|
|
16
18
|
import type { ServerMessage } from "./ipc-protocol.js";
|
|
17
19
|
import { refreshSurfacesForApp } from "./session-surfaces.js";
|
|
18
20
|
import type { ToolSetupContext } from "./session-tool-setup.js";
|
|
19
21
|
|
|
22
|
+
const log = getLogger("tool-side-effects");
|
|
23
|
+
|
|
20
24
|
// ── Types ────────────────────────────────────────────────────────────
|
|
21
25
|
|
|
22
26
|
export interface SideEffectContext {
|
|
@@ -65,13 +69,36 @@ function registerHook(
|
|
|
65
69
|
|
|
66
70
|
// Broadcast app_files_changed when a new app is created so clients
|
|
67
71
|
// (e.g. macOS "Things" sidebar) refresh their app list immediately.
|
|
72
|
+
// Also kicks off async icon generation via Gemini.
|
|
68
73
|
registerHook(
|
|
69
74
|
"app_create",
|
|
70
75
|
(_name, _input, result, { ctx, broadcastToAllClients }) => {
|
|
71
76
|
try {
|
|
72
|
-
const parsed = JSON.parse(result.content) as {
|
|
77
|
+
const parsed = JSON.parse(result.content) as {
|
|
78
|
+
id?: string;
|
|
79
|
+
name?: string;
|
|
80
|
+
description?: string;
|
|
81
|
+
};
|
|
73
82
|
if (parsed.id) {
|
|
74
83
|
handleAppChange(ctx, parsed.id, broadcastToAllClients);
|
|
84
|
+
|
|
85
|
+
// Fire-and-forget: generate an app icon in the background.
|
|
86
|
+
// When complete, broadcast again so clients pick up the new icon.
|
|
87
|
+
if (parsed.name) {
|
|
88
|
+
void generateAppIcon(parsed.id, parsed.name, parsed.description)
|
|
89
|
+
.then(() => {
|
|
90
|
+
broadcastToAllClients?.({
|
|
91
|
+
type: "app_files_changed",
|
|
92
|
+
appId: parsed.id!,
|
|
93
|
+
});
|
|
94
|
+
})
|
|
95
|
+
.catch((err) => {
|
|
96
|
+
log.warn(
|
|
97
|
+
{ err, appId: parsed.id },
|
|
98
|
+
"Background icon generation failed",
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
75
102
|
}
|
|
76
103
|
} catch {
|
|
77
104
|
// Result wasn't valid JSON — skip the broadcast.
|
|
@@ -79,6 +106,17 @@ registerHook(
|
|
|
79
106
|
},
|
|
80
107
|
);
|
|
81
108
|
|
|
109
|
+
// Broadcast app_files_changed when an icon is (re)generated so clients refresh.
|
|
110
|
+
registerHook(
|
|
111
|
+
"app_generate_icon",
|
|
112
|
+
(_name, input, _result, { broadcastToAllClients }) => {
|
|
113
|
+
const appId = input.app_id as string | undefined;
|
|
114
|
+
if (appId) {
|
|
115
|
+
broadcastToAllClients?.({ type: "app_files_changed", appId });
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
|
|
82
120
|
// Auto-refresh workspace surfaces when a persisted app is updated.
|
|
83
121
|
registerHook(
|
|
84
122
|
"app_update",
|
package/src/email/service.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
setOutboundPaused,
|
|
18
18
|
} from "../cli/email-guardrails.js";
|
|
19
19
|
import {
|
|
20
|
+
getNestedValue,
|
|
20
21
|
loadRawConfig,
|
|
21
22
|
saveRawConfig,
|
|
22
23
|
setNestedValue,
|
|
@@ -74,6 +75,8 @@ export class EmailService {
|
|
|
74
75
|
/** Force re-creation of the provider (e.g. after `provider set`). */
|
|
75
76
|
resetProvider(): void {
|
|
76
77
|
this.providerInstance = null;
|
|
78
|
+
this.primaryAddressResolved = false;
|
|
79
|
+
this.cachedPrimaryAddress = undefined;
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
// =========================================================================
|
|
@@ -109,6 +112,54 @@ export class EmailService {
|
|
|
109
112
|
};
|
|
110
113
|
}
|
|
111
114
|
|
|
115
|
+
// =========================================================================
|
|
116
|
+
// Primary inbox address (cached)
|
|
117
|
+
// =========================================================================
|
|
118
|
+
|
|
119
|
+
private primaryAddressResolved = false;
|
|
120
|
+
private cachedPrimaryAddress: string | undefined;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Return the assistant's primary inbox email address, caching the result
|
|
124
|
+
* for the lifetime of this service instance. Returns `undefined` when no
|
|
125
|
+
* inboxes are configured or the provider is unavailable.
|
|
126
|
+
*/
|
|
127
|
+
async getPrimaryInboxAddress(): Promise<string | undefined> {
|
|
128
|
+
if (this.primaryAddressResolved) {
|
|
129
|
+
return this.cachedPrimaryAddress;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const p = await this.provider();
|
|
133
|
+
const health = await p.health();
|
|
134
|
+
this.cachedPrimaryAddress =
|
|
135
|
+
health.inboxes.length > 0 ? health.inboxes[0].address : undefined;
|
|
136
|
+
} catch {
|
|
137
|
+
this.cachedPrimaryAddress = undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Only cache positive results from the provider so a missing inbox is
|
|
141
|
+
// retried on next call (e.g. user sets up email after initial miss).
|
|
142
|
+
if (this.cachedPrimaryAddress !== undefined) {
|
|
143
|
+
this.primaryAddressResolved = true;
|
|
144
|
+
return this.cachedPrimaryAddress;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Fall back to the statically configured email address in workspace config
|
|
148
|
+
// when the provider can't list inboxes (e.g. provider temporarily unavailable).
|
|
149
|
+
// Intentionally NOT setting primaryAddressResolved so the provider is retried
|
|
150
|
+
// on the next call — the fallback is a best-effort stopgap, not authoritative.
|
|
151
|
+
try {
|
|
152
|
+
const raw = loadRawConfig();
|
|
153
|
+
const configured = getNestedValue(raw, "email.address");
|
|
154
|
+
if (typeof configured === "string" && configured.length > 0) {
|
|
155
|
+
return configured;
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
// Config unavailable — leave as undefined
|
|
159
|
+
}
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
112
163
|
// =========================================================================
|
|
113
164
|
// Domain setup
|
|
114
165
|
// =========================================================================
|
|
@@ -138,7 +189,10 @@ export class EmailService {
|
|
|
138
189
|
displayName?: string,
|
|
139
190
|
): Promise<EmailInbox> {
|
|
140
191
|
const p = await this.provider();
|
|
141
|
-
|
|
192
|
+
const inbox = await p.createInbox({ username, domain, displayName });
|
|
193
|
+
this.primaryAddressResolved = false;
|
|
194
|
+
this.cachedPrimaryAddress = undefined;
|
|
195
|
+
return inbox;
|
|
142
196
|
}
|
|
143
197
|
|
|
144
198
|
async listInboxes(): Promise<EmailInbox[]> {
|
|
@@ -148,7 +202,10 @@ export class EmailService {
|
|
|
148
202
|
|
|
149
203
|
async ensureInboxes(domain: string): Promise<EmailInbox[]> {
|
|
150
204
|
const p = await this.provider();
|
|
151
|
-
|
|
205
|
+
const inboxes = await p.ensureInboxes({ domain });
|
|
206
|
+
this.primaryAddressResolved = false;
|
|
207
|
+
this.cachedPrimaryAddress = undefined;
|
|
208
|
+
return inboxes;
|
|
152
209
|
}
|
|
153
210
|
|
|
154
211
|
// =========================================================================
|
package/src/index.ts
CHANGED
|
@@ -1,63 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { buildCliProgram } from "./cli/program.js";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const require = createRequire(import.meta.url);
|
|
8
|
-
const { version } = require("../package.json") as { version: string };
|
|
9
|
-
|
|
10
|
-
import { registerAmazonCommand } from "./cli/amazon.js";
|
|
11
|
-
import {
|
|
12
|
-
registerConfigCommand,
|
|
13
|
-
registerKeysCommand,
|
|
14
|
-
registerMemoryCommand,
|
|
15
|
-
registerTrustCommand,
|
|
16
|
-
} from "./cli/config-commands.js";
|
|
17
|
-
import {
|
|
18
|
-
registerAuditCommand,
|
|
19
|
-
registerCompletionsCommand,
|
|
20
|
-
registerDefaultAction,
|
|
21
|
-
registerDevCommand,
|
|
22
|
-
registerDoctorCommand,
|
|
23
|
-
registerSessionsCommand,
|
|
24
|
-
} from "./cli/core-commands.js";
|
|
25
|
-
import { registerEmailCommand } from "./cli/email.js";
|
|
26
|
-
import { registerInfluencerCommand } from "./cli/influencer.js";
|
|
27
|
-
import {
|
|
28
|
-
registerContactsCommand,
|
|
29
|
-
registerIntegrationsCommand,
|
|
30
|
-
} from "./cli/integrations.js";
|
|
31
|
-
import { registerMapCommand } from "./cli/map.js";
|
|
32
|
-
import { registerMcpCommand } from "./cli/mcp.js";
|
|
33
|
-
import { registerSequenceCommand } from "./cli/sequence.js";
|
|
34
|
-
import { registerTwitterCommand } from "./cli/twitter.js";
|
|
35
|
-
import { registerHooksCommand } from "./hooks/cli.js";
|
|
36
|
-
|
|
37
|
-
const program = new Command();
|
|
38
|
-
|
|
39
|
-
program.name("vellum").description("Local AI assistant").version(version);
|
|
40
|
-
|
|
41
|
-
registerDefaultAction(program);
|
|
42
|
-
registerDevCommand(program);
|
|
43
|
-
registerSessionsCommand(program);
|
|
44
|
-
registerConfigCommand(program);
|
|
45
|
-
registerKeysCommand(program);
|
|
46
|
-
registerTrustCommand(program);
|
|
47
|
-
registerMemoryCommand(program);
|
|
48
|
-
registerAuditCommand(program);
|
|
49
|
-
registerDoctorCommand(program);
|
|
50
|
-
registerHooksCommand(program);
|
|
51
|
-
registerMcpCommand(program);
|
|
52
|
-
registerEmailCommand(program);
|
|
53
|
-
registerIntegrationsCommand(program);
|
|
54
|
-
registerContactsCommand(program);
|
|
55
|
-
registerAmazonCommand(program);
|
|
56
|
-
registerCompletionsCommand(program);
|
|
57
|
-
|
|
58
|
-
registerTwitterCommand(program);
|
|
59
|
-
registerMapCommand(program);
|
|
60
|
-
registerInfluencerCommand(program);
|
|
61
|
-
registerSequenceCommand(program);
|
|
62
|
-
|
|
63
|
-
program.parse();
|
|
5
|
+
buildCliProgram().parse();
|