@vellumai/assistant 0.8.11 → 0.8.12-staging.1
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 +15 -17
- package/README.md +0 -6
- package/bun.lock +6 -122
- package/node_modules/@vellumai/gateway-client/bun.lock +1 -0
- package/node_modules/@vellumai/gateway-client/package.json +3 -1
- package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -1
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +87 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +3 -5
- package/openapi.yaml +126 -4
- package/package.json +1 -3
- package/src/__tests__/adaptive-thinking-repair.test.ts +185 -0
- package/src/__tests__/agent-loop-compaction-events.test.ts +7 -6
- package/src/__tests__/anthropic-provider.test.ts +129 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +4 -1
- package/src/__tests__/btw-routes.test.ts +7 -34
- package/src/__tests__/checker.test.ts +6 -12
- package/src/__tests__/config-loader-backfill.test.ts +4 -2
- package/src/__tests__/config-loader-quarantine-notice.test.ts +167 -0
- package/src/__tests__/config-watcher.test.ts +2 -2
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
- package/src/__tests__/conversation-error.test.ts +2 -6
- package/src/__tests__/conversation-history-web-search.test.ts +8 -0
- package/src/__tests__/conversation-title-service.test.ts +2 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/disk-pressure-tools.test.ts +1 -1
- package/src/__tests__/exploration-drift-hook.test.ts +692 -0
- package/src/__tests__/filing-service.test.ts +8 -3
- package/src/__tests__/guardian-action-store.test.ts +0 -167
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -1
- package/src/__tests__/heartbeat-service.test.ts +5 -2
- package/src/__tests__/identity-intro-cache.test.ts +12 -5
- package/src/__tests__/identity-routes.test.ts +16 -57
- package/src/__tests__/injector-chain.test.ts +8 -3
- package/src/__tests__/injector-config-quarantine-notice.test.ts +115 -0
- package/src/__tests__/llm-usage-store.test.ts +11 -0
- package/src/__tests__/memory-v2-static-injector.test.ts +22 -0
- package/src/__tests__/model-intents.test.ts +1 -1
- package/src/__tests__/oauth-cli.test.ts +19 -8
- package/src/__tests__/openai-provider.test.ts +34 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +0 -1
- package/src/__tests__/recurrence-engine.test.ts +45 -0
- package/src/__tests__/schedule-routes.test.ts +34 -0
- package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
- package/src/__tests__/script-proxy-conversation-manager.test.ts +10 -5
- package/src/__tests__/skill-tool-factory.test.ts +49 -0
- package/src/__tests__/subagent-role-registry.test.ts +24 -1
- package/src/__tests__/subagent-tools.test.ts +1 -0
- package/src/__tests__/system-prompt.test.ts +109 -11
- package/src/__tests__/tool-error-hook.test.ts +1 -0
- package/src/__tests__/tool-result-spool.test.ts +337 -0
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
- package/src/__tests__/validate-input.test.ts +95 -1
- package/src/__tests__/workspace-migration-098-remove-stale-updates-bulletin-file.test.ts +65 -0
- package/src/__tests__/workspace-migration-099-disable-cache-one-shot-callsites.test.ts +139 -0
- package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
- package/src/agent/loop.ts +81 -24
- package/src/api/events/usage-progress.ts +28 -0
- package/src/api/index.ts +6 -0
- package/src/background-wake/wake-intent-hooks.test.ts +2 -0
- package/src/bundler/app-bundler.ts +25 -42
- package/src/calls/call-controller.ts +1 -1
- package/src/cli/commands/plugins.ts +248 -15
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +318 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +16 -9
- package/src/cli/lib/__tests__/plugin-artifact.test.ts +183 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +158 -0
- package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +245 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +301 -0
- package/src/cli/lib/inspect-plugin.ts +252 -0
- package/src/cli/lib/install-from-github.ts +214 -21
- package/src/cli/lib/list-installed-plugins.ts +17 -6
- package/src/cli/lib/plugin-artifact.ts +103 -0
- package/src/cli/lib/plugin-details.ts +18 -1
- package/src/cli/lib/plugin-fingerprint.ts +197 -0
- package/src/cli/lib/upgrade-plugin.ts +219 -0
- package/src/config/bundled-skills/subagent/SKILL.md +2 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +8 -2
- package/src/config/call-site-defaults.ts +13 -2
- package/src/config/feature-flag-registry.json +8 -16
- package/src/config/loader.ts +52 -59
- package/src/config/schema.ts +0 -2
- package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
- package/src/config/schemas/__tests__/memory-v3.test.ts +10 -0
- package/src/config/schemas/llm.ts +10 -0
- package/src/config/schemas/memory-v2.ts +13 -0
- package/src/config/schemas/memory-v3.ts +92 -0
- package/src/context/post-turn-tool-result-truncation.ts +32 -18
- package/src/context/tool-result-spool.ts +104 -0
- package/src/credential-execution/feature-gates.ts +0 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +41 -16
- package/src/daemon/conversation-error.ts +6 -15
- package/src/daemon/conversation.ts +9 -0
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/lifecycle.ts +1 -20
- package/src/daemon/message-types/conversations.ts +2 -15
- package/src/daemon/trust-context.ts +1 -1
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
- package/src/home/__tests__/home-content-refresh.test.ts +114 -0
- package/src/home/__tests__/suggested-prompts.test.ts +86 -5
- package/src/home/home-content-refresh.ts +43 -31
- package/src/home/home-greeting-cache.ts +8 -1
- package/src/home/home-greeting.ts +13 -9
- package/src/home/suggested-prompts.ts +77 -24
- package/src/ipc/routes/trust-rules.test.ts +66 -72
- package/src/media/image-credentials.ts +2 -2
- package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
- package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts} +264 -55
- package/src/memory/conversation-attention-store.ts +1 -0
- package/src/memory/conversation-bootstrap.ts +18 -9
- package/src/memory/conversation-crud.ts +12 -2
- package/src/memory/conversation-title-service.ts +53 -9
- package/src/memory/delivery-channels.ts +0 -69
- package/src/memory/graph/extraction-job.ts +0 -15
- package/src/memory/guardian-action-store.ts +1 -376
- package/src/memory/llm-usage-store.ts +5 -1
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +2 -2
- package/src/memory/v2/__tests__/consolidation-job.test.ts +183 -2
- package/src/memory/v2/__tests__/injection.test.ts +70 -0
- package/src/memory/v2/__tests__/static-context.test.ts +12 -0
- package/src/memory/v2/consolidation-job.ts +93 -9
- package/src/memory/v2/injection.ts +53 -0
- package/src/memory/v2/prompts/consolidation.ts +1 -0
- package/src/memory/v2/static-context.ts +13 -1
- package/src/memory/v2/sweep-job.ts +1 -1
- package/src/memory/v2/types.ts +5 -0
- package/src/plugin-api/types.ts +7 -0
- package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +300 -0
- package/src/plugins/defaults/exploration-drift/package.json +15 -0
- package/src/plugins/defaults/index.ts +25 -0
- package/src/plugins/defaults/memory-retrieval/injectors.ts +132 -4
- package/src/plugins/defaults/memory-v3-shadow/__tests__/card.test.ts +92 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +2 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/fresh-set.test.ts +52 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +2 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +136 -5
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +17 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +6 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +5 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +68 -4
- package/src/plugins/defaults/memory-v3-shadow/card.ts +49 -5
- package/src/plugins/defaults/memory-v3-shadow/fresh-set.ts +59 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +4 -2
- package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +169 -0
- package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +178 -0
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +115 -26
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -9
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +144 -22
- package/src/plugins/defaults/memory-v3-shadow/types.ts +24 -6
- package/src/plugins/defaults/title-generate/hooks/stop.ts +13 -0
- package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +16 -0
- package/src/prompts/cache-boundary.ts +17 -0
- package/src/prompts/sections.ts +50 -17
- package/src/prompts/system-prompt.ts +12 -4
- package/src/prompts/templates/system-sections.ts +22 -0
- package/src/providers/anthropic/client.ts +74 -28
- package/src/providers/gemini/client.ts +5 -1
- package/src/providers/minimax/client.ts +9 -0
- package/src/providers/model-intents.ts +2 -2
- package/src/providers/openai/chat-completions-provider.ts +2 -1
- package/src/providers/openai/responses-provider.ts +5 -1
- package/src/providers/retry.ts +8 -0
- package/src/providers/types.ts +11 -0
- package/src/runtime/AGENTS.md +6 -0
- package/src/runtime/__tests__/agent-wake.test.ts +2 -2
- package/src/runtime/agent-wake.ts +5 -5
- package/src/runtime/background-job-runner.ts +2 -2
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +150 -3
- package/src/runtime/migrations/vbundle-import-analyzer.ts +29 -6
- package/src/runtime/migrations/vbundle-import-policy.ts +23 -0
- package/src/runtime/migrations/vbundle-importer.ts +9 -4
- package/src/runtime/migrations/vbundle-streaming-importer.ts +8 -3
- package/src/runtime/pre-first-message-gate.ts +1 -1
- package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +241 -0
- package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +97 -185
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +17 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +1 -0
- package/src/runtime/routes/__tests__/task-routes.test.ts +3 -3
- package/src/runtime/routes/btw-routes.ts +0 -14
- package/src/runtime/routes/conversation-compaction-routes.ts +86 -19
- package/src/runtime/routes/conversation-list-routes.ts +77 -5
- package/src/runtime/routes/conversation-management-routes.ts +54 -0
- package/src/runtime/routes/gateway-log-routes.ts +14 -64
- package/src/runtime/routes/home-feed-routes.ts +10 -0
- package/src/runtime/routes/identity-intro-cache.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +76 -20
- package/src/runtime/routes/inbound-message-handler.ts +0 -36
- package/src/runtime/routes/plugins-routes.ts +21 -0
- package/src/runtime/routes/schedule-routes.ts +19 -2
- package/src/runtime/routes/trust-rules-routes.ts +14 -67
- package/src/schedule/recurrence-engine.ts +34 -0
- package/src/schedule/scheduler.ts +1 -0
- package/src/skills/validate-input.ts +41 -1
- package/src/subagent/types.ts +26 -1
- package/src/telemetry/types.ts +15 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
- package/src/telemetry/usage-telemetry-reporter.ts +1 -0
- package/src/tools/apps/executors.ts +1 -1
- package/src/tools/skills/skill-tool-factory.ts +19 -8
- package/src/usage/types.ts +8 -1
- package/src/util/platform.ts +16 -0
- package/src/watcher/engine.ts +1 -0
- package/src/workspace/adaptive-thinking-repair.ts +113 -0
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +70 -67
- package/src/workspace/migrations/098-remove-stale-updates-bulletin-file.ts +31 -0
- package/src/workspace/migrations/099-disable-cache-one-shot-callsites.ts +81 -0
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/__tests__/config-loader-quarantine-bulletin.test.ts +0 -202
- package/src/__tests__/conversation-starters-cadence.test.ts +0 -161
- package/src/__tests__/guardian-action-followup-executor.test.ts +0 -322
- package/src/__tests__/guardian-action-followup-store.test.ts +0 -373
- package/src/__tests__/guardian-action-late-reply.test.ts +0 -1083
- package/src/__tests__/update-bulletin-job.test.ts +0 -292
- package/src/config/schemas/updates.ts +0 -14
- package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +0 -227
- package/src/memory/conversation-starters-cadence.ts +0 -78
- package/src/prompts/update-bulletin-job.ts +0 -180
- package/src/runtime/guardian-action-followup-executor.ts +0 -306
|
@@ -1,83 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Trust rule listing route — gateway
|
|
2
|
+
* Trust rule listing route — gateway IPC proxy.
|
|
3
3
|
*
|
|
4
|
-
* The handler
|
|
5
|
-
*
|
|
4
|
+
* The handler calls the gateway over the local IPC socket because trust rule
|
|
5
|
+
* storage is gateway-owned in Docker mode.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
TrustRulesListIpcParamsSchema,
|
|
9
|
+
type TrustRulesListIpcResponse,
|
|
10
|
+
TrustRulesListIpcResponseSchema,
|
|
11
|
+
} from "@vellumai/gateway-client/gateway-ipc-contracts";
|
|
8
12
|
|
|
9
|
-
import {
|
|
13
|
+
import { ipcCallPersistent } from "../../ipc/gateway-client.js";
|
|
10
14
|
import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
|
|
11
15
|
import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
|
|
12
16
|
|
|
13
|
-
// ── Shared helper ───────────────────────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
async function gatewayFetch(
|
|
16
|
-
path: string,
|
|
17
|
-
init?: RequestInit,
|
|
18
|
-
): Promise<unknown> {
|
|
19
|
-
const base = getGatewayInternalBaseUrl();
|
|
20
|
-
const res = await fetch(`${base}${path}`, init);
|
|
21
|
-
if (!res.ok) {
|
|
22
|
-
let message = `Gateway request failed (${res.status})`;
|
|
23
|
-
try {
|
|
24
|
-
const body = (await res.json()) as { error?: unknown };
|
|
25
|
-
if (typeof body.error === "string") {
|
|
26
|
-
message = body.error;
|
|
27
|
-
}
|
|
28
|
-
} catch {
|
|
29
|
-
// ignore JSON parse failures
|
|
30
|
-
}
|
|
31
|
-
throw new Error(message);
|
|
32
|
-
}
|
|
33
|
-
return res.json();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ── Schemas ─────────────────────────────────────────────────────────────
|
|
37
|
-
|
|
38
|
-
const TrustRulesListParams = z
|
|
39
|
-
.object({
|
|
40
|
-
tool: z.string().optional(),
|
|
41
|
-
origin: z.string().optional(),
|
|
42
|
-
include_all: z.boolean().optional(),
|
|
43
|
-
})
|
|
44
|
-
.strict();
|
|
45
|
-
|
|
46
|
-
const TrustRuleSchema = z.object({
|
|
47
|
-
id: z.string(),
|
|
48
|
-
tool: z.string(),
|
|
49
|
-
pattern: z.string(),
|
|
50
|
-
risk: z.enum(["low", "medium", "high"]),
|
|
51
|
-
description: z.string(),
|
|
52
|
-
origin: z.enum(["default", "user_defined"]),
|
|
53
|
-
userModified: z.boolean(),
|
|
54
|
-
deleted: z.boolean(),
|
|
55
|
-
createdAt: z.string(),
|
|
56
|
-
updatedAt: z.string(),
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const TrustRulesListResponseSchema = z.object({
|
|
60
|
-
rules: z.array(TrustRuleSchema),
|
|
61
|
-
});
|
|
62
|
-
type TrustRulesListResponse = z.infer<typeof TrustRulesListResponseSchema>;
|
|
63
|
-
|
|
64
17
|
// ── Handlers ────────────────────────────────────────────────────────────
|
|
65
18
|
|
|
66
19
|
async function handleList({
|
|
67
20
|
queryParams = {},
|
|
68
21
|
body = {},
|
|
69
|
-
}: RouteHandlerArgs): Promise<
|
|
22
|
+
}: RouteHandlerArgs): Promise<TrustRulesListIpcResponse> {
|
|
70
23
|
// HTTP GET delivers filters via queryParams; CLI IPC puts them in body.
|
|
71
24
|
const source = Object.keys(queryParams).length > 0 ? queryParams : body;
|
|
72
|
-
const p =
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
if (p.origin) qs.set("origin", p.origin);
|
|
76
|
-
if (p.include_all) qs.set("include_all", "true");
|
|
77
|
-
const query = qs.toString();
|
|
78
|
-
return gatewayFetch(
|
|
79
|
-
`/v1/trust-rules${query ? `?${query}` : ""}`,
|
|
80
|
-
) as Promise<TrustRulesListResponse>;
|
|
25
|
+
const p = TrustRulesListIpcParamsSchema.parse(source);
|
|
26
|
+
const result = await ipcCallPersistent("trust_rules_list", p);
|
|
27
|
+
return TrustRulesListIpcResponseSchema.parse(result);
|
|
81
28
|
}
|
|
82
29
|
|
|
83
30
|
// ── Route definitions ───────────────────────────────────────────────────
|
|
@@ -96,7 +43,7 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
96
43
|
description:
|
|
97
44
|
"List trust rules, optionally filtered by tool, origin, or include_all.",
|
|
98
45
|
tags: ["trust-rules"],
|
|
99
|
-
responseBody:
|
|
46
|
+
responseBody: TrustRulesListIpcResponseSchema,
|
|
100
47
|
queryParams: [
|
|
101
48
|
{ name: "tool", description: "Filter by tool name" },
|
|
102
49
|
{ name: "origin", description: "Filter by origin" },
|
|
@@ -135,6 +135,40 @@ export function isValidScheduleExpression(spec: ScheduleSpec): boolean {
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Detect whether an RRULE expression fires exactly once — a single RRULE
|
|
140
|
+
* with COUNT=1 and no set constructs. Such schedules are semantically
|
|
141
|
+
* one-shots even though they carry a recurrence expression.
|
|
142
|
+
*/
|
|
143
|
+
export function isSingleFireRRule(expression: string): boolean {
|
|
144
|
+
try {
|
|
145
|
+
const normalized = normalizeRruleExpression(expression);
|
|
146
|
+
if (hasSetConstructs(normalized)) return false;
|
|
147
|
+
const rule = rrulestr(normalized);
|
|
148
|
+
return !(rule instanceof RRuleSet) && rule.options.count === 1;
|
|
149
|
+
} catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Human-readable description of an RRULE expression for display surfaces.
|
|
156
|
+
* Single-fire rules read as "One-time"; rules the library cannot express
|
|
157
|
+
* fall back to "Custom recurrence" rather than leaking raw iCalendar text.
|
|
158
|
+
*/
|
|
159
|
+
export function describeRRuleExpression(expression: string): string {
|
|
160
|
+
if (isSingleFireRRule(expression)) return "One-time";
|
|
161
|
+
try {
|
|
162
|
+
const normalized = normalizeRruleExpression(expression);
|
|
163
|
+
if (hasSetConstructs(normalized)) return "Custom recurrence";
|
|
164
|
+
const text = rrulestr(normalized).toText();
|
|
165
|
+
if (!text) return "Custom recurrence";
|
|
166
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
167
|
+
} catch {
|
|
168
|
+
return "Custom recurrence";
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
138
172
|
/**
|
|
139
173
|
* Compute the next run timestamp (epoch ms) for a schedule expression.
|
|
140
174
|
* Throws if no future runs exist.
|
|
@@ -619,6 +619,7 @@ export async function runScheduleDueWorkOnce(
|
|
|
619
619
|
jobName: `schedule:${job.id}`,
|
|
620
620
|
source: "schedule",
|
|
621
621
|
prompt: job.message,
|
|
622
|
+
systemHint: `Schedule: ${job.name}`,
|
|
622
623
|
trustContext: { sourceChannel: "vellum", trustClass: "guardian" },
|
|
623
624
|
callSite: "mainAgent",
|
|
624
625
|
timeoutMs: SCHEDULE_TALK_TIMEOUT_MS,
|
|
@@ -66,6 +66,42 @@ function quoteList(values: readonly string[]): string {
|
|
|
66
66
|
return values.map((v) => `"${v}"`).join(", ");
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Coerce string-encoded booleans (`"true"`/`"false"`) to real booleans for
|
|
71
|
+
* properties the schema declares as `type: "boolean"`.
|
|
72
|
+
*
|
|
73
|
+
* Some providers' models serialize booleans as JSON strings. Rejecting those
|
|
74
|
+
* loses the caller's intent: the model's typical recovery is to drop the field
|
|
75
|
+
* and retry, at which point the field's default silently inverts what it asked
|
|
76
|
+
* for (e.g. `app_create` with `auto_open: "false"` → retry omits the field →
|
|
77
|
+
* default `true` opens a half-built app). Accepting the unambiguous string
|
|
78
|
+
* forms preserves intent.
|
|
79
|
+
*
|
|
80
|
+
* Pure: returns a new object when a coercion applies, otherwise returns
|
|
81
|
+
* `input` unchanged. Never mutates `input` or `schema`.
|
|
82
|
+
*/
|
|
83
|
+
export function coerceStringBooleans(
|
|
84
|
+
input: Record<string, unknown>,
|
|
85
|
+
schema: Record<string, unknown> | undefined,
|
|
86
|
+
): Record<string, unknown> {
|
|
87
|
+
if (!schema) return input;
|
|
88
|
+
const properties = schema.properties;
|
|
89
|
+
if (!isPlainObject(properties)) return input;
|
|
90
|
+
|
|
91
|
+
let coerced: Record<string, unknown> | undefined;
|
|
92
|
+
for (const [key, rawSubSchema] of Object.entries(properties)) {
|
|
93
|
+
if (!isPlainObject(rawSubSchema)) continue;
|
|
94
|
+
if (rawSubSchema.type !== "boolean") continue;
|
|
95
|
+
const value = input[key];
|
|
96
|
+
if (typeof value !== "string") continue;
|
|
97
|
+
const normalized = value.trim().toLowerCase();
|
|
98
|
+
if (normalized !== "true" && normalized !== "false") continue;
|
|
99
|
+
coerced ??= { ...input };
|
|
100
|
+
coerced[key] = normalized === "true";
|
|
101
|
+
}
|
|
102
|
+
return coerced ?? input;
|
|
103
|
+
}
|
|
104
|
+
|
|
69
105
|
/**
|
|
70
106
|
* Validate a tool input object against the (optional) JSON-schema definition
|
|
71
107
|
* declared on the tool entry. Returns `{ ok: true }` if the input is valid (or
|
|
@@ -121,7 +157,11 @@ export function validateInputAgainstSchema(
|
|
|
121
157
|
if (typeof declaredType === "string" && SUPPORTED_TYPES.has(declaredType)) {
|
|
122
158
|
const type = declaredType as SupportedType;
|
|
123
159
|
if (!matchesType(value, type)) {
|
|
124
|
-
errors.push(
|
|
160
|
+
errors.push(
|
|
161
|
+
type === "boolean" && typeof value === "string"
|
|
162
|
+
? `${key} must be a boolean — pass true or false as a JSON boolean, not a string`
|
|
163
|
+
: `${key} must be ${typeArticle(type)} ${type}`,
|
|
164
|
+
);
|
|
125
165
|
// No point checking enum/items if the base type is wrong.
|
|
126
166
|
continue;
|
|
127
167
|
}
|
package/src/subagent/types.ts
CHANGED
|
@@ -105,7 +105,12 @@ export const SUBAGENT_LIMITS = {
|
|
|
105
105
|
|
|
106
106
|
// ── Roles ───────────────────────────────────────────────────────────────
|
|
107
107
|
|
|
108
|
-
export type SubagentRole =
|
|
108
|
+
export type SubagentRole =
|
|
109
|
+
| "general"
|
|
110
|
+
| "researcher"
|
|
111
|
+
| "coder"
|
|
112
|
+
| "planner"
|
|
113
|
+
| "investigator";
|
|
109
114
|
|
|
110
115
|
export interface SubagentRoleConfig {
|
|
111
116
|
/**
|
|
@@ -167,4 +172,24 @@ export const SUBAGENT_ROLE_REGISTRY: Record<SubagentRole, SubagentRoleConfig> =
|
|
|
167
172
|
systemPromptPreamble:
|
|
168
173
|
"You are an analysis-focused subagent with read-only access. Read files, search the web, and synthesize findings. You cannot write files or run shell commands.",
|
|
169
174
|
},
|
|
175
|
+
investigator: {
|
|
176
|
+
allowedTools: [
|
|
177
|
+
"bash",
|
|
178
|
+
"file_read",
|
|
179
|
+
"file_list",
|
|
180
|
+
"web_search",
|
|
181
|
+
"web_fetch",
|
|
182
|
+
"recall",
|
|
183
|
+
"notify_parent",
|
|
184
|
+
],
|
|
185
|
+
skillIds: [],
|
|
186
|
+
systemPromptPreamble: [
|
|
187
|
+
"You are an investigation-focused subagent for root-cause analysis: debugging, log forensics, and tracing behavior across code.",
|
|
188
|
+
"Your shell access is for read-only investigation (grep, find, reading files and logs) — do not modify files or system state.",
|
|
189
|
+
"Working method: read whole files instead of many small line-range slices; prefer broad searches (e.g. grep -rn across a directory) over one-symbol-at-a-time queries.",
|
|
190
|
+
"Send notify_parent (urgency 'important') as soon as each finding is confirmed, so progress survives interruption.",
|
|
191
|
+
"Your final message must be a compact root-cause report with these sections: Symptom, Root cause, Evidence (file:line references), Suggested fix, Open questions.",
|
|
192
|
+
"If you approach context limits, stop investigating and produce the report from what you have — a partial report delivered is worth more than a complete investigation lost.",
|
|
193
|
+
].join(" "),
|
|
194
|
+
},
|
|
170
195
|
};
|
package/src/telemetry/types.ts
CHANGED
|
@@ -55,7 +55,12 @@ export interface ModelTelemetryEventBase extends TelemetryEventBase {
|
|
|
55
55
|
inference_profile_source: UsageAttributionProfileSource | null;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* LLM usage event — one per persisted usage row. The main agent loop
|
|
60
|
+
* persists a single row per turn with token totals summed across every
|
|
61
|
+
* provider API call in the loop (`llm_call_count` carries the call count);
|
|
62
|
+
* auxiliary call sites persist one row per call.
|
|
63
|
+
*/
|
|
59
64
|
export interface LlmUsageTelemetryEvent extends TelemetryEventBase {
|
|
60
65
|
type: "llm_usage";
|
|
61
66
|
/**
|
|
@@ -85,6 +90,15 @@ export interface LlmUsageTelemetryEvent extends TelemetryEventBase {
|
|
|
85
90
|
output_tokens: number;
|
|
86
91
|
cache_creation_input_tokens: number | null;
|
|
87
92
|
cache_read_input_tokens: number | null;
|
|
93
|
+
/**
|
|
94
|
+
* Number of provider API calls aggregated into this event. The main
|
|
95
|
+
* agent loop persists one usage row per turn with token totals summed
|
|
96
|
+
* across every call in the loop, so this is how downstream consumers
|
|
97
|
+
* recover per-call averages (effective tokens ÷ calls). Auxiliary call
|
|
98
|
+
* sites record exactly 1. Null for rows persisted before daemon
|
|
99
|
+
* migration `200-usage-llm-call-count`; consumers treat null as 1.
|
|
100
|
+
*/
|
|
101
|
+
llm_call_count: number | null;
|
|
88
102
|
/**
|
|
89
103
|
* The provider's untouched `usage` block. Anthropic surfaces a TTL
|
|
90
104
|
* breakdown under `cache_creation.ephemeral_{5m,1h}_input_tokens`;
|
|
@@ -183,7 +183,7 @@ initializeDb();
|
|
|
183
183
|
|
|
184
184
|
let eventIdCounter = 0;
|
|
185
185
|
|
|
186
|
-
// The reporter consumes `UnreportedUsageEvent` (UsageEvent + the
|
|
186
|
+
// The reporter consumes `UnreportedUsageEvent` (UsageEvent + the
|
|
187
187
|
// JOIN-computed fields `conversationType` and `turnIndex`). Build that
|
|
188
188
|
// shape directly so the mock matches `queryUnreportedUsageEvents`'
|
|
189
189
|
// return type exactly.
|
|
@@ -218,6 +218,7 @@ function makeUsageEvent(
|
|
|
218
218
|
assistantVersion: "test-app-version",
|
|
219
219
|
conversationType: "standard",
|
|
220
220
|
turnIndex: 1,
|
|
221
|
+
llmCallCount: 1,
|
|
221
222
|
...overrides,
|
|
222
223
|
};
|
|
223
224
|
}
|
|
@@ -507,6 +508,7 @@ describe("UsageTelemetryReporter", () => {
|
|
|
507
508
|
callSite: "compactionAgent",
|
|
508
509
|
inferenceProfile: "quality-optimized",
|
|
509
510
|
inferenceProfileSource: "conversation",
|
|
511
|
+
llmCallCount: 3,
|
|
510
512
|
createdAt: 1700000099000,
|
|
511
513
|
});
|
|
512
514
|
mockQueryUnreportedUsageEvents.mockReturnValue([event]);
|
|
@@ -538,6 +540,7 @@ describe("UsageTelemetryReporter", () => {
|
|
|
538
540
|
expect(e.output_tokens).toBe(100);
|
|
539
541
|
expect(e.cache_creation_input_tokens).toBe(20);
|
|
540
542
|
expect(e.cache_read_input_tokens).toBe(15);
|
|
543
|
+
expect(e.llm_call_count).toBe(3);
|
|
541
544
|
expect(e.actor).toBe("context_compactor");
|
|
542
545
|
expect(e.llm_call_site).toBe("compactionAgent");
|
|
543
546
|
expect(e.inference_profile).toBe("quality-optimized");
|
|
@@ -599,6 +602,7 @@ describe("UsageTelemetryReporter", () => {
|
|
|
599
602
|
callSite: null,
|
|
600
603
|
inferenceProfile: null,
|
|
601
604
|
inferenceProfileSource: null,
|
|
605
|
+
llmCallCount: null,
|
|
602
606
|
});
|
|
603
607
|
mockQueryUnreportedUsageEvents.mockReturnValue([event]);
|
|
604
608
|
mockFetch.mockImplementation(() =>
|
|
@@ -617,6 +621,7 @@ describe("UsageTelemetryReporter", () => {
|
|
|
617
621
|
llm_call_site: null,
|
|
618
622
|
inference_profile: null,
|
|
619
623
|
inference_profile_source: null,
|
|
624
|
+
llm_call_count: null,
|
|
620
625
|
});
|
|
621
626
|
});
|
|
622
627
|
|
|
@@ -379,6 +379,7 @@ export class UsageTelemetryReporter {
|
|
|
379
379
|
output_tokens: e.outputTokens,
|
|
380
380
|
cache_creation_input_tokens: e.cacheCreationInputTokens ?? null,
|
|
381
381
|
cache_read_input_tokens: e.cacheReadInputTokens ?? null,
|
|
382
|
+
llm_call_count: e.llmCallCount,
|
|
382
383
|
raw_usage: e.rawUsage,
|
|
383
384
|
actor: e.actor,
|
|
384
385
|
llm_call_site: e.callSite,
|
|
@@ -548,7 +548,7 @@ export async function executeAppGenerateIcon(
|
|
|
548
548
|
return {
|
|
549
549
|
content: JSON.stringify({
|
|
550
550
|
error:
|
|
551
|
-
"Icon generation failed. Make sure a Gemini API key is configured in Settings.",
|
|
551
|
+
"Icon generation failed. Make sure a Gemini API key is configured in Settings → Models & Services.",
|
|
552
552
|
}),
|
|
553
553
|
isError: true,
|
|
554
554
|
};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { SkillToolEntry } from "../../config/skills.js";
|
|
2
2
|
import { RiskLevel } from "../../permissions/types.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
coerceStringBooleans,
|
|
5
|
+
validateInputAgainstSchema,
|
|
6
|
+
} from "../../skills/validate-input.js";
|
|
4
7
|
import type {
|
|
5
8
|
ExecutionTarget,
|
|
6
9
|
Tool,
|
|
@@ -42,10 +45,12 @@ export function createSkillTool(
|
|
|
42
45
|
input: Record<string, unknown>,
|
|
43
46
|
context: ToolContext,
|
|
44
47
|
): Promise<ToolExecutionResult> {
|
|
48
|
+
const schema = entry.input_schema as Record<string, unknown> | undefined;
|
|
49
|
+
const coercedInput = coerceStringBooleans(input, schema);
|
|
45
50
|
const validation = validateInputAgainstSchema(
|
|
46
51
|
entry.name,
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
coercedInput,
|
|
53
|
+
schema,
|
|
49
54
|
);
|
|
50
55
|
if (!validation.ok) {
|
|
51
56
|
return {
|
|
@@ -54,11 +59,17 @@ export function createSkillTool(
|
|
|
54
59
|
};
|
|
55
60
|
}
|
|
56
61
|
|
|
57
|
-
return runSkillToolScript(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
return runSkillToolScript(
|
|
63
|
+
skillDir,
|
|
64
|
+
entry.executor,
|
|
65
|
+
coercedInput,
|
|
66
|
+
context,
|
|
67
|
+
{
|
|
68
|
+
target: entry.execution_target,
|
|
69
|
+
expectedSkillVersionHash: versionHash,
|
|
70
|
+
bundled,
|
|
71
|
+
},
|
|
72
|
+
);
|
|
62
73
|
},
|
|
63
74
|
};
|
|
64
75
|
}
|
package/src/usage/types.ts
CHANGED
|
@@ -62,7 +62,7 @@ export interface UsageEventInput {
|
|
|
62
62
|
inferenceProfile?: string | null;
|
|
63
63
|
inferenceProfileSource?: UsageAttributionProfileSource | null;
|
|
64
64
|
/** Number of actual LLM API calls represented by this event (defaults to 1). */
|
|
65
|
-
llmCallCount?: number;
|
|
65
|
+
llmCallCount?: number | null;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
@@ -85,6 +85,13 @@ export interface UsageEvent extends UsageEventInput {
|
|
|
85
85
|
inferenceProfileSource: UsageAttributionProfileSource | null;
|
|
86
86
|
estimatedCostUsd: number | null;
|
|
87
87
|
pricingStatus: "priced" | "unpriced";
|
|
88
|
+
/**
|
|
89
|
+
* Number of provider API calls aggregated into this event. The main agent
|
|
90
|
+
* loop persists one row per turn with this set to the number of calls in
|
|
91
|
+
* the loop; auxiliary call sites persist 1. `null` only for rows persisted
|
|
92
|
+
* before migration `200-usage-llm-call-count` ran.
|
|
93
|
+
*/
|
|
94
|
+
llmCallCount: number | null;
|
|
88
95
|
/**
|
|
89
96
|
* Version of the assistant binary at the moment this event was
|
|
90
97
|
* RECORDED, captured by `recordUsageEvent` and persisted with the
|
package/src/util/platform.ts
CHANGED
|
@@ -79,6 +79,22 @@ export function getDataDir(): string {
|
|
|
79
79
|
return join(getWorkspaceDir(), "data");
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Returns the path to the config-quarantine notice sentinel
|
|
84
|
+
* (`<workspace>/data/config-quarantine-notice.json`).
|
|
85
|
+
*
|
|
86
|
+
* Written by the config loader when a corrupt `config.json` is quarantined and
|
|
87
|
+
* read by the per-turn `config-quarantine-notice` injector. Lives under the
|
|
88
|
+
* internal data dir (runtime state, config-free to resolve) rather than the
|
|
89
|
+
* user-facing workspace root because it is daemon-written bookkeeping, not a
|
|
90
|
+
* file the user edits. The path resolves without loading config, so it is safe
|
|
91
|
+
* to call during early-boot config load before the DB or `getConfig().dataDir`
|
|
92
|
+
* exist.
|
|
93
|
+
*/
|
|
94
|
+
export function getConfigQuarantineNoticePath(): string {
|
|
95
|
+
return join(getDataDir(), "config-quarantine-notice.json");
|
|
96
|
+
}
|
|
97
|
+
|
|
82
98
|
/**
|
|
83
99
|
* Returns the embedding models directory (~/.vellum/workspace/embedding-models).
|
|
84
100
|
* Downloaded embedding runtime (onnxruntime-node, transformers bundle, model weights)
|
package/src/watcher/engine.ts
CHANGED
|
@@ -251,6 +251,7 @@ export async function runWatchersOnce(
|
|
|
251
251
|
// The seed lives in the sandwich messages; processMessage runs
|
|
252
252
|
// with an empty prompt so we don't double-inject the action prompt.
|
|
253
253
|
prompt: "",
|
|
254
|
+
systemHint: `Watcher: ${watcher.name}`,
|
|
254
255
|
trustContext: { sourceChannel: "vellum", trustClass: "guardian" },
|
|
255
256
|
callSite: "mainAgent",
|
|
256
257
|
timeoutMs: WATCHER_JOB_TIMEOUT_MS,
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
// Enable adaptive thinking on the managed "balanced" and "quality-optimized"
|
|
5
|
+
// profiles.
|
|
6
|
+
//
|
|
7
|
+
// The assistant-side seed defaults (MANAGED_PROFILE_TEMPLATES in
|
|
8
|
+
// seed-inference-profiles.ts) already ship thinking: { enabled: true,
|
|
9
|
+
// streamThinking: true } for both profiles, which normalizes to
|
|
10
|
+
// { type: "adaptive" } on the wire. Off-platform (BYOK) instances pick this
|
|
11
|
+
// up on every boot because the seeder overwrites managed profiles. On-platform
|
|
12
|
+
// instances preserve existing profiles (the platform overlay is authoritative),
|
|
13
|
+
// so instances that were hatched before thinking was enabled in the templates
|
|
14
|
+
// are stuck with thinking disabled or absent.
|
|
15
|
+
//
|
|
16
|
+
// Workspace migration 097 patches the on-disk config once, but it runs before
|
|
17
|
+
// mergeDefaultWorkspaceConfig() which can overwrite the fix with overlay
|
|
18
|
+
// profiles that have thinking disabled or absent. lifecycle.ts calls this
|
|
19
|
+
// repair again after the overlay merge + profile seeding so the fix sticks.
|
|
20
|
+
// The migration keeps its own frozen copy of this logic (migration files are
|
|
21
|
+
// self-contained snapshots and must not be imported from).
|
|
22
|
+
//
|
|
23
|
+
// The repair patches both profiles, adding thinking: { enabled: true,
|
|
24
|
+
// streamThinking: true } where it's missing or explicitly disabled. It skips
|
|
25
|
+
// profiles that:
|
|
26
|
+
// - Don't exist (no profile to patch)
|
|
27
|
+
// - Already have thinking enabled (idempotent)
|
|
28
|
+
// - Are source: "user" (user-created profiles are untouched)
|
|
29
|
+
// - Have a non-managed, non-absent source (unknown origin)
|
|
30
|
+
// - Resolve to a non-Anthropic provider (adaptive thinking is
|
|
31
|
+
// Anthropic-specific). A profile with no explicit provider inherits
|
|
32
|
+
// llm.default.provider, so the check falls back to that — with a
|
|
33
|
+
// completely absent llm.default.provider treated as Anthropic, matching
|
|
34
|
+
// migration 052's own `?? "anthropic"` default. This keeps the legacy
|
|
35
|
+
// non-Anthropic empty `{}` shells seeded by migration 052 off the repair.
|
|
36
|
+
|
|
37
|
+
const ADAPTIVE_THINKING = { enabled: true, streamThinking: true } as const;
|
|
38
|
+
const TARGET_PROFILES = ["balanced", "quality-optimized"] as const;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Patch managed Anthropic profiles that are missing adaptive thinking.
|
|
42
|
+
*
|
|
43
|
+
* Idempotent: profiles that already have thinking enabled are skipped.
|
|
44
|
+
*/
|
|
45
|
+
export function repairAdaptiveThinkingOnManagedProfiles(
|
|
46
|
+
workspaceDir: string,
|
|
47
|
+
): void {
|
|
48
|
+
const configPath = join(workspaceDir, "config.json");
|
|
49
|
+
if (!existsSync(configPath)) return;
|
|
50
|
+
|
|
51
|
+
let config: Record<string, unknown>;
|
|
52
|
+
try {
|
|
53
|
+
const raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
54
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
|
|
55
|
+
config = raw as Record<string, unknown>;
|
|
56
|
+
} catch {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const llm = readObject(config.llm);
|
|
61
|
+
if (llm === null) return;
|
|
62
|
+
|
|
63
|
+
const profiles = readObject(llm.profiles);
|
|
64
|
+
if (profiles === null) return;
|
|
65
|
+
|
|
66
|
+
// Profiles without an explicit provider inherit llm.default.provider at
|
|
67
|
+
// resolution time; an absent llm.default.provider resolves to Anthropic.
|
|
68
|
+
const defaultBlock = readObject(llm.default);
|
|
69
|
+
const defaultProvider =
|
|
70
|
+
typeof defaultBlock?.provider === "string"
|
|
71
|
+
? defaultBlock.provider
|
|
72
|
+
: "anthropic";
|
|
73
|
+
|
|
74
|
+
let changed = false;
|
|
75
|
+
|
|
76
|
+
for (const name of TARGET_PROFILES) {
|
|
77
|
+
const profile = readObject(profiles[name]);
|
|
78
|
+
if (profile === null) continue;
|
|
79
|
+
|
|
80
|
+
// Only patch managed Anthropic profiles.
|
|
81
|
+
// Legacy profiles created before the `source` metadata field was introduced
|
|
82
|
+
// have source=undefined. Treat these as managed when the profile name is one
|
|
83
|
+
// of the canonical managed names (which TARGET_PROFILES already guarantees)
|
|
84
|
+
// and the effective provider — explicit, or inherited from llm.default — is
|
|
85
|
+
// Anthropic. Explicit `source: "user"` profiles are always skipped.
|
|
86
|
+
if (profile.source === "user") continue;
|
|
87
|
+
if (profile.source !== undefined && profile.source !== "managed") continue;
|
|
88
|
+
const effectiveProvider =
|
|
89
|
+
typeof profile.provider === "string" ? profile.provider : defaultProvider;
|
|
90
|
+
if (effectiveProvider !== "anthropic") continue;
|
|
91
|
+
|
|
92
|
+
// Skip if thinking is already enabled.
|
|
93
|
+
const thinking = readObject(profile.thinking);
|
|
94
|
+
if (thinking !== null && thinking.enabled === true) continue;
|
|
95
|
+
|
|
96
|
+
profile.thinking = { ...ADAPTIVE_THINKING };
|
|
97
|
+
profiles[name] = profile;
|
|
98
|
+
changed = true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (changed) {
|
|
102
|
+
llm.profiles = profiles;
|
|
103
|
+
config.llm = llm;
|
|
104
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readObject(value: unknown): Record<string, unknown> | null {
|
|
109
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return value as Record<string, unknown>;
|
|
113
|
+
}
|