@vellumai/assistant 0.4.30 → 0.4.32
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 +1 -1
- package/Dockerfile +14 -8
- package/README.md +2 -2
- package/docs/architecture/memory.md +28 -29
- package/docs/runbook-trusted-contacts.md +1 -4
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -7
- package/src/__tests__/anthropic-provider.test.ts +86 -1
- package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
- package/src/__tests__/checker.test.ts +37 -98
- package/src/__tests__/commit-message-enrichment-service.test.ts +15 -4
- package/src/__tests__/config-schema.test.ts +6 -14
- package/src/__tests__/conflict-policy.test.ts +76 -0
- package/src/__tests__/conflict-store.test.ts +14 -20
- package/src/__tests__/contacts-tools.test.ts +8 -61
- package/src/__tests__/contradiction-checker.test.ts +5 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/daemon-server-session-init.test.ts +1 -19
- package/src/__tests__/followup-tools.test.ts +0 -30
- package/src/__tests__/gemini-provider.test.ts +79 -1
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
- package/src/__tests__/guardian-routing-invariants.test.ts +6 -4
- package/src/__tests__/ipc-snapshot.test.ts +0 -4
- package/src/__tests__/managed-proxy-context.test.ts +163 -0
- package/src/__tests__/memory-lifecycle-e2e.test.ts +13 -12
- package/src/__tests__/memory-regressions.test.ts +6 -6
- package/src/__tests__/openai-provider.test.ts +82 -0
- package/src/__tests__/provider-fail-open-selection.test.ts +134 -1
- package/src/__tests__/provider-managed-proxy-integration.test.ts +269 -0
- package/src/__tests__/recurrence-types.test.ts +0 -15
- package/src/__tests__/registry.test.ts +0 -10
- package/src/__tests__/schedule-tools.test.ts +28 -44
- package/src/__tests__/script-proxy-session-runtime.test.ts +6 -1
- package/src/__tests__/session-agent-loop.test.ts +0 -2
- package/src/__tests__/session-conflict-gate.test.ts +243 -388
- package/src/__tests__/session-profile-injection.test.ts +0 -2
- package/src/__tests__/session-runtime-assembly.test.ts +2 -3
- package/src/__tests__/session-skill-tools.test.ts +0 -49
- package/src/__tests__/session-workspace-cache-state.test.ts +0 -1
- package/src/__tests__/session-workspace-injection.test.ts +0 -1
- package/src/__tests__/session-workspace-tool-tracking.test.ts +0 -1
- package/src/__tests__/skill-feature-flags.test.ts +2 -2
- package/src/__tests__/task-management-tools.test.ts +111 -0
- package/src/__tests__/tool-grant-request-escalation.test.ts +2 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -1
- package/src/__tests__/twilio-config.test.ts +0 -3
- package/src/amazon/session.ts +30 -91
- package/src/approvals/guardian-decision-primitive.ts +11 -7
- package/src/approvals/guardian-request-resolvers.ts +5 -3
- package/src/calls/call-controller.ts +423 -571
- package/src/calls/finalize-call.ts +20 -0
- package/src/calls/relay-access-wait.ts +340 -0
- package/src/calls/relay-server.ts +269 -899
- package/src/calls/relay-setup-router.ts +307 -0
- package/src/calls/relay-verification.ts +280 -0
- package/src/calls/twilio-config.ts +1 -8
- package/src/calls/voice-control-protocol.ts +184 -0
- package/src/calls/voice-session-bridge.ts +1 -8
- package/src/config/agent-schema.ts +1 -1
- package/src/config/bundled-skills/contacts/SKILL.md +7 -18
- package/src/config/bundled-skills/contacts/TOOLS.json +4 -20
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +2 -4
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +6 -12
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +3 -24
- package/src/config/bundled-skills/followups/TOOLS.json +0 -4
- package/src/config/bundled-skills/schedule/SKILL.md +1 -1
- package/src/config/bundled-skills/schedule/TOOLS.json +2 -10
- package/src/config/bundled-tool-registry.ts +0 -5
- package/src/config/core-schema.ts +1 -1
- package/src/config/env.ts +0 -10
- package/src/config/feature-flag-registry.json +1 -1
- package/src/config/loader.ts +19 -0
- package/src/config/memory-schema.ts +0 -10
- package/src/config/schema.ts +2 -2
- package/src/config/system-prompt.ts +6 -0
- package/src/contacts/contact-store.ts +36 -62
- package/src/contacts/contacts-write.ts +14 -3
- package/src/contacts/types.ts +9 -4
- package/src/daemon/handlers/config-heartbeat.ts +1 -2
- package/src/daemon/handlers/contacts.ts +2 -2
- package/src/daemon/handlers/guardian-actions.ts +1 -1
- package/src/daemon/handlers/session-history.ts +398 -0
- package/src/daemon/handlers/session-user-message.ts +982 -0
- package/src/daemon/handlers/sessions.ts +9 -1337
- package/src/daemon/ipc-contract/contacts.ts +2 -2
- package/src/daemon/ipc-contract/sessions.ts +0 -6
- package/src/daemon/ipc-contract-inventory.json +0 -1
- package/src/daemon/lifecycle.ts +0 -29
- package/src/daemon/session-agent-loop.ts +1 -45
- package/src/daemon/session-conflict-gate.ts +21 -82
- package/src/daemon/session-memory.ts +7 -52
- package/src/daemon/session-process.ts +3 -1
- package/src/daemon/session-runtime-assembly.ts +18 -35
- package/src/heartbeat/heartbeat-service.ts +5 -1
- package/src/home-base/app-link-store.ts +0 -7
- package/src/memory/conflict-intent.ts +3 -6
- package/src/memory/conflict-policy.ts +34 -0
- package/src/memory/conflict-store.ts +10 -18
- package/src/memory/contradiction-checker.ts +2 -2
- package/src/memory/conversation-attention-store.ts +1 -1
- package/src/memory/conversation-store.ts +0 -51
- package/src/memory/db-init.ts +8 -0
- package/src/memory/job-handlers/conflict.ts +24 -7
- package/src/memory/migrations/105-contacts-and-triage.ts +4 -7
- package/src/memory/migrations/134-contacts-notes-column.ts +68 -0
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +31 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/migrations/registry.ts +6 -0
- package/src/memory/recall-cache.ts +0 -5
- package/src/memory/schema/calls.ts +274 -0
- package/src/memory/schema/contacts.ts +125 -0
- package/src/memory/schema/conversations.ts +129 -0
- package/src/memory/schema/guardian.ts +172 -0
- package/src/memory/schema/index.ts +8 -0
- package/src/memory/schema/infrastructure.ts +205 -0
- package/src/memory/schema/memory-core.ts +196 -0
- package/src/memory/schema/notifications.ts +191 -0
- package/src/memory/schema/tasks.ts +78 -0
- package/src/memory/schema.ts +1 -1402
- package/src/memory/slack-thread-store.ts +0 -69
- package/src/messaging/index.ts +0 -1
- package/src/messaging/types.ts +0 -38
- package/src/notifications/decisions-store.ts +2 -105
- package/src/notifications/deliveries-store.ts +0 -11
- package/src/notifications/preferences-store.ts +1 -58
- package/src/permissions/checker.ts +6 -17
- package/src/providers/anthropic/client.ts +6 -2
- package/src/providers/gemini/client.ts +13 -2
- package/src/providers/managed-proxy/constants.ts +55 -0
- package/src/providers/managed-proxy/context.ts +77 -0
- package/src/providers/registry.ts +112 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +51 -23
- package/src/runtime/guardian-action-service.ts +3 -2
- package/src/runtime/guardian-outbound-actions.ts +3 -3
- package/src/runtime/guardian-reply-router.ts +4 -4
- package/src/runtime/http-server.ts +83 -710
- package/src/runtime/http-types.ts +0 -16
- package/src/runtime/middleware/auth.ts +0 -12
- package/src/runtime/routes/app-routes.ts +33 -0
- package/src/runtime/routes/approval-routes.ts +32 -0
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -3
- package/src/runtime/routes/attachment-routes.ts +32 -0
- package/src/runtime/routes/brain-graph-routes.ts +27 -0
- package/src/runtime/routes/call-routes.ts +41 -0
- package/src/runtime/routes/channel-readiness-routes.ts +20 -0
- package/src/runtime/routes/channel-routes.ts +70 -0
- package/src/runtime/routes/contact-routes.ts +371 -29
- package/src/runtime/routes/conversation-attention-routes.ts +15 -0
- package/src/runtime/routes/conversation-routes.ts +192 -194
- package/src/runtime/routes/debug-routes.ts +15 -0
- package/src/runtime/routes/events-routes.ts +16 -0
- package/src/runtime/routes/global-search-routes.ts +17 -2
- package/src/runtime/routes/guardian-action-routes.ts +23 -1
- package/src/runtime/routes/guardian-approval-interception.ts +2 -1
- package/src/runtime/routes/guardian-bootstrap-routes.ts +26 -1
- package/src/runtime/routes/guardian-refresh-routes.ts +20 -0
- package/src/runtime/routes/identity-routes.ts +20 -0
- package/src/runtime/routes/inbound-message-handler.ts +8 -0
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +5 -1
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +2 -1
- package/src/runtime/routes/integration-routes.ts +83 -0
- package/src/runtime/routes/invite-routes.ts +31 -0
- package/src/runtime/routes/migration-routes.ts +47 -17
- package/src/runtime/routes/pairing-routes.ts +18 -0
- package/src/runtime/routes/secret-routes.ts +20 -0
- package/src/runtime/routes/surface-action-routes.ts +26 -0
- package/src/runtime/routes/trust-rules-routes.ts +31 -0
- package/src/runtime/routes/twilio-routes.ts +79 -0
- package/src/schedule/recurrence-types.ts +1 -11
- package/src/tools/followups/followup_create.ts +9 -3
- package/src/tools/mcp/mcp-tool-factory.ts +0 -17
- package/src/tools/memory/definitions.ts +0 -6
- package/src/tools/network/script-proxy/session-manager.ts +38 -3
- package/src/tools/schedule/create.ts +1 -3
- package/src/tools/schedule/update.ts +9 -6
- package/src/twitter/session.ts +29 -77
- package/src/util/cookie-session.ts +114 -0
- package/src/workspace/git-service.ts +6 -4
- package/src/__tests__/conversation-routes.test.ts +0 -99
- package/src/__tests__/get-weather.test.ts +0 -393
- package/src/__tests__/task-tools.test.ts +0 -685
- package/src/__tests__/weather-skill-regression.test.ts +0 -276
- package/src/autonomy/autonomy-resolver.ts +0 -62
- package/src/autonomy/autonomy-store.ts +0 -138
- package/src/autonomy/disposition-mapper.ts +0 -31
- package/src/autonomy/index.ts +0 -11
- package/src/autonomy/types.ts +0 -43
- package/src/config/bundled-skills/weather/SKILL.md +0 -38
- package/src/config/bundled-skills/weather/TOOLS.json +0 -36
- package/src/config/bundled-skills/weather/icon.svg +0 -24
- package/src/config/bundled-skills/weather/tools/get-weather.ts +0 -12
- package/src/contacts/startup-migration.ts +0 -21
- package/src/messaging/triage-engine.ts +0 -344
- package/src/tools/weather/service.ts +0 -712
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Voice call control marker constants, regexes, and stripping utilities.
|
|
3
|
+
*
|
|
4
|
+
* Centralizes all marker definitions so call-controller.ts and
|
|
5
|
+
* voice-session-bridge.ts share a single source of truth.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// String constants
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
export const CALL_OPENING_MARKER = "[CALL_OPENING]";
|
|
13
|
+
export const CALL_OPENING_ACK_MARKER = "[CALL_OPENING_ACK]";
|
|
14
|
+
export const END_CALL_MARKER = "[END_CALL]";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Regexes
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export const ASK_GUARDIAN_CAPTURE_REGEX = /\[ASK_GUARDIAN:\s*(.+?)\]/;
|
|
21
|
+
export const ASK_GUARDIAN_MARKER_REGEX = /\[ASK_GUARDIAN:\s*.+?\]/g;
|
|
22
|
+
|
|
23
|
+
// Flexible prefix for ASK_GUARDIAN_APPROVAL — tolerates variable whitespace
|
|
24
|
+
// after the colon so the marker is recognized even if the model omits the
|
|
25
|
+
// space or inserts a newline.
|
|
26
|
+
export const ASK_GUARDIAN_APPROVAL_PREFIX_RE = /\[ASK_GUARDIAN_APPROVAL:\s*/;
|
|
27
|
+
|
|
28
|
+
export const USER_ANSWERED_MARKER_REGEX = /\[USER_ANSWERED:\s*.+?\]/g;
|
|
29
|
+
export const USER_INSTRUCTION_MARKER_REGEX = /\[USER_INSTRUCTION:\s*.+?\]/g;
|
|
30
|
+
export const CALL_OPENING_MARKER_REGEX = /\[CALL_OPENING\]/g;
|
|
31
|
+
export const CALL_OPENING_ACK_MARKER_REGEX = /\[CALL_OPENING_ACK\]/g;
|
|
32
|
+
export const END_CALL_MARKER_REGEX = /\[END_CALL\]/g;
|
|
33
|
+
export const GUARDIAN_TIMEOUT_MARKER_REGEX = /\[GUARDIAN_TIMEOUT\]/g;
|
|
34
|
+
export const GUARDIAN_UNAVAILABLE_MARKER_REGEX = /\[GUARDIAN_UNAVAILABLE\]/g;
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Balanced JSON extraction (used by stripGuardianApprovalMarkers)
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Extract a balanced JSON object from text that starts with an
|
|
42
|
+
* ASK_GUARDIAN_APPROVAL prefix. Uses brace counting with string-literal
|
|
43
|
+
* awareness so that `}` or `}]` inside JSON string values does not
|
|
44
|
+
* terminate the match prematurely.
|
|
45
|
+
*
|
|
46
|
+
* Returns the extracted JSON string, the full marker text
|
|
47
|
+
* (prefix + JSON + "]"), and the start index — or null when:
|
|
48
|
+
* - no prefix is found,
|
|
49
|
+
* - braces are unbalanced (still streaming), or
|
|
50
|
+
* - the closing `]` has not yet arrived (prevents stripping
|
|
51
|
+
* the marker body while the bracket leaks into TTS in a later delta).
|
|
52
|
+
*/
|
|
53
|
+
export function extractBalancedJson(
|
|
54
|
+
text: string,
|
|
55
|
+
): { json: string; fullMatch: string; startIndex: number } | null {
|
|
56
|
+
const prefixMatch = ASK_GUARDIAN_APPROVAL_PREFIX_RE.exec(text);
|
|
57
|
+
if (!prefixMatch) return null;
|
|
58
|
+
|
|
59
|
+
const prefixIdx = prefixMatch.index;
|
|
60
|
+
const jsonStart = prefixIdx + prefixMatch[0].length;
|
|
61
|
+
if (jsonStart >= text.length || text[jsonStart] !== "{") return null;
|
|
62
|
+
|
|
63
|
+
let depth = 0;
|
|
64
|
+
let inString = false;
|
|
65
|
+
let escape = false;
|
|
66
|
+
|
|
67
|
+
for (let i = jsonStart; i < text.length; i++) {
|
|
68
|
+
const ch = text[i];
|
|
69
|
+
|
|
70
|
+
if (escape) {
|
|
71
|
+
escape = false;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (ch === "\\" && inString) {
|
|
76
|
+
escape = true;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (ch === '"') {
|
|
81
|
+
inString = !inString;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (inString) continue;
|
|
86
|
+
|
|
87
|
+
if (ch === "{") {
|
|
88
|
+
depth++;
|
|
89
|
+
} else if (ch === "}") {
|
|
90
|
+
depth--;
|
|
91
|
+
if (depth === 0) {
|
|
92
|
+
const jsonEnd = i + 1;
|
|
93
|
+
const json = text.slice(jsonStart, jsonEnd);
|
|
94
|
+
// Skip any whitespace between the closing '}' and the expected ']'.
|
|
95
|
+
// Models sometimes emit formatted markers with spaces or newlines
|
|
96
|
+
// before the bracket (e.g. `{ ... }\n]` or `{ ... } ]`).
|
|
97
|
+
let bracketIdx = jsonEnd;
|
|
98
|
+
while (bracketIdx < text.length && /\s/.test(text[bracketIdx])) {
|
|
99
|
+
bracketIdx++;
|
|
100
|
+
}
|
|
101
|
+
// Require the closing ']' to be present before considering this
|
|
102
|
+
// a complete match. If it hasn't arrived yet (streaming), return
|
|
103
|
+
// null so the caller keeps buffering.
|
|
104
|
+
if (bracketIdx >= text.length || text[bracketIdx] !== "]") {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const fullMatchEnd = bracketIdx + 1;
|
|
108
|
+
const fullMatch = text.slice(prefixIdx, fullMatchEnd);
|
|
109
|
+
return { json, fullMatch, startIndex: prefixIdx };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return null; // Unbalanced braces — still streaming
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Marker stripping
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Strip all balanced ASK_GUARDIAN_APPROVAL markers from text, handling
|
|
123
|
+
* nested braces, string literals, and flexible whitespace correctly.
|
|
124
|
+
* Only strips complete markers (prefix + balanced JSON + closing `]`).
|
|
125
|
+
*/
|
|
126
|
+
export function stripGuardianApprovalMarkers(text: string): string {
|
|
127
|
+
let result = text;
|
|
128
|
+
for (;;) {
|
|
129
|
+
const match = extractBalancedJson(result);
|
|
130
|
+
if (!match) break;
|
|
131
|
+
result =
|
|
132
|
+
result.slice(0, match.startIndex) +
|
|
133
|
+
result.slice(match.startIndex + match.fullMatch.length);
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function stripInternalSpeechMarkers(text: string): string {
|
|
139
|
+
let result = stripGuardianApprovalMarkers(text);
|
|
140
|
+
result = result
|
|
141
|
+
.replace(ASK_GUARDIAN_MARKER_REGEX, "")
|
|
142
|
+
.replace(USER_ANSWERED_MARKER_REGEX, "")
|
|
143
|
+
.replace(USER_INSTRUCTION_MARKER_REGEX, "")
|
|
144
|
+
.replace(CALL_OPENING_MARKER_REGEX, "")
|
|
145
|
+
.replace(CALL_OPENING_ACK_MARKER_REGEX, "")
|
|
146
|
+
.replace(END_CALL_MARKER_REGEX, "")
|
|
147
|
+
.replace(GUARDIAN_TIMEOUT_MARKER_REGEX, "")
|
|
148
|
+
.replace(GUARDIAN_UNAVAILABLE_MARKER_REGEX, "");
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Control marker detection
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* All known control marker prefixes. Used by couldBeControlMarker to detect
|
|
158
|
+
* whether a buffer that starts with `[` might be the beginning of a control
|
|
159
|
+
* marker (and should therefore be held rather than flushed to TTS).
|
|
160
|
+
*/
|
|
161
|
+
const CONTROL_MARKER_STRINGS = [
|
|
162
|
+
"[ASK_GUARDIAN_APPROVAL:",
|
|
163
|
+
"[ASK_GUARDIAN:",
|
|
164
|
+
"[USER_ANSWERED:",
|
|
165
|
+
"[USER_INSTRUCTION:",
|
|
166
|
+
"[CALL_OPENING]",
|
|
167
|
+
"[CALL_OPENING_ACK]",
|
|
168
|
+
"[END_CALL]",
|
|
169
|
+
"[GUARDIAN_TIMEOUT]",
|
|
170
|
+
"[GUARDIAN_UNAVAILABLE]",
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Check whether `text` could be a partial or complete control marker.
|
|
175
|
+
*
|
|
176
|
+
* Returns true if any known marker string is a prefix of `text`
|
|
177
|
+
* (text starts with the marker) or `text` is a prefix of a marker
|
|
178
|
+
* (the marker starts with text — i.e. text is still being streamed).
|
|
179
|
+
*/
|
|
180
|
+
export function couldBeControlMarker(text: string): boolean {
|
|
181
|
+
return CONTROL_MARKER_STRINGS.some(
|
|
182
|
+
(marker) => marker.startsWith(text) || text.startsWith(marker),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
@@ -24,14 +24,7 @@ import { checkIngressForSecrets } from "../security/secret-ingress.js";
|
|
|
24
24
|
import { computeToolApprovalDigest } from "../security/tool-approval-digest.js";
|
|
25
25
|
import { IngressBlockedError } from "../util/errors.js";
|
|
26
26
|
import { getLogger } from "../util/logger.js";
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Matches the exact `[CALL_OPENING]` marker that call-controller sends for
|
|
30
|
-
* the initial greeting turn. We replace it with a benign content string before
|
|
31
|
-
* persisting so the marker never appears in session history where it could
|
|
32
|
-
* retrigger opener behavior after a barge-in interruption.
|
|
33
|
-
*/
|
|
34
|
-
const CALL_OPENING_MARKER = "[CALL_OPENING]";
|
|
27
|
+
import { CALL_OPENING_MARKER } from "./voice-control-protocol.js";
|
|
35
28
|
|
|
36
29
|
const log = getLogger("voice-session-bridge");
|
|
37
30
|
|
|
@@ -82,7 +82,7 @@ export const SwarmConfigSchema = z.object({
|
|
|
82
82
|
coder: z.number().int().positive().optional(),
|
|
83
83
|
reviewer: z.number().int().positive().optional(),
|
|
84
84
|
})
|
|
85
|
-
.default({}
|
|
85
|
+
.default({}),
|
|
86
86
|
plannerModelIntent: z
|
|
87
87
|
.enum(["latency-optimized", "quality-optimized", "vision-optimized"], {
|
|
88
88
|
error: "swarm.plannerModelIntent must be a valid model intent",
|
|
@@ -25,10 +25,7 @@ curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/contacts" \
|
|
|
25
25
|
-H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
|
|
26
26
|
-d '{
|
|
27
27
|
"displayName": "<name>",
|
|
28
|
-
"
|
|
29
|
-
"importance": 0.5,
|
|
30
|
-
"responseExpectation": "<speed>",
|
|
31
|
-
"preferredTone": "<tone>",
|
|
28
|
+
"notes": "<free-text notes about this contact>",
|
|
32
29
|
"channels": [
|
|
33
30
|
{
|
|
34
31
|
"type": "<channel_type>",
|
|
@@ -48,15 +45,12 @@ Required fields:
|
|
|
48
45
|
Optional fields:
|
|
49
46
|
|
|
50
47
|
- `id` -- contact ID to update (omit to create new, or auto-match by channel address)
|
|
51
|
-
- `
|
|
52
|
-
- `importance` -- score from 0 to 1 (default 0.5), higher means more important
|
|
53
|
-
- `responseExpectation` -- expected response speed: immediate, within_hours, within_day, casual
|
|
54
|
-
- `preferredTone` -- communication tone: formal, casual, friendly, professional
|
|
48
|
+
- `notes` -- free-text notes about this contact (e.g. relationship, communication preferences, response expectations)
|
|
55
49
|
- `channels` -- list of communication channels
|
|
56
50
|
|
|
57
51
|
### Search contacts
|
|
58
52
|
|
|
59
|
-
Search for contacts by name, channel address,
|
|
53
|
+
Search for contacts by name, channel address, or other criteria using the gateway API.
|
|
60
54
|
|
|
61
55
|
```bash
|
|
62
56
|
curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/contacts?query=<search_term>" \
|
|
@@ -68,7 +62,6 @@ Optional query parameters:
|
|
|
68
62
|
- `query` -- search by display name (partial match)
|
|
69
63
|
- `channelAddress` -- search by channel address (email, phone, handle)
|
|
70
64
|
- `channelType` -- filter by channel type when searching by address
|
|
71
|
-
- `relationship` -- filter by relationship type (exact match)
|
|
72
65
|
- `limit` -- maximum results to return (default 50, max 100)
|
|
73
66
|
|
|
74
67
|
### Merge contacts
|
|
@@ -76,7 +69,7 @@ Optional query parameters:
|
|
|
76
69
|
When you discover two contacts are the same person (e.g. same person on email and Slack), merge them to consolidate. Merging:
|
|
77
70
|
|
|
78
71
|
- Combines all channels from both contacts
|
|
79
|
-
-
|
|
72
|
+
- Merges notes from both contacts
|
|
80
73
|
- Sums interaction counts
|
|
81
74
|
- Deletes the donor contact
|
|
82
75
|
|
|
@@ -404,10 +397,7 @@ Replace `<invite_id>` with the invite's `id` from the list response. The same re
|
|
|
404
397
|
## Contact Fields
|
|
405
398
|
|
|
406
399
|
- **displayName** -- the contact's name (required)
|
|
407
|
-
- **
|
|
408
|
-
- **importance** -- score from 0 to 1 (default 0.5), higher means more important
|
|
409
|
-
- **responseExpectation** -- expected response speed: immediate, within_hours, within_day, casual
|
|
410
|
-
- **preferredTone** -- communication tone: formal, casual, friendly, professional
|
|
400
|
+
- **notes** -- free-text notes about this contact (e.g. relationship, communication preferences, response expectations)
|
|
411
401
|
- **channels** -- list of communication channels (email, slack, whatsapp, phone, telegram, discord, other)
|
|
412
402
|
|
|
413
403
|
### Channel Types
|
|
@@ -446,9 +436,8 @@ Each channel has:
|
|
|
446
436
|
## Tips
|
|
447
437
|
|
|
448
438
|
- Use contact search with `channelAddress` to find contacts by their email, phone, or handle.
|
|
449
|
-
- When creating follow-ups, provide a `contact_id` to link the follow-up to a specific contact
|
|
450
|
-
-
|
|
451
|
-
- When merging contacts, the surviving contact keeps the higher importance score and gains all channels from the donor.
|
|
439
|
+
- When creating follow-ups, provide a `contact_id` to link the follow-up to a specific contact.
|
|
440
|
+
- When merging contacts, the surviving contact gains all channels and merged notes from the donor.
|
|
452
441
|
|
|
453
442
|
## Typical Workflows
|
|
454
443
|
|
|
@@ -17,21 +17,9 @@
|
|
|
17
17
|
"type": "string",
|
|
18
18
|
"description": "Display name for the contact"
|
|
19
19
|
},
|
|
20
|
-
"
|
|
20
|
+
"notes": {
|
|
21
21
|
"type": "string",
|
|
22
|
-
"description": "
|
|
23
|
-
},
|
|
24
|
-
"importance": {
|
|
25
|
-
"type": "number",
|
|
26
|
-
"description": "Importance score 0-1 (default 0.5). Higher = more important."
|
|
27
|
-
},
|
|
28
|
-
"response_expectation": {
|
|
29
|
-
"type": "string",
|
|
30
|
-
"description": "Expected response speed (e.g. immediate, within_hours, within_day, casual)"
|
|
31
|
-
},
|
|
32
|
-
"preferred_tone": {
|
|
33
|
-
"type": "string",
|
|
34
|
-
"description": "Preferred communication tone (e.g. formal, casual, friendly, professional)"
|
|
22
|
+
"description": "Free-text notes about this contact (e.g. relationship, communication preferences, response expectations)"
|
|
35
23
|
},
|
|
36
24
|
"channels": {
|
|
37
25
|
"type": "array",
|
|
@@ -76,7 +64,7 @@
|
|
|
76
64
|
},
|
|
77
65
|
{
|
|
78
66
|
"name": "contact_search",
|
|
79
|
-
"description": "Search for contacts by name, channel address,
|
|
67
|
+
"description": "Search for contacts by name, channel address, or other criteria. Returns matching contacts with their channel information.",
|
|
80
68
|
"category": "contacts",
|
|
81
69
|
"risk": "low",
|
|
82
70
|
"input_schema": {
|
|
@@ -94,10 +82,6 @@
|
|
|
94
82
|
"type": "string",
|
|
95
83
|
"description": "Filter by channel type when searching by address (email, slack, whatsapp, phone, etc.)"
|
|
96
84
|
},
|
|
97
|
-
"relationship": {
|
|
98
|
-
"type": "string",
|
|
99
|
-
"description": "Filter by relationship type (exact match)"
|
|
100
|
-
},
|
|
101
85
|
"limit": {
|
|
102
86
|
"type": "number",
|
|
103
87
|
"description": "Maximum results to return (default 20, max 100)"
|
|
@@ -114,7 +98,7 @@
|
|
|
114
98
|
},
|
|
115
99
|
{
|
|
116
100
|
"name": "contact_merge",
|
|
117
|
-
"description": "Merge two contacts when you discover they are the same person (e.g. same person on email and Slack). Combines channels,
|
|
101
|
+
"description": "Merge two contacts when you discover they are the same person (e.g. same person on email and Slack). Combines channels, merges notes, and deletes the donor contact.",
|
|
118
102
|
"category": "contacts",
|
|
119
103
|
"risk": "medium",
|
|
120
104
|
"input_schema": {
|
|
@@ -17,8 +17,7 @@ interface ContactChannel {
|
|
|
17
17
|
interface ContactResponse {
|
|
18
18
|
id: string;
|
|
19
19
|
displayName: string;
|
|
20
|
-
|
|
21
|
-
importance: number;
|
|
20
|
+
notes: string | null;
|
|
22
21
|
interactionCount: number;
|
|
23
22
|
channels: ContactChannel[];
|
|
24
23
|
}
|
|
@@ -96,9 +95,8 @@ export async function executeContactMerge(
|
|
|
96
95
|
``,
|
|
97
96
|
`Surviving contact (${merged.id}):`,
|
|
98
97
|
` Name: ${merged.displayName}`,
|
|
99
|
-
` Importance: ${merged.importance.toFixed(2)}`,
|
|
100
98
|
` Interactions: ${merged.interactionCount}`,
|
|
101
|
-
merged.
|
|
99
|
+
merged.notes ? ` Notes: ${merged.notes}` : null,
|
|
102
100
|
merged.channels.length > 0 ? ` Channels:\n${channelList}` : null,
|
|
103
101
|
``,
|
|
104
102
|
`Deleted contact: ${mergeContact.displayName} (${mergeId})`,
|
|
@@ -16,20 +16,16 @@ interface ContactChannel {
|
|
|
16
16
|
interface ContactResponse {
|
|
17
17
|
id: string;
|
|
18
18
|
displayName: string;
|
|
19
|
-
|
|
20
|
-
importance: number;
|
|
19
|
+
notes: string | null;
|
|
21
20
|
interactionCount: number;
|
|
22
21
|
channels: ContactChannel[];
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
function formatContactSummary(c: ContactResponse): string {
|
|
26
25
|
const parts = [`- **${c.displayName}** (ID: ${c.id})`];
|
|
27
|
-
if (c.
|
|
28
|
-
|
|
29
|
-
`
|
|
30
|
-
c.interactionCount
|
|
31
|
-
}`,
|
|
32
|
-
);
|
|
26
|
+
if (c.notes) parts.push(` Notes: ${c.notes}`);
|
|
27
|
+
if (c.interactionCount > 0)
|
|
28
|
+
parts.push(` Interactions: ${c.interactionCount}`);
|
|
33
29
|
if (c.channels.length > 0) {
|
|
34
30
|
const channelList = c.channels
|
|
35
31
|
.map((ch) => `${ch.type}:${ch.address}${ch.isPrimary ? "*" : ""}`)
|
|
@@ -46,13 +42,12 @@ export async function executeContactSearch(
|
|
|
46
42
|
const query = input.query as string | undefined;
|
|
47
43
|
const channelAddress = input.channel_address as string | undefined;
|
|
48
44
|
const channelType = input.channel_type as string | undefined;
|
|
49
|
-
const relationship = input.relationship as string | undefined;
|
|
50
45
|
const limit = input.limit as number | undefined;
|
|
51
46
|
|
|
52
|
-
if (!query && !channelAddress
|
|
47
|
+
if (!query && !channelAddress) {
|
|
53
48
|
return {
|
|
54
49
|
content:
|
|
55
|
-
"Error: At least one search criterion is required (query
|
|
50
|
+
"Error: At least one search criterion is required (query or channel_address)",
|
|
56
51
|
isError: true,
|
|
57
52
|
};
|
|
58
53
|
}
|
|
@@ -62,7 +57,6 @@ export async function executeContactSearch(
|
|
|
62
57
|
if (query) params.set("query", query);
|
|
63
58
|
if (channelAddress) params.set("channelAddress", channelAddress);
|
|
64
59
|
if (channelType) params.set("channelType", channelType);
|
|
65
|
-
if (relationship) params.set("relationship", relationship);
|
|
66
60
|
if (limit !== undefined) params.set("limit", String(limit));
|
|
67
61
|
|
|
68
62
|
const qs = params.toString();
|
|
@@ -16,21 +16,14 @@ interface ContactChannel {
|
|
|
16
16
|
interface ContactResponse {
|
|
17
17
|
id: string;
|
|
18
18
|
displayName: string;
|
|
19
|
-
|
|
20
|
-
importance: number;
|
|
21
|
-
responseExpectation: string | null;
|
|
22
|
-
preferredTone: string | null;
|
|
19
|
+
notes: string | null;
|
|
23
20
|
interactionCount: number;
|
|
24
21
|
channels: ContactChannel[];
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
function formatContact(c: ContactResponse): string {
|
|
28
25
|
const lines = [`Contact ${c.id}`, ` Name: ${c.displayName}`];
|
|
29
|
-
if (c.
|
|
30
|
-
lines.push(` Importance: ${c.importance.toFixed(2)}`);
|
|
31
|
-
if (c.responseExpectation)
|
|
32
|
-
lines.push(` Response expectation: ${c.responseExpectation}`);
|
|
33
|
-
if (c.preferredTone) lines.push(` Preferred tone: ${c.preferredTone}`);
|
|
26
|
+
if (c.notes) lines.push(` Notes: ${c.notes}`);
|
|
34
27
|
if (c.interactionCount > 0)
|
|
35
28
|
lines.push(` Interactions: ${c.interactionCount}`);
|
|
36
29
|
if (c.channels.length > 0) {
|
|
@@ -59,17 +52,6 @@ export async function executeContactUpsert(
|
|
|
59
52
|
};
|
|
60
53
|
}
|
|
61
54
|
|
|
62
|
-
const importance = input.importance as number | undefined;
|
|
63
|
-
if (
|
|
64
|
-
importance !== undefined &&
|
|
65
|
-
(typeof importance !== "number" || importance < 0 || importance > 1)
|
|
66
|
-
) {
|
|
67
|
-
return {
|
|
68
|
-
content: "Error: importance must be a number between 0 and 1",
|
|
69
|
-
isError: true,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
55
|
const rawChannels = input.channels as
|
|
74
56
|
| Array<{ type: string; address: string; is_primary?: boolean }>
|
|
75
57
|
| undefined;
|
|
@@ -86,10 +68,7 @@ export async function executeContactUpsert(
|
|
|
86
68
|
}>("/v1/contacts", {
|
|
87
69
|
id: input.id as string | undefined,
|
|
88
70
|
displayName: displayName.trim(),
|
|
89
|
-
|
|
90
|
-
importance,
|
|
91
|
-
responseExpectation: input.response_expectation as string | undefined,
|
|
92
|
-
preferredTone: input.preferred_tone as string | undefined,
|
|
71
|
+
notes: input.notes as string | undefined,
|
|
93
72
|
channels,
|
|
94
73
|
});
|
|
95
74
|
|
|
@@ -29,10 +29,6 @@
|
|
|
29
29
|
"type": "string",
|
|
30
30
|
"description": "Optional recurrence schedule ID to fire a reminder when overdue"
|
|
31
31
|
},
|
|
32
|
-
"reminder_cron_id": {
|
|
33
|
-
"type": "string",
|
|
34
|
-
"description": "Deprecated alias for reminder_schedule_id. Use reminder_schedule_id instead."
|
|
35
|
-
},
|
|
36
32
|
"reason": {
|
|
37
33
|
"type": "string",
|
|
38
34
|
"description": "Brief non-technical explanation of why this tool is being called"
|
|
@@ -63,7 +63,7 @@ Exclusions (EXDATE, EXRULE) always take precedence over inclusions (RRULE, RDATE
|
|
|
63
63
|
|
|
64
64
|
## Tool Input
|
|
65
65
|
|
|
66
|
-
Use `syntax` + `expression` to specify the schedule type explicitly, or just `expression` to auto-detect.
|
|
66
|
+
Use `syntax` + `expression` to specify the schedule type explicitly, or just `expression` to auto-detect.
|
|
67
67
|
|
|
68
68
|
## Lifecycle
|
|
69
69
|
|
|
@@ -20,11 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"expression": {
|
|
22
22
|
"type": "string",
|
|
23
|
-
"description": "
|
|
24
|
-
},
|
|
25
|
-
"cron_expression": {
|
|
26
|
-
"type": "string",
|
|
27
|
-
"description": "Deprecated alias for expression with cron syntax. Use expression + syntax instead."
|
|
23
|
+
"description": "Schedule expression (cron or RRULE)"
|
|
28
24
|
},
|
|
29
25
|
"timezone": {
|
|
30
26
|
"type": "string",
|
|
@@ -97,11 +93,7 @@
|
|
|
97
93
|
},
|
|
98
94
|
"expression": {
|
|
99
95
|
"type": "string",
|
|
100
|
-
"description": "New schedule expression (cron or RRULE)
|
|
101
|
-
},
|
|
102
|
-
"cron_expression": {
|
|
103
|
-
"type": "string",
|
|
104
|
-
"description": "Deprecated alias for expression with cron syntax. Use expression + syntax instead."
|
|
96
|
+
"description": "New schedule expression (cron or RRULE)"
|
|
105
97
|
},
|
|
106
98
|
"timezone": {
|
|
107
99
|
"type": "string",
|
|
@@ -175,8 +175,6 @@ import * as watcherDelete from "./bundled-skills/watcher/tools/watcher-delete.js
|
|
|
175
175
|
import * as watcherDigest from "./bundled-skills/watcher/tools/watcher-digest.js";
|
|
176
176
|
import * as watcherList from "./bundled-skills/watcher/tools/watcher-list.js";
|
|
177
177
|
import * as watcherUpdate from "./bundled-skills/watcher/tools/watcher-update.js";
|
|
178
|
-
// ── weather ──────────────────────────────────────────────────────────────────
|
|
179
|
-
import * as getWeather from "./bundled-skills/weather/tools/get-weather.js";
|
|
180
178
|
|
|
181
179
|
// ─── Registry ────────────────────────────────────────────────────────────────
|
|
182
180
|
|
|
@@ -375,7 +373,4 @@ export const bundledToolRegistry = new Map<string, SkillToolScript>([
|
|
|
375
373
|
["watcher:tools/watcher-update.ts", watcherUpdate],
|
|
376
374
|
["watcher:tools/watcher-delete.ts", watcherDelete],
|
|
377
375
|
["watcher:tools/watcher-digest.ts", watcherDigest],
|
|
378
|
-
|
|
379
|
-
// weather
|
|
380
|
-
["weather:tools/get-weather.ts", getWeather],
|
|
381
376
|
]);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
3
|
const VALID_SECRET_ACTIONS = ["redact", "warn", "block", "prompt"] as const;
|
|
4
|
-
const VALID_PERMISSIONS_MODES = ["
|
|
4
|
+
const VALID_PERMISSIONS_MODES = ["strict", "workspace"] as const;
|
|
5
5
|
const VALID_SMS_PROVIDERS = ["twilio"] as const;
|
|
6
6
|
|
|
7
7
|
export const TimeoutConfigSchema = z.object({
|
package/src/config/env.ts
CHANGED
|
@@ -131,10 +131,6 @@ export function getTwilioUserPhoneNumber(): string | undefined {
|
|
|
131
131
|
return str("TWILIO_USER_PHONE_NUMBER");
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
export function getTwilioWssBaseUrl(): string | undefined {
|
|
135
|
-
return str("TWILIO_WSS_BASE_URL");
|
|
136
|
-
}
|
|
137
|
-
|
|
138
134
|
export function isTwilioWebhookValidationDisabled(): boolean {
|
|
139
135
|
// Intentionally strict: only exact "true" disables validation (not "1").
|
|
140
136
|
// This is a security-sensitive bypass — we don't want environments that
|
|
@@ -212,12 +208,6 @@ export function validateEnv(): void {
|
|
|
212
208
|
throw new Error(`Invalid RUNTIME_HTTP_PORT: ${httpPort} (must be 1-65535)`);
|
|
213
209
|
}
|
|
214
210
|
|
|
215
|
-
if (getTwilioWssBaseUrl()) {
|
|
216
|
-
log.warn(
|
|
217
|
-
"TWILIO_WSS_BASE_URL env var is deprecated. Relay URL is now derived from ingress.publicBaseUrl.",
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
211
|
for (const warning of checkUnrecognizedEnvVars()) {
|
|
222
212
|
log.warn(warning);
|
|
223
213
|
}
|
package/src/config/loader.ts
CHANGED
|
@@ -55,6 +55,21 @@ function ensureMigratedDataDir(): void {
|
|
|
55
55
|
ensureDataDir();
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Migrate deprecated raw config values before Zod validation.
|
|
60
|
+
* This prevents `validateWithSchema` from silently dropping fields whose
|
|
61
|
+
* values were valid in older releases but have since been removed from the
|
|
62
|
+
* schema enum, which would cause a fallback to the default and silently
|
|
63
|
+
* change behavior on upgrade.
|
|
64
|
+
*/
|
|
65
|
+
function migrateRawConfig(raw: Record<string, unknown>): void {
|
|
66
|
+
const permissions = raw.permissions as Record<string, unknown> | undefined;
|
|
67
|
+
if (permissions?.mode === "legacy") {
|
|
68
|
+
permissions.mode = "workspace";
|
|
69
|
+
log.info('Migrated permissions.mode from "legacy" to "workspace".');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
58
73
|
/**
|
|
59
74
|
* Zod 4's .default({}) returns {} as output without running inner-schema
|
|
60
75
|
* parsing, so nested object defaults are never applied. Re-parse the config
|
|
@@ -227,6 +242,10 @@ export function loadConfig(): AssistantConfig {
|
|
|
227
242
|
}
|
|
228
243
|
}
|
|
229
244
|
|
|
245
|
+
// Migrate removed config values before validation so existing configs
|
|
246
|
+
// don't silently change behavior when Zod drops unknown enum values.
|
|
247
|
+
migrateRawConfig(fileConfig);
|
|
248
|
+
|
|
230
249
|
// Validate and apply defaults via Zod schema
|
|
231
250
|
const config = validateWithSchema(fileConfig);
|
|
232
251
|
|
|
@@ -509,11 +509,6 @@ export const MemoryConflictsConfigSchema = z.object({
|
|
|
509
509
|
gateMode: z
|
|
510
510
|
.enum(["soft"], { error: 'memory.conflicts.gateMode must be "soft"' })
|
|
511
511
|
.default("soft"),
|
|
512
|
-
reaskCooldownTurns: z
|
|
513
|
-
.number({ error: "memory.conflicts.reaskCooldownTurns must be a number" })
|
|
514
|
-
.int("memory.conflicts.reaskCooldownTurns must be an integer")
|
|
515
|
-
.positive("memory.conflicts.reaskCooldownTurns must be a positive integer")
|
|
516
|
-
.default(3),
|
|
517
512
|
resolverLlmTimeoutMs: z
|
|
518
513
|
.number({ error: "memory.conflicts.resolverLlmTimeoutMs must be a number" })
|
|
519
514
|
.int("memory.conflicts.resolverLlmTimeoutMs must be an integer")
|
|
@@ -526,11 +521,6 @@ export const MemoryConflictsConfigSchema = z.object({
|
|
|
526
521
|
.min(0, "memory.conflicts.relevanceThreshold must be >= 0")
|
|
527
522
|
.max(1, "memory.conflicts.relevanceThreshold must be <= 1")
|
|
528
523
|
.default(0.3),
|
|
529
|
-
askOnIrrelevantTurns: z
|
|
530
|
-
.boolean({
|
|
531
|
-
error: "memory.conflicts.askOnIrrelevantTurns must be a boolean",
|
|
532
|
-
})
|
|
533
|
-
.default(false),
|
|
534
524
|
conflictableKinds: z
|
|
535
525
|
.array(
|
|
536
526
|
z.enum(VALID_MEMORY_ITEM_KINDS, {
|
package/src/config/schema.ts
CHANGED
|
@@ -201,7 +201,7 @@ export const AssistantConfigSchema = z
|
|
|
201
201
|
z.string(),
|
|
202
202
|
z.string({ error: "Each apiKeys value must be a string" }),
|
|
203
203
|
)
|
|
204
|
-
.default({} as
|
|
204
|
+
.default({} as Record<string, string>),
|
|
205
205
|
webSearchProvider: z
|
|
206
206
|
.enum(VALID_WEB_SEARCH_PROVIDERS, {
|
|
207
207
|
error: `webSearchProvider must be one of: ${VALID_WEB_SEARCH_PROVIDERS.join(
|
|
@@ -273,7 +273,7 @@ export const AssistantConfigSchema = z
|
|
|
273
273
|
z.string(),
|
|
274
274
|
z.boolean({ error: "featureFlags values must be booleans" }),
|
|
275
275
|
)
|
|
276
|
-
.default({} as
|
|
276
|
+
.default({} as Record<string, boolean>),
|
|
277
277
|
assistantFeatureFlagValues: z
|
|
278
278
|
.record(
|
|
279
279
|
z.string(),
|
|
@@ -944,5 +944,11 @@ function formatSkillsCatalog(skills: SkillSummary[]): string {
|
|
|
944
944
|
"When a credential is missing, check if any skill declares `credential-setup-for` matching that service — if so, load that skill.",
|
|
945
945
|
"",
|
|
946
946
|
lines.join("\n"),
|
|
947
|
+
"",
|
|
948
|
+
"### Installing additional skills",
|
|
949
|
+
"If `skill_load` fails because a skill is not found, additional first-party skills may be available in the Vellum catalog.",
|
|
950
|
+
"Use `bash` to discover and install them:",
|
|
951
|
+
"- `vellum skills list` — list all available catalog skills",
|
|
952
|
+
"- `vellum skills install <skill-id>` — install a skill, then retry `skill_load`",
|
|
947
953
|
].join("\n");
|
|
948
954
|
}
|