@vellumai/assistant 0.4.4 → 0.4.6
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 +4 -4
- package/README.md +6 -6
- package/bun.lock +6 -2
- package/docs/architecture/memory.md +4 -4
- package/package.json +2 -2
- package/src/__tests__/actor-token-service.test.ts +5 -2
- package/src/__tests__/assistant-feature-flags-integration.test.ts +1 -0
- package/src/__tests__/call-controller.test.ts +78 -0
- package/src/__tests__/call-domain.test.ts +148 -10
- package/src/__tests__/call-pointer-message-composer.test.ts +39 -49
- package/src/__tests__/call-pointer-messages.test.ts +105 -43
- package/src/__tests__/canonical-guardian-store.test.ts +44 -10
- package/src/__tests__/channel-approval-routes.test.ts +67 -65
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +1 -0
- package/src/__tests__/conversation-attention-telegram.test.ts +2 -2
- package/src/__tests__/deterministic-verification-control-plane.test.ts +6 -6
- package/src/__tests__/guardian-actions-endpoint.test.ts +7 -6
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +57 -12
- package/src/__tests__/guardian-grant-minting.test.ts +24 -24
- package/src/__tests__/guardian-principal-id-roundtrip.test.ts +205 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +64 -25
- package/src/__tests__/guardian-routing-state.test.ts +4 -4
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +2 -2
- package/src/__tests__/inbound-invite-redemption.test.ts +8 -8
- package/src/__tests__/memory-retrieval.benchmark.test.ts +22 -47
- package/src/__tests__/no-is-trusted-guard.test.ts +77 -0
- package/src/__tests__/non-member-access-request.test.ts +50 -47
- package/src/__tests__/relay-server.test.ts +71 -0
- package/src/__tests__/send-endpoint-busy.test.ts +6 -0
- package/src/__tests__/session-tool-setup-tools-disabled.test.ts +155 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +1 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +66 -2
- package/src/__tests__/system-prompt.test.ts +1 -0
- package/src/__tests__/tool-approval-handler.test.ts +1 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +9 -2
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +8 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +22 -22
- package/src/__tests__/trusted-contact-multichannel.test.ts +4 -4
- package/src/__tests__/trusted-contact-verification.test.ts +10 -10
- package/src/approvals/guardian-decision-primitive.ts +29 -25
- package/src/approvals/guardian-request-resolvers.ts +9 -5
- package/src/calls/call-pointer-message-composer.ts +27 -85
- package/src/calls/call-pointer-messages.ts +54 -21
- package/src/calls/guardian-dispatch.ts +30 -0
- package/src/calls/relay-server.ts +13 -13
- package/src/config/system-prompt.ts +10 -3
- package/src/config/templates/BOOTSTRAP.md +6 -5
- package/src/config/templates/USER.md +1 -0
- package/src/config/user-reference.ts +44 -0
- package/src/daemon/handlers/guardian-actions.ts +5 -2
- package/src/daemon/handlers/sessions.ts +8 -3
- package/src/daemon/lifecycle.ts +109 -3
- package/src/daemon/server.ts +32 -24
- package/src/daemon/session-agent-loop.ts +4 -3
- package/src/daemon/session-lifecycle.ts +1 -9
- package/src/daemon/session-process.ts +2 -2
- package/src/daemon/session-runtime-assembly.ts +2 -0
- package/src/daemon/session-tool-setup.ts +10 -0
- package/src/daemon/session.ts +1 -0
- package/src/memory/canonical-guardian-store.ts +40 -0
- package/src/memory/conversation-crud.ts +26 -0
- package/src/memory/conversation-store.ts +1 -0
- package/src/memory/db-init.ts +8 -0
- package/src/memory/guardian-bindings.ts +4 -0
- package/src/memory/job-handlers/backfill.ts +2 -9
- package/src/memory/migrations/125-guardian-principal-id-columns.ts +19 -0
- package/src/memory/migrations/126-backfill-guardian-principal-id.ts +210 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/migrations/registry.ts +5 -0
- package/src/memory/schema.ts +3 -0
- package/src/notifications/copy-composer.ts +2 -2
- package/src/runtime/access-request-helper.ts +43 -28
- package/src/runtime/actor-trust-resolver.ts +19 -14
- package/src/runtime/channel-guardian-service.ts +6 -0
- package/src/runtime/guardian-context-resolver.ts +6 -2
- package/src/runtime/guardian-reply-router.ts +33 -16
- package/src/runtime/guardian-vellum-migration.ts +29 -5
- package/src/runtime/http-types.ts +0 -13
- package/src/runtime/local-actor-identity.ts +19 -13
- package/src/runtime/middleware/actor-token.ts +2 -2
- package/src/runtime/routes/channel-delivery-routes.ts +5 -5
- package/src/runtime/routes/conversation-routes.ts +45 -35
- package/src/runtime/routes/guardian-action-routes.ts +7 -1
- package/src/runtime/routes/guardian-approval-interception.ts +52 -52
- package/src/runtime/routes/guardian-bootstrap-routes.ts +1 -0
- package/src/runtime/routes/inbound-conversation.ts +7 -7
- package/src/runtime/routes/inbound-message-handler.ts +105 -94
- package/src/runtime/tool-grant-request-helper.ts +1 -0
- package/src/util/logger.ts +10 -0
- package/src/daemon/call-pointer-generators.ts +0 -59
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Deterministic call pointer message templates and instruction builder.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Follows the same pattern as approval-message-composer.ts and
|
|
9
|
-
* guardian-action-message-composer.ts.
|
|
4
|
+
* Provides fallback templates for untrusted audiences and builds
|
|
5
|
+
* structured instructions for the daemon session to generate pointer
|
|
6
|
+
* copy as a natural conversation turn for trusted audiences.
|
|
10
7
|
*/
|
|
11
|
-
import type { PointerCopyGenerator } from '../runtime/http-types.js';
|
|
12
|
-
import { getLogger } from '../util/logger.js';
|
|
13
|
-
|
|
14
|
-
const log = getLogger('call-pointer-message-composer');
|
|
15
8
|
|
|
16
9
|
// ---------------------------------------------------------------------------
|
|
17
10
|
// Types
|
|
@@ -33,83 +26,33 @@ export interface CallPointerMessageContext {
|
|
|
33
26
|
channel?: string;
|
|
34
27
|
}
|
|
35
28
|
|
|
36
|
-
export interface ComposeCallPointerMessageOptions {
|
|
37
|
-
fallbackText?: string;
|
|
38
|
-
requiredFacts?: string[];
|
|
39
|
-
maxTokens?: number;
|
|
40
|
-
timeoutMs?: number;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// Constants (exported for the daemon-injected generator implementation)
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
|
|
47
|
-
export const POINTER_COPY_TIMEOUT_MS = 3_000;
|
|
48
|
-
export const POINTER_COPY_MAX_TOKENS = 120;
|
|
49
|
-
export const POINTER_COPY_SYSTEM_PROMPT =
|
|
50
|
-
'You are an assistant writing a brief status update about a phone call. '
|
|
51
|
-
+ 'Keep it concise (1-2 sentences), natural, and informative. '
|
|
52
|
-
+ 'Preserve all factual details exactly (phone numbers, durations, failure reasons, verification status). '
|
|
53
|
-
+ 'Do not mention internal systems or technical details. '
|
|
54
|
-
+ 'Return plain text only.';
|
|
55
|
-
|
|
56
29
|
// ---------------------------------------------------------------------------
|
|
57
|
-
//
|
|
30
|
+
// Daemon instruction builder
|
|
58
31
|
// ---------------------------------------------------------------------------
|
|
59
32
|
|
|
60
33
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* The generator parameter is the daemon-provided function that knows about
|
|
65
|
-
* providers. When absent (or in test env), only the deterministic fallback
|
|
66
|
-
* is used.
|
|
34
|
+
* Build an instruction message to send to the daemon session so the
|
|
35
|
+
* assistant generates a natural pointer status update as a conversation turn.
|
|
67
36
|
*/
|
|
68
|
-
export
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return fallbackText;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** @internal Exported for use by the daemon-injected generator implementation. */
|
|
92
|
-
export function buildPointerGenerationPrompt(
|
|
93
|
-
context: CallPointerMessageContext,
|
|
94
|
-
fallbackText: string,
|
|
95
|
-
requiredFacts: string[] | undefined,
|
|
96
|
-
): string {
|
|
97
|
-
const factClause = requiredFacts && requiredFacts.length > 0
|
|
98
|
-
? `Required facts to include: ${requiredFacts.join(', ')}.\n`
|
|
99
|
-
: '';
|
|
100
|
-
return [
|
|
101
|
-
'Rewrite the following call status message as a natural, conversational update.',
|
|
102
|
-
'Keep the same concrete facts (phone number, duration, failure reason, verification status).',
|
|
103
|
-
factClause,
|
|
104
|
-
`Context JSON: ${JSON.stringify(context)}`,
|
|
105
|
-
`Fallback message: ${fallbackText}`,
|
|
106
|
-
].filter(Boolean).join('\n\n');
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** @internal Exported for use by the daemon-injected generator implementation. */
|
|
110
|
-
export function includesRequiredFacts(text: string, requiredFacts: string[] | undefined): boolean {
|
|
111
|
-
if (!requiredFacts || requiredFacts.length === 0) return true;
|
|
112
|
-
return requiredFacts.every((fact) => text.includes(fact));
|
|
37
|
+
export function buildPointerInstruction(context: CallPointerMessageContext): string {
|
|
38
|
+
const parts: string[] = [
|
|
39
|
+
'[CALL_STATUS_EVENT]',
|
|
40
|
+
`Event: ${context.scenario}`,
|
|
41
|
+
`Phone number: ${context.phoneNumber}`,
|
|
42
|
+
];
|
|
43
|
+
if (context.duration) parts.push(`Duration: ${context.duration}`);
|
|
44
|
+
if (context.reason) parts.push(`Reason: ${context.reason}`);
|
|
45
|
+
if (context.verificationCode) parts.push(`Verification code: ${context.verificationCode}`);
|
|
46
|
+
if (context.channel) parts.push(`Channel: ${context.channel}`);
|
|
47
|
+
|
|
48
|
+
parts.push('');
|
|
49
|
+
parts.push(
|
|
50
|
+
'Write a brief (1-2 sentence) status update about this phone call event for the user. '
|
|
51
|
+
+ 'Preserve all factual details exactly (phone numbers, durations, failure reasons, verification codes). '
|
|
52
|
+
+ 'Be concise, natural, and informative.',
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return parts.join('\n');
|
|
113
56
|
}
|
|
114
57
|
|
|
115
58
|
// ---------------------------------------------------------------------------
|
|
@@ -119,8 +62,7 @@ export function includesRequiredFacts(text: string, requiredFacts: string[] | un
|
|
|
119
62
|
/**
|
|
120
63
|
* Return a scenario-specific deterministic fallback message.
|
|
121
64
|
*
|
|
122
|
-
*
|
|
123
|
-
* templates from call-pointer-messages.ts.
|
|
65
|
+
* Used for untrusted audiences and when the daemon processor is unavailable.
|
|
124
66
|
*/
|
|
125
67
|
export function getPointerFallbackMessage(context: CallPointerMessageContext): string {
|
|
126
68
|
switch (context.scenario) {
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
* so the user sees call lifecycle events without the full transcript
|
|
4
4
|
* (which lives in the dedicated voice conversation).
|
|
5
5
|
*
|
|
6
|
-
* Trust-aware: trusted audiences
|
|
7
|
-
*
|
|
8
|
-
* deterministic fallback text
|
|
6
|
+
* Trust-aware: trusted audiences get pointer messages routed through the
|
|
7
|
+
* daemon session as a conversation turn (the assistant generates the text).
|
|
8
|
+
* Untrusted/unknown audiences always receive deterministic fallback text
|
|
9
|
+
* written directly to the conversation store.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import * as conversationStore from '../memory/conversation-store.js';
|
|
12
|
-
import type { PointerCopyGenerator } from '../runtime/http-types.js';
|
|
13
13
|
import { getLogger } from '../util/logger.js';
|
|
14
14
|
import {
|
|
15
|
+
buildPointerInstruction,
|
|
15
16
|
type CallPointerMessageContext,
|
|
16
|
-
composeCallPointerMessageGenerative,
|
|
17
17
|
getPointerFallbackMessage,
|
|
18
18
|
} from './call-pointer-message-composer.js';
|
|
19
19
|
|
|
@@ -23,24 +23,40 @@ export type PointerEvent = 'started' | 'completed' | 'failed' | 'guardian_verifi
|
|
|
23
23
|
|
|
24
24
|
export type PointerAudienceMode = 'auto' | 'trusted' | 'untrusted';
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Daemon-injected function that sends a message through the daemon session
|
|
28
|
+
* pipeline (persistAndProcessMessage), letting the assistant generate the
|
|
29
|
+
* pointer text as a natural conversation turn.
|
|
30
|
+
*
|
|
31
|
+
* @param requiredFacts - facts that must appear verbatim in the generated
|
|
32
|
+
* text (phone number, duration, outcome keyword, etc.). The processor
|
|
33
|
+
* should validate the output and throw if any are missing so the
|
|
34
|
+
* deterministic fallback fires.
|
|
35
|
+
*/
|
|
36
|
+
export type PointerMessageProcessor = (
|
|
37
|
+
conversationId: string,
|
|
38
|
+
instruction: string,
|
|
39
|
+
requiredFacts?: string[],
|
|
40
|
+
) => Promise<void>;
|
|
41
|
+
|
|
26
42
|
// ---------------------------------------------------------------------------
|
|
27
|
-
// Module-level
|
|
43
|
+
// Module-level processor injection (set by daemon lifecycle at startup)
|
|
28
44
|
// ---------------------------------------------------------------------------
|
|
29
45
|
|
|
30
|
-
let
|
|
46
|
+
let pointerMessageProcessor: PointerMessageProcessor | undefined;
|
|
31
47
|
|
|
32
48
|
/**
|
|
33
|
-
* Inject the daemon-provided pointer
|
|
49
|
+
* Inject the daemon-provided pointer message processor.
|
|
34
50
|
* Called from daemon/lifecycle.ts at startup, following the same pattern
|
|
35
51
|
* as setRelayBroadcast.
|
|
36
52
|
*/
|
|
37
|
-
export function
|
|
38
|
-
|
|
53
|
+
export function setPointerMessageProcessor(processor: PointerMessageProcessor): void {
|
|
54
|
+
pointerMessageProcessor = processor;
|
|
39
55
|
}
|
|
40
56
|
|
|
41
57
|
/** @internal Reset for tests. */
|
|
42
|
-
export function
|
|
43
|
-
|
|
58
|
+
export function resetPointerMessageProcessor(): void {
|
|
59
|
+
pointerMessageProcessor = undefined;
|
|
44
60
|
}
|
|
45
61
|
|
|
46
62
|
// ---------------------------------------------------------------------------
|
|
@@ -51,6 +67,7 @@ export function resetPointerCopyGenerator(): void {
|
|
|
51
67
|
* Resolve whether the audience for a pointer message is trusted.
|
|
52
68
|
*
|
|
53
69
|
* Trusted when:
|
|
70
|
+
* - recent message provenance trust class is 'guardian' or 'trusted_contact'
|
|
54
71
|
* - conversation threadType is 'private' (local desktop-origin context)
|
|
55
72
|
* - conversation origin channel is 'vellum' (desktop app)
|
|
56
73
|
*
|
|
@@ -58,6 +75,12 @@ export function resetPointerCopyGenerator(): void {
|
|
|
58
75
|
*/
|
|
59
76
|
function resolvePointerAudienceTrust(conversationId: string): boolean {
|
|
60
77
|
try {
|
|
78
|
+
// Check provenance trust class on recent messages first — this catches
|
|
79
|
+
// trusted contacts who initiate calls from gateway channels (e.g. WhatsApp)
|
|
80
|
+
// where the conversation itself isn't a desktop-origin private thread.
|
|
81
|
+
const provenance = conversationStore.getConversationRecentProvenanceTrustClass(conversationId);
|
|
82
|
+
if (provenance === 'guardian' || provenance === 'trusted_contact') return true;
|
|
83
|
+
|
|
61
84
|
const threadType = conversationStore.getConversationThreadType(conversationId);
|
|
62
85
|
if (threadType === 'private') return true;
|
|
63
86
|
|
|
@@ -91,6 +114,7 @@ export async function addPointerMessage(
|
|
|
91
114
|
};
|
|
92
115
|
|
|
93
116
|
// Build required-facts list so generated text cannot drop key details.
|
|
117
|
+
// These are passed to the processor for post-generation validation.
|
|
94
118
|
const requiredFacts: string[] = [phoneNumber];
|
|
95
119
|
if (extra?.duration) requiredFacts.push(extra.duration);
|
|
96
120
|
if (extra?.verificationCode) requiredFacts.push(extra.verificationCode);
|
|
@@ -109,21 +133,30 @@ export async function addPointerMessage(
|
|
|
109
133
|
const outcomeKeyword = eventOutcomeKeywords[event];
|
|
110
134
|
if (outcomeKeyword) requiredFacts.push(outcomeKeyword);
|
|
111
135
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const isTrusted =
|
|
136
|
+
const trustedAudience =
|
|
115
137
|
audienceMode === 'trusted' ||
|
|
116
138
|
(audienceMode === 'auto' && resolvePointerAudienceTrust(conversationId));
|
|
117
139
|
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
140
|
+
if (trustedAudience && pointerMessageProcessor) {
|
|
141
|
+
// Route through the daemon session — the assistant generates the
|
|
142
|
+
// pointer text as a natural conversation turn, shaped by context,
|
|
143
|
+
// identity, and preferences.
|
|
144
|
+
const instruction = buildPointerInstruction(context);
|
|
145
|
+
try {
|
|
146
|
+
await pointerMessageProcessor(conversationId, instruction, requiredFacts);
|
|
147
|
+
return;
|
|
148
|
+
} catch (err) {
|
|
149
|
+
log.warn({ err, event, conversationId }, 'Daemon pointer processing failed, falling back to deterministic');
|
|
123
150
|
}
|
|
124
|
-
|
|
151
|
+
} else if (!trustedAudience && pointerMessageProcessor) {
|
|
152
|
+
log.debug({ event, conversationId }, 'Untrusted audience — using deterministic pointer copy');
|
|
125
153
|
}
|
|
126
154
|
|
|
155
|
+
// Deterministic fallback: write directly to the conversation store.
|
|
156
|
+
// Used for untrusted audiences, when the daemon processor is unavailable,
|
|
157
|
+
// or when daemon processing fails.
|
|
158
|
+
const text = getPointerFallbackMessage(context);
|
|
159
|
+
|
|
127
160
|
// Pointer messages are assistant-generated status updates in the initiating
|
|
128
161
|
// desktop thread. Do not set userMessageChannel — doing so would mark the
|
|
129
162
|
// conversation's origin channel as voice, causing it to leak into the
|
|
@@ -14,8 +14,10 @@ import {
|
|
|
14
14
|
listCanonicalGuardianRequests,
|
|
15
15
|
updateCanonicalGuardianDelivery,
|
|
16
16
|
} from '../memory/canonical-guardian-store.js';
|
|
17
|
+
import { getActiveBinding } from '../memory/guardian-bindings.js';
|
|
17
18
|
import { emitNotificationSignal } from '../notifications/emit-signal.js';
|
|
18
19
|
import type { NotificationDeliveryResult } from '../notifications/types.js';
|
|
20
|
+
import { ensureVellumGuardianBinding } from '../runtime/guardian-vellum-migration.js';
|
|
19
21
|
import { getLogger } from '../util/logger.js';
|
|
20
22
|
import { getUserConsultationTimeoutMs } from './call-constants.js';
|
|
21
23
|
import type { CallPendingQuestion } from './types.js';
|
|
@@ -88,6 +90,33 @@ async function dispatchGuardianQuestionInner(params: GuardianDispatchParams): Pr
|
|
|
88
90
|
try {
|
|
89
91
|
const expiresAt = Date.now() + getUserConsultationTimeoutMs();
|
|
90
92
|
|
|
93
|
+
// Voice decisions are handled in guardian threads tied to the assistant-
|
|
94
|
+
// level guardian identity. Resolve the principal from the vellum binding
|
|
95
|
+
// (the canonical assistant-level binding) so the request is attributed to
|
|
96
|
+
// the assistant's guardian principal.
|
|
97
|
+
let vellumBinding = getActiveBinding(assistantId, 'vellum');
|
|
98
|
+
let guardianPrincipalId = vellumBinding?.guardianPrincipalId ?? undefined;
|
|
99
|
+
|
|
100
|
+
// Self-heal: if the vellum binding is missing or lacks a principal,
|
|
101
|
+
// bootstrap it so the pending_question request can be attributed.
|
|
102
|
+
if (!guardianPrincipalId) {
|
|
103
|
+
log.info(
|
|
104
|
+
{ callSessionId, assistantId, hadBinding: !!vellumBinding },
|
|
105
|
+
'Vellum binding missing or lacks principal — self-healing for voice dispatch',
|
|
106
|
+
);
|
|
107
|
+
const healedPrincipalId = ensureVellumGuardianBinding(assistantId);
|
|
108
|
+
vellumBinding = getActiveBinding(assistantId, 'vellum');
|
|
109
|
+
guardianPrincipalId = vellumBinding?.guardianPrincipalId ?? healedPrincipalId;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!guardianPrincipalId) {
|
|
113
|
+
log.error(
|
|
114
|
+
{ callSessionId, assistantId },
|
|
115
|
+
'Voice guardian dispatch: no guardianPrincipalId after self-heal — cannot create pending_question',
|
|
116
|
+
);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
91
120
|
// Create the canonical guardian request as the primary record.
|
|
92
121
|
const request = createCanonicalGuardianRequest({
|
|
93
122
|
kind: 'pending_question',
|
|
@@ -97,6 +126,7 @@ async function dispatchGuardianQuestionInner(params: GuardianDispatchParams): Pr
|
|
|
97
126
|
callSessionId,
|
|
98
127
|
pendingQuestionId: pendingQuestion.id,
|
|
99
128
|
questionText: pendingQuestion.questionText,
|
|
129
|
+
guardianPrincipalId,
|
|
100
130
|
toolName,
|
|
101
131
|
inputDigest,
|
|
102
132
|
expiresAt: new Date(expiresAt).toISOString(),
|
|
@@ -510,8 +510,8 @@ export class RelayConnection {
|
|
|
510
510
|
const initialActorTrust = resolveActorTrust({
|
|
511
511
|
assistantId,
|
|
512
512
|
sourceChannel: 'voice',
|
|
513
|
-
|
|
514
|
-
|
|
513
|
+
conversationExternalId: otherPartyNumber,
|
|
514
|
+
actorExternalId: otherPartyNumber || undefined,
|
|
515
515
|
});
|
|
516
516
|
const initialGuardianContext = toGuardianRuntimeContextFromTrust(initialActorTrust, otherPartyNumber);
|
|
517
517
|
|
|
@@ -561,8 +561,8 @@ export class RelayConnection {
|
|
|
561
561
|
const actorTrust = resolveActorTrust({
|
|
562
562
|
assistantId,
|
|
563
563
|
sourceChannel: 'voice',
|
|
564
|
-
|
|
565
|
-
|
|
564
|
+
conversationExternalId: msg.from,
|
|
565
|
+
actorExternalId: msg.from || undefined,
|
|
566
566
|
});
|
|
567
567
|
|
|
568
568
|
// Check for a pending voice guardian challenge before the ACL deny
|
|
@@ -979,8 +979,8 @@ export class RelayConnection {
|
|
|
979
979
|
const verifiedActorTrust = resolveActorTrust({
|
|
980
980
|
assistantId: this.guardianChallengeAssistantId,
|
|
981
981
|
sourceChannel: 'voice',
|
|
982
|
-
|
|
983
|
-
|
|
982
|
+
conversationExternalId: this.guardianVerificationFromNumber,
|
|
983
|
+
actorExternalId: this.guardianVerificationFromNumber,
|
|
984
984
|
});
|
|
985
985
|
this.controller.setGuardianContext(
|
|
986
986
|
toGuardianRuntimeContextFromTrust(verifiedActorTrust, this.guardianVerificationFromNumber),
|
|
@@ -1153,9 +1153,9 @@ export class RelayConnection {
|
|
|
1153
1153
|
const accessResult = notifyGuardianOfAccessRequest({
|
|
1154
1154
|
canonicalAssistantId: this.accessRequestAssistantId,
|
|
1155
1155
|
sourceChannel: 'voice',
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1156
|
+
conversationExternalId: this.accessRequestFromNumber,
|
|
1157
|
+
actorExternalId: this.accessRequestFromNumber,
|
|
1158
|
+
actorDisplayName: callerName,
|
|
1159
1159
|
});
|
|
1160
1160
|
|
|
1161
1161
|
if (accessResult.notified) {
|
|
@@ -1308,8 +1308,8 @@ export class RelayConnection {
|
|
|
1308
1308
|
const updatedTrust = resolveActorTrust({
|
|
1309
1309
|
assistantId,
|
|
1310
1310
|
sourceChannel: 'voice',
|
|
1311
|
-
|
|
1312
|
-
|
|
1311
|
+
conversationExternalId: fromNumber,
|
|
1312
|
+
actorExternalId: fromNumber,
|
|
1313
1313
|
});
|
|
1314
1314
|
|
|
1315
1315
|
if (this.controller) {
|
|
@@ -1583,8 +1583,8 @@ export class RelayConnection {
|
|
|
1583
1583
|
const redeemedActorTrust = resolveActorTrust({
|
|
1584
1584
|
assistantId: this.inviteRedemptionAssistantId,
|
|
1585
1585
|
sourceChannel: 'voice',
|
|
1586
|
-
|
|
1587
|
-
|
|
1586
|
+
conversationExternalId: this.inviteRedemptionFromNumber,
|
|
1587
|
+
actorExternalId: this.inviteRedemptionFromNumber,
|
|
1588
1588
|
});
|
|
1589
1589
|
this.controller.setGuardianContext(
|
|
1590
1590
|
toGuardianRuntimeContextFromTrust(redeemedActorTrust, this.inviteRedemptionFromNumber),
|
|
@@ -11,7 +11,7 @@ import { getBaseDataDir, getIsContainerized } from './env-registry.js';
|
|
|
11
11
|
import { getConfig } from './loader.js';
|
|
12
12
|
import { skillFlagKey } from './skill-state.js';
|
|
13
13
|
import { loadSkillCatalog, type SkillSummary } from './skills.js';
|
|
14
|
-
import { resolveUserReference } from './user-reference.js';
|
|
14
|
+
import { resolveUserPronouns, resolveUserReference } from './user-reference.js';
|
|
15
15
|
|
|
16
16
|
const log = getLogger('system-prompt');
|
|
17
17
|
|
|
@@ -489,16 +489,23 @@ export function buildChannelCommandIntentSection(): string {
|
|
|
489
489
|
|
|
490
490
|
export function buildExternalCommsIdentitySection(): string {
|
|
491
491
|
const userRef = resolveUserReference();
|
|
492
|
-
|
|
492
|
+
const userPronouns = resolveUserPronouns();
|
|
493
|
+
const lines = [
|
|
493
494
|
'## External Communications Identity',
|
|
494
495
|
'',
|
|
495
496
|
'When communicating with people outside your user\'s household (phone calls, messages to non-guardian contacts, emails to third parties, etc.):',
|
|
496
497
|
'',
|
|
497
498
|
`- Refer to yourself as an **assistant**, not an "AI assistant" or "artificial intelligence". Keep identity framing simple and natural.`,
|
|
498
499
|
`- When introducing who you represent, say you are acting on behalf of **${userRef}**.`,
|
|
500
|
+
];
|
|
501
|
+
if (userPronouns) {
|
|
502
|
+
lines.push(`- Your user's pronouns are **${userPronouns}**. Use these when referring to your user in the third person.`);
|
|
503
|
+
}
|
|
504
|
+
lines.push(
|
|
499
505
|
'- Do not volunteer that you are an AI unless directly asked. If asked, answer honestly.',
|
|
500
506
|
'- This is guidance for natural, human-like communication — not a hard constraint. Occasional variations are acceptable.',
|
|
501
|
-
|
|
507
|
+
);
|
|
508
|
+
return lines.join('\n');
|
|
502
509
|
}
|
|
503
510
|
|
|
504
511
|
export function buildSwarmGuidanceSection(): string {
|
|
@@ -33,7 +33,8 @@ Once they respond, follow the remaining steps in order, one at a time:
|
|
|
33
33
|
- What they do for work (role, field, day-to-day)
|
|
34
34
|
- What they do for fun (hobbies, interests)
|
|
35
35
|
- What tools they rely on daily (apps, platforms, workflows)
|
|
36
|
-
|
|
36
|
+
- Their pronouns (he/him, she/her, they/them, etc.)
|
|
37
|
+
Weave these into the conversation. Inferred answers are fine when confidence is high — for pronouns, if the user's name is strongly gendered, you can infer with reasonable confidence, but default to they/them if unsure. If something is unclear, ask one short follow-up, but don't turn it into an interview. One or two natural exchanges should cover it. If the user declines to share something, respect that and move on (see Privacy below).
|
|
37
38
|
|
|
38
39
|
6. **Show them what you can take off their plate.** Based on everything you've learned, present exactly 2 actionable task suggestions. Each should feel specific to this user, not generic. Frame it as: here's what you can hand off to me right now. Avoid language like "let's build automations" or "let's set up workflows." If `ui_show` is available (dashboard channels), show the suggestions as a card with 2 action buttons. Use `surface_type: "card"` with a short title and body, and add one `relay_prompt` action per suggestion. Each action's `data.prompt` should contain a natural-language request the user would say. Example structure:
|
|
39
40
|
```
|
|
@@ -54,18 +55,18 @@ Ask one question at a time. Don't dump a form on them.
|
|
|
54
55
|
|
|
55
56
|
## Privacy
|
|
56
57
|
|
|
57
|
-
Only the assistant's name is hard-required. Everything else about the user (their name, work role, hobbies, daily tools) is best-effort. Ask naturally, not as a form. If something is unclear, you can ask one short follow-up, but if the user declines or dodges, do not push. Just move on.
|
|
58
|
+
Only the assistant's name is hard-required. Everything else about the user (their name, pronouns, work role, hobbies, daily tools) is best-effort. Ask naturally, not as a form. If something is unclear, you can ask one short follow-up, but if the user declines or dodges, do not push. Just move on.
|
|
58
59
|
|
|
59
60
|
A field is "resolved" when any of these is true:
|
|
60
61
|
- The user gave an explicit answer
|
|
61
62
|
- You confidently inferred it from conversation
|
|
62
63
|
- The user declined, dodged, or sidestepped it (treat all of these as declined)
|
|
63
64
|
|
|
64
|
-
When saving to `USER.md`, mark declined fields so you don't re-ask later (e.g., `Work role: declined_by_user`). Inferred values can note the source (e.g., `Daily tools: inferred: Slack, Figma`).
|
|
65
|
+
When saving to `USER.md`, mark declined fields so you don't re-ask later (e.g., `Work role: declined_by_user`). Inferred values can note the source (e.g., `Daily tools: inferred: Slack, Figma`). For pronouns, if inferred from name, note the source (e.g., `Pronouns: inferred: he/him`).
|
|
65
66
|
|
|
66
67
|
## Saving What You Learn
|
|
67
68
|
|
|
68
|
-
Save what you learn as you go. Update `IDENTITY.md` (name, nature, personality, emoji, style tendency) and `USER.md` (their name, how to address them, goals, locale, work role, hobbies, daily tools) using `file_edit`. If the conversation reveals how the user wants you to behave (e.g., "be direct," "don't be too chatty"), save those behavioral guidelines to `SOUL.md` — that file is about your personality and how you operate, not the user's data. Just do it quietly. Don't tell the user which files you're editing or mention tool names.
|
|
69
|
+
Save what you learn as you go. Update `IDENTITY.md` (name, nature, personality, emoji, style tendency) and `USER.md` (their name, pronouns, how to address them, goals, locale, work role, hobbies, daily tools) using `file_edit`. If the conversation reveals how the user wants you to behave (e.g., "be direct," "don't be too chatty"), save those behavioral guidelines to `SOUL.md` — that file is about your personality and how you operate, not the user's data. Just do it quietly. Don't tell the user which files you're editing or mention tool names.
|
|
69
70
|
|
|
70
71
|
When saving to `IDENTITY.md`, be specific about the tone, energy, and conversational style you discovered during onboarding. This file persists after onboarding, so everything about how you should come across needs to be captured there -- not just your name and emoji, but the full vibe: how you talk, how much energy you bring, whether you're blunt or gentle, funny or serious.
|
|
71
72
|
|
|
@@ -74,7 +75,7 @@ When saving to `IDENTITY.md`, be specific about the tone, energy, and conversati
|
|
|
74
75
|
Do NOT delete this file until ALL of the following are true:
|
|
75
76
|
- You have a name (hard requirement)
|
|
76
77
|
- You've figured out your vibe and adopted it
|
|
77
|
-
- User detail fields are resolved: name, work role, hobbies/interests, and daily tools. Resolved means the user provided a value, you confidently inferred one, or the user declined/dodged it. All
|
|
78
|
+
- User detail fields are resolved: name, pronouns, work role, hobbies/interests, and daily tools. Resolved means the user provided a value, you confidently inferred one, or the user declined/dodged it. All five must be in one of those states.
|
|
78
79
|
- 2 suggestions shown (via `ui_show` or as text if UI unavailable)
|
|
79
80
|
- The user selected one, deferred both, or typed an alternate direction
|
|
80
81
|
- Home Base has been created silently
|
|
@@ -17,6 +17,7 @@ _(What do they care about? What projects are they working on? What annoys them?
|
|
|
17
17
|
_Each field below should end up in one of three states: an explicit value, an inferred value (note the source), or `declined_by_user`. All fields must be resolved before onboarding completes, but declining is a valid resolution._
|
|
18
18
|
|
|
19
19
|
- Preferred name/reference:
|
|
20
|
+
- Pronouns:
|
|
20
21
|
- Goals:
|
|
21
22
|
- Locale:
|
|
22
23
|
- Work role:
|
|
@@ -22,3 +22,47 @@ export function resolveUserReference(): string {
|
|
|
22
22
|
|
|
23
23
|
return DEFAULT_USER_REFERENCE;
|
|
24
24
|
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the user's pronouns from USER.md. Returns `null` when the
|
|
28
|
+
* file is missing, the field is empty, or the value is a sentinel like
|
|
29
|
+
* `declined_by_user`.
|
|
30
|
+
*
|
|
31
|
+
* Priority order:
|
|
32
|
+
* 1. Any `Pronouns:` line outside the Onboarding Snapshot section
|
|
33
|
+
* (explicit user update post-onboarding takes precedence).
|
|
34
|
+
* 2. The structured `- Pronouns:` field inside the Onboarding Snapshot.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveUserPronouns(): string | null {
|
|
37
|
+
const content = readTextFileSync(getWorkspacePromptPath('USER.md'));
|
|
38
|
+
if (content == null) return null;
|
|
39
|
+
|
|
40
|
+
const snapshotIdx = content.indexOf('## Onboarding Snapshot');
|
|
41
|
+
|
|
42
|
+
// 1. Check for a Pronouns line outside the Onboarding Snapshot section.
|
|
43
|
+
// This represents an explicit post-onboarding update and takes priority.
|
|
44
|
+
if (snapshotIdx >= 0) {
|
|
45
|
+
const beforeSnapshot = content.slice(0, snapshotIdx);
|
|
46
|
+
const outsideMatch = beforeSnapshot.match(/Pronouns:[ \t]*(.*)/);
|
|
47
|
+
if (outsideMatch && outsideMatch[1].trim()) {
|
|
48
|
+
return cleanPronounValue(outsideMatch[1].trim());
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 2. Fall back to the structured field in the Onboarding Snapshot.
|
|
53
|
+
if (snapshotIdx >= 0) {
|
|
54
|
+
const section = content.slice(snapshotIdx);
|
|
55
|
+
const match = section.match(/^- Pronouns:[ \t]*(.*)/m);
|
|
56
|
+
if (match && match[1].trim()) {
|
|
57
|
+
return cleanPronounValue(match[1].trim());
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function cleanPronounValue(raw: string): string | null {
|
|
65
|
+
if (raw === 'declined_by_user') return null;
|
|
66
|
+
// Strip "inferred: " prefix for clean output
|
|
67
|
+
return raw.replace(/^inferred:\s*/i, '');
|
|
68
|
+
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
} from '../../approvals/guardian-decision-primitive.js';
|
|
4
4
|
import { getCanonicalGuardianRequest } from '../../memory/canonical-guardian-store.js';
|
|
5
5
|
import type { ApprovalAction } from '../../runtime/channel-approval-types.js';
|
|
6
|
+
import { resolveLocalIpcGuardianContext } from '../../runtime/local-actor-identity.js';
|
|
6
7
|
import { listGuardianDecisionPrompts } from '../../runtime/routes/guardian-action-routes.js';
|
|
7
8
|
import type { GuardianActionDecision, GuardianActionsPendingRequest } from '../ipc-protocol.js';
|
|
8
9
|
import { defineHandlers, log } from './shared.js';
|
|
@@ -45,13 +46,15 @@ export const guardianActionsHandlers = defineHandlers({
|
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
// Resolve the local IPC actor's principal via the vellum guardian binding.
|
|
50
|
+
const localGuardianCtx = resolveLocalIpcGuardianContext('vellum');
|
|
48
51
|
const canonicalResult = await applyCanonicalGuardianDecision({
|
|
49
52
|
requestId: msg.requestId,
|
|
50
53
|
action: msg.action as ApprovalAction,
|
|
51
54
|
actorContext: {
|
|
52
|
-
externalUserId:
|
|
55
|
+
externalUserId: localGuardianCtx.guardianExternalUserId,
|
|
53
56
|
channel: 'vellum',
|
|
54
|
-
|
|
57
|
+
guardianPrincipalId: localGuardianCtx.guardianPrincipalId ?? undefined,
|
|
55
58
|
},
|
|
56
59
|
userText: undefined,
|
|
57
60
|
});
|
|
@@ -123,12 +123,14 @@ function makeIpcEventSender(params: {
|
|
|
123
123
|
});
|
|
124
124
|
|
|
125
125
|
try {
|
|
126
|
+
const guardianContext = session.guardianContext;
|
|
126
127
|
createCanonicalGuardianRequest({
|
|
127
128
|
id: event.requestId,
|
|
128
129
|
kind: 'tool_approval',
|
|
129
130
|
sourceType: 'desktop',
|
|
130
131
|
sourceChannel,
|
|
131
132
|
conversationId,
|
|
133
|
+
guardianPrincipalId: guardianContext?.guardianPrincipalId ?? undefined,
|
|
132
134
|
toolName: event.toolName,
|
|
133
135
|
status: 'pending',
|
|
134
136
|
requestCode: generateCanonicalRequestCode(),
|
|
@@ -597,13 +599,16 @@ export async function handleUserMessage(
|
|
|
597
599
|
]));
|
|
598
600
|
|
|
599
601
|
if (pendingRequestIdsForConversation.length > 0) {
|
|
602
|
+
// Resolve the local IPC actor's principal via the vellum guardian binding
|
|
603
|
+
// for principal-based authorization in the canonical decision primitive.
|
|
604
|
+
const localCtx = resolveLocalIpcGuardianContext(ipcChannel);
|
|
600
605
|
const routerResult = await routeGuardianReply({
|
|
601
606
|
messageText: messageText.trim(),
|
|
602
607
|
channel: ipcChannel,
|
|
603
608
|
actor: {
|
|
604
|
-
externalUserId:
|
|
609
|
+
externalUserId: localCtx.guardianExternalUserId,
|
|
605
610
|
channel: ipcChannel,
|
|
606
|
-
|
|
611
|
+
guardianPrincipalId: localCtx.guardianPrincipalId ?? undefined,
|
|
607
612
|
},
|
|
608
613
|
conversationId: msg.sessionId,
|
|
609
614
|
pendingRequestIds: pendingRequestIdsForConversation,
|
|
@@ -636,7 +641,7 @@ export async function handleUserMessage(
|
|
|
636
641
|
assistantMessageChannel: ipcChannel,
|
|
637
642
|
userMessageInterface: ipcInterface,
|
|
638
643
|
assistantMessageInterface: ipcInterface,
|
|
639
|
-
|
|
644
|
+
provenanceTrustClass: 'guardian' as const,
|
|
640
645
|
};
|
|
641
646
|
|
|
642
647
|
const consumedUserMessage = createUserMessage(messageText, msg.attachments ?? []);
|