@vellumai/assistant 0.8.2 → 0.8.3
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 +11 -12
- package/docker-entrypoint.sh +13 -1
- package/docker-init-apt-root.sh +79 -6
- package/openapi.yaml +336 -21
- package/package.json +1 -1
- package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
- package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
- package/src/__tests__/config-get-vision-flag.test.ts +136 -0
- package/src/__tests__/config-loader-backfill.test.ts +115 -18
- package/src/__tests__/context-token-estimator.test.ts +30 -65
- package/src/__tests__/conversation-agent-loop.test.ts +57 -1
- package/src/__tests__/conversation-media-retry.test.ts +19 -8
- package/src/__tests__/conversation-runtime-assembly.test.ts +26 -4
- package/src/__tests__/date-context.test.ts +45 -0
- package/src/__tests__/external-plugin-loader.test.ts +91 -19
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
- package/src/__tests__/guardian-dispatch.test.ts +1 -0
- package/src/__tests__/heartbeat-service.test.ts +24 -164
- package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
- package/src/__tests__/host-app-control-proxy.test.ts +241 -0
- package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
- package/src/__tests__/injector-background-turn.test.ts +153 -0
- package/src/__tests__/injector-chain.test.ts +5 -0
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
- package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
- package/src/__tests__/llm-catalog-parity.test.ts +3 -0
- package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
- package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
- package/src/__tests__/llm-resolver.test.ts +255 -2
- package/src/__tests__/managed-profile-guard.test.ts +10 -0
- package/src/__tests__/notification-decision-fallback.test.ts +0 -91
- package/src/__tests__/notification-decision-strategy.test.ts +14 -31
- package/src/__tests__/notification-deep-link.test.ts +15 -0
- package/src/__tests__/notification-guardian-path.test.ts +1 -2
- package/src/__tests__/notification-platform-adapter.test.ts +5 -4
- package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
- package/src/__tests__/openai-provider.test.ts +218 -3
- package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
- package/src/__tests__/openrouter-provider-only.test.ts +51 -3
- package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
- package/src/__tests__/platform-proxy-context.test.ts +6 -1
- package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
- package/src/__tests__/plugin-types.test.ts +2 -2
- package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
- package/src/__tests__/system-prompt.test.ts +6 -73
- package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
- package/src/a2a/__tests__/agent-card.test.ts +98 -0
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
- package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
- package/src/a2a/__tests__/task-store.test.ts +246 -0
- package/src/a2a/agent-card.ts +58 -0
- package/src/a2a/feature-gate.ts +8 -0
- package/src/a2a/protocol-constants.ts +21 -0
- package/src/a2a/protocol-errors.ts +50 -0
- package/src/a2a/protocol-types.ts +162 -0
- package/src/a2a/task-store.ts +168 -0
- package/src/agent/loop.ts +167 -18
- package/src/channels/config.ts +9 -0
- package/src/channels/types.ts +14 -0
- package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
- package/src/cli/commands/__tests__/schedules.test.ts +469 -0
- package/src/cli/commands/notifications.ts +65 -35
- package/src/cli/commands/plugins.ts +67 -0
- package/src/cli/commands/schedules.ts +297 -5
- package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
- package/src/cli/lib/install-from-github.ts +8 -9
- package/src/cli/lib/search-plugins.ts +163 -0
- package/src/cli/program.ts +14 -0
- package/src/config/assistant-feature-flags.ts +24 -54
- package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
- package/src/config/call-site-defaults.ts +105 -0
- package/src/config/feature-flag-registry.json +21 -29
- package/src/config/llm-resolver.ts +52 -1
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
- package/src/config/schemas/channels.ts +9 -0
- package/src/config/schemas/conversations.ts +10 -0
- package/src/config/schemas/heartbeat.ts +14 -0
- package/src/config/schemas/llm.ts +1 -3
- package/src/config/schemas/memory-retrospective.ts +1 -1
- package/src/config/schemas/memory-v2.ts +4 -4
- package/src/config/schemas/memory.ts +3 -1
- package/src/config/seed-inference-profiles.ts +99 -29
- package/src/context/compactor.ts +72 -12
- package/src/context/token-estimator.ts +32 -34
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
- package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
- package/src/daemon/conversation-agent-loop.ts +29 -2
- package/src/daemon/conversation-runtime-assembly.ts +9 -0
- package/src/daemon/conversation.ts +0 -7
- package/src/daemon/date-context.ts +40 -0
- package/src/daemon/guardian-action-generators.ts +1 -125
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
- package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
- package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
- package/src/daemon/handlers/config-a2a.ts +289 -0
- package/src/daemon/handlers/conversations.ts +1 -0
- package/src/daemon/host-app-control-proxy.ts +69 -18
- package/src/daemon/host-proxy-preactivation.ts +85 -18
- package/src/daemon/lifecycle.ts +49 -61
- package/src/daemon/memory-v2-startup.ts +49 -13
- package/src/daemon/message-types/notifications.ts +21 -0
- package/src/daemon/pkb-reminder-builder.test.ts +10 -53
- package/src/daemon/pkb-reminder-builder.ts +4 -19
- package/src/daemon/process-message.ts +3 -0
- package/src/daemon/skill-memory-refresh.ts +5 -1
- package/src/daemon/wake-target-adapter.ts +2 -0
- package/src/export/__tests__/transcript-formatter.test.ts +121 -0
- package/src/export/transcript-formatter.ts +54 -20
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -0
- package/src/heartbeat/heartbeat-service.ts +34 -191
- package/src/home/__tests__/feed-types.test.ts +40 -0
- package/src/home/feed-types.ts +14 -2
- package/src/ipc/cli-client.ts +147 -45
- package/src/memory/__tests__/conversation-queries.test.ts +220 -0
- package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
- package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
- package/src/memory/conversation-queries.ts +87 -1
- package/src/memory/conversation-title-service.ts +26 -4
- package/src/memory/db-init.ts +6 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
- package/src/memory/graph/conversation-graph-memory.ts +18 -6
- package/src/memory/graph/tools.ts +6 -37
- package/src/memory/invite-store.ts +53 -0
- package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
- package/src/memory/llm-request-log-store.ts +92 -1
- package/src/memory/memory-retrospective-enqueue.ts +1 -20
- package/src/memory/memory-retrospective-job.ts +33 -6
- package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
- package/src/memory/migrations/251-a2a-tasks.ts +49 -0
- package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/a2a.ts +15 -0
- package/src/memory/schema/index.ts +1 -0
- package/src/memory/schema/inference.ts +2 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
- package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
- package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
- package/src/memory/v2/__tests__/injection.test.ts +190 -3
- package/src/memory/v2/__tests__/static-context.test.ts +12 -1
- package/src/memory/v2/activation-store.ts +14 -16
- package/src/memory/v2/cli-command-content.ts +19 -0
- package/src/memory/v2/cli-command-store.ts +304 -0
- package/src/memory/v2/frontmatter-sweep.ts +7 -1
- package/src/memory/v2/injection.ts +49 -20
- package/src/memory/v2/page-index.ts +38 -13
- package/src/memory/v2/static-context.ts +4 -4
- package/src/memory/v2/types.ts +23 -0
- package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
- package/src/messaging/providers/a2a/deliver.ts +156 -0
- package/src/messaging/providers/gmail/client.ts +9 -2
- package/src/messaging/providers/index.ts +11 -2
- package/src/notifications/__tests__/broadcaster.test.ts +203 -0
- package/src/notifications/__tests__/decision-engine.test.ts +283 -0
- package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
- package/src/notifications/adapters/macos.ts +12 -2
- package/src/notifications/broadcaster.ts +29 -4
- package/src/notifications/copy-composer.ts +17 -64
- package/src/notifications/decision-engine.ts +111 -44
- package/src/notifications/deterministic-checks.ts +96 -0
- package/src/notifications/emit-signal.ts +1 -0
- package/src/notifications/home-feed-side-effect.ts +85 -6
- package/src/notifications/signal.ts +0 -4
- package/src/notifications/types.ts +8 -0
- package/src/oauth/platform-connection.test.ts +43 -3
- package/src/oauth/platform-connection.ts +13 -4
- package/src/plugins/defaults/injectors.ts +38 -19
- package/src/plugins/external-plugin-loader.ts +82 -10
- package/src/plugins/types.ts +16 -7
- package/src/prompts/__tests__/system-prompt.test.ts +6 -51
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
- package/src/prompts/system-prompt.ts +0 -8
- package/src/prompts/templates/BOOTSTRAP.md +5 -5
- package/src/prompts/templates/system-sections.ts +0 -9
- package/src/providers/__tests__/inference.test.ts +2 -0
- package/src/providers/call-site-routing.ts +24 -6
- package/src/providers/connection-resolution.ts +63 -13
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
- package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
- package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
- package/src/providers/inference/adapter-factory.ts +9 -20
- package/src/providers/inference/auth.ts +12 -0
- package/src/providers/inference/backfill.ts +14 -1
- package/src/providers/inference/connections.ts +85 -5
- package/src/providers/inference/resolve-auth.ts +2 -0
- package/src/providers/model-catalog.ts +199 -244
- package/src/providers/model-intents.ts +3 -3
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
- package/src/providers/openai/chat-completions-provider.ts +159 -6
- package/src/providers/openrouter/client.ts +42 -4
- package/src/providers/platform-proxy/constants.ts +3 -4
- package/src/providers/provider-catalog-visibility.ts +3 -1
- package/src/providers/provider-send-message.ts +27 -12
- package/src/providers/registry.ts +30 -1
- package/src/runtime/agent-wake.ts +61 -1
- package/src/runtime/auth/route-policy.ts +13 -0
- package/src/runtime/http-server.ts +7 -16
- package/src/runtime/http-types.ts +0 -47
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +66 -4
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
- package/src/runtime/routes/channel-availability-routes.ts +5 -0
- package/src/runtime/routes/consolidation-routes.ts +100 -0
- package/src/runtime/routes/conversation-query-routes.ts +70 -11
- package/src/runtime/routes/conversation-routes.ts +7 -0
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +134 -1
- package/src/runtime/routes/integrations/a2a.ts +235 -0
- package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
- package/src/runtime/routes/subagents-routes.ts +41 -0
- package/src/subagent/manager.ts +2 -0
- package/src/tools/memory/register.ts +1 -9
- package/src/tools/registry.ts +2 -2
- package/src/tools/types.ts +37 -2
- package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
- package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
- package/src/runtime/guardian-action-conversation-turn.ts +0 -99
|
@@ -17,11 +17,16 @@ import { z } from "zod";
|
|
|
17
17
|
|
|
18
18
|
import { getConfig } from "../../config/loader.js";
|
|
19
19
|
import { getMemoryCheckpoint } from "../../memory/checkpoints.js";
|
|
20
|
+
import {
|
|
21
|
+
getMessageRoleStatsByConversation,
|
|
22
|
+
listConversationsBySource,
|
|
23
|
+
} from "../../memory/conversation-queries.js";
|
|
20
24
|
import {
|
|
21
25
|
enqueueMemoryJob,
|
|
22
26
|
hasActiveJobOfType,
|
|
23
27
|
} from "../../memory/jobs-store.js";
|
|
24
28
|
import { GRAPH_MAINTENANCE_CHECKPOINTS } from "../../memory/jobs-worker.js";
|
|
29
|
+
import { MEMORY_V2_CONSOLIDATION_SOURCE } from "../../memory/v2/constants.js";
|
|
25
30
|
import { BadRequestError } from "./errors.js";
|
|
26
31
|
import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
|
|
27
32
|
|
|
@@ -111,4 +116,99 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
111
116
|
return { success: true, ran: true, jobId };
|
|
112
117
|
},
|
|
113
118
|
},
|
|
119
|
+
{
|
|
120
|
+
operationId: "listConsolidationRuns",
|
|
121
|
+
endpoint: "consolidation/runs",
|
|
122
|
+
method: "GET",
|
|
123
|
+
policyKey: "consolidation",
|
|
124
|
+
summary: "List consolidation runs",
|
|
125
|
+
description:
|
|
126
|
+
"Return recent memory v2 consolidation conversations as run records. " +
|
|
127
|
+
"Each consolidation dispatch creates exactly one background conversation " +
|
|
128
|
+
"tagged with `source = memory_v2_consolidation`; that conversation IS " +
|
|
129
|
+
"the run. Synthetic fields: `id` mirrors `conversationId` (no separate " +
|
|
130
|
+
"run row exists), `scheduledFor` and `startedAt` both equal " +
|
|
131
|
+
"`conversation.createdAt` (no separate schedule timestamp), " +
|
|
132
|
+
"`finishedAt` is the `createdAt` of the latest assistant message in " +
|
|
133
|
+
"the conversation (NOT `conversation.lastMessageAt`, which the kickoff " +
|
|
134
|
+
"user prompt bumps before the agent runs). `status` is `'ok'` when " +
|
|
135
|
+
"the conversation has at least one assistant message — i.e. positive " +
|
|
136
|
+
"evidence the agent emitted output — otherwise `'running'`. This is a " +
|
|
137
|
+
"weaker signal than heartbeat's `'ok'`: without a dedicated runs " +
|
|
138
|
+
"table we cannot distinguish 'ran cleanly' from 'crashed after " +
|
|
139
|
+
"emitting at least one assistant message'. `skipReason` and `error` " +
|
|
140
|
+
"are always null — skipped runs (lock held, disabled, empty buffer) " +
|
|
141
|
+
"never create a conversation, and run failure detail is not stored " +
|
|
142
|
+
"on the conversation row. Shape mirrors `heartbeat/runs` so the " +
|
|
143
|
+
"schedules settings UI can reuse its run-row component.",
|
|
144
|
+
tags: ["consolidation"],
|
|
145
|
+
queryParams: [
|
|
146
|
+
{
|
|
147
|
+
name: "limit",
|
|
148
|
+
schema: { type: "integer" },
|
|
149
|
+
description: "Max runs to return (default 20, max 100)",
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
responseBody: z.object({
|
|
153
|
+
runs: z
|
|
154
|
+
.array(
|
|
155
|
+
z.object({
|
|
156
|
+
id: z.string(),
|
|
157
|
+
scheduledFor: z.number(),
|
|
158
|
+
startedAt: z.number().nullable(),
|
|
159
|
+
finishedAt: z.number().nullable(),
|
|
160
|
+
durationMs: z.number().nullable(),
|
|
161
|
+
status: z.enum(["ok", "running"]),
|
|
162
|
+
skipReason: z.string().nullable(),
|
|
163
|
+
error: z.string().nullable(),
|
|
164
|
+
conversationId: z.string().nullable(),
|
|
165
|
+
createdAt: z.number(),
|
|
166
|
+
}),
|
|
167
|
+
)
|
|
168
|
+
.describe("Consolidation run records"),
|
|
169
|
+
}),
|
|
170
|
+
handler: async ({ queryParams }: RouteHandlerArgs) => {
|
|
171
|
+
const params = queryParams ?? {};
|
|
172
|
+
const rawLimit = Number(params.limit ?? 20);
|
|
173
|
+
const limit = Number.isFinite(rawLimit)
|
|
174
|
+
? Math.min(Math.max(Math.floor(rawLimit), 1), 100)
|
|
175
|
+
: 20;
|
|
176
|
+
const rows = listConversationsBySource(
|
|
177
|
+
MEMORY_V2_CONSOLIDATION_SOURCE,
|
|
178
|
+
limit,
|
|
179
|
+
);
|
|
180
|
+
// Aggregate assistant-message stats in one batched query: presence of
|
|
181
|
+
// an assistant message is the strongest "agent emitted output" signal
|
|
182
|
+
// available without a dedicated consolidation runs table. The kickoff
|
|
183
|
+
// user prompt is persisted via `addMessage` before the agent run,
|
|
184
|
+
// which bumps `conversations.lastMessageAt` — so that field cannot
|
|
185
|
+
// be used to infer completion.
|
|
186
|
+
const assistantStats = getMessageRoleStatsByConversation(
|
|
187
|
+
rows.map((r) => r.id),
|
|
188
|
+
"assistant",
|
|
189
|
+
);
|
|
190
|
+
return {
|
|
191
|
+
runs: rows.map((c) => {
|
|
192
|
+
const stat = assistantStats.get(c.id);
|
|
193
|
+
const hasAssistantOutput = (stat?.count ?? 0) > 0;
|
|
194
|
+
const finishedAt = hasAssistantOutput ? stat!.lastAt : null;
|
|
195
|
+
return {
|
|
196
|
+
id: c.id,
|
|
197
|
+
scheduledFor: c.createdAt,
|
|
198
|
+
startedAt: c.createdAt,
|
|
199
|
+
finishedAt,
|
|
200
|
+
durationMs:
|
|
201
|
+
finishedAt != null ? finishedAt - c.createdAt : null,
|
|
202
|
+
status: (hasAssistantOutput ? "ok" : "running") as
|
|
203
|
+
| "ok"
|
|
204
|
+
| "running",
|
|
205
|
+
skipReason: null,
|
|
206
|
+
error: null,
|
|
207
|
+
conversationId: c.id,
|
|
208
|
+
createdAt: c.createdAt,
|
|
209
|
+
};
|
|
210
|
+
}),
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
},
|
|
114
214
|
];
|
|
@@ -59,12 +59,20 @@ import {
|
|
|
59
59
|
getConversation,
|
|
60
60
|
getMessageById,
|
|
61
61
|
} from "../../memory/conversation-crud.js";
|
|
62
|
+
import { getDb } from "../../memory/db-connection.js";
|
|
62
63
|
import { clearEmbeddingBackendCache } from "../../memory/embedding-backend.js";
|
|
63
64
|
import { getLlmRequestLogSource } from "../../memory/llm-request-log-source.js";
|
|
64
65
|
import { getMemoryRecallLogByMessageIds } from "../../memory/memory-recall-log-store.js";
|
|
65
66
|
import { getMemoryV2ActivationLogByMessageIds } from "../../memory/memory-v2-activation-log-store.js";
|
|
66
67
|
import { MEMORY_V2_CONSOLIDATION_SOURCE } from "../../memory/v2/constants.js";
|
|
68
|
+
import {
|
|
69
|
+
createConnection,
|
|
70
|
+
listConnections,
|
|
71
|
+
PROVIDERS_REQUIRING_BASE_URL_AND_MODELS,
|
|
72
|
+
} from "../../providers/inference/connections.js";
|
|
73
|
+
import { PROVIDER_CATALOG } from "../../providers/model-catalog.js";
|
|
67
74
|
import { initializeProviders } from "../../providers/registry.js";
|
|
75
|
+
import { credentialKey } from "../../security/credential-key.js";
|
|
68
76
|
import { validateAllowlistFile } from "../../security/secret-allowlist.js";
|
|
69
77
|
import { resolvePricingForUsage } from "../../util/pricing.js";
|
|
70
78
|
import { BadRequestError, InternalError, NotFoundError } from "./errors.js";
|
|
@@ -379,13 +387,42 @@ function readPlainObject(value: unknown): Record<string, unknown> | undefined {
|
|
|
379
387
|
|
|
380
388
|
function handleGetConfig() {
|
|
381
389
|
try {
|
|
382
|
-
|
|
390
|
+
const config = applyContextDefaultsToRawConfig(loadRawConfig());
|
|
391
|
+
enrichProfilesWithVisionFlag(config);
|
|
392
|
+
return config;
|
|
383
393
|
} catch (err) {
|
|
384
394
|
const message = err instanceof Error ? err.message : String(err);
|
|
385
395
|
throw new InternalError(`Failed to read config: ${message}`);
|
|
386
396
|
}
|
|
387
397
|
}
|
|
388
398
|
|
|
399
|
+
/**
|
|
400
|
+
* Annotate each profile in `config.llm.profiles` with `supportsVision`
|
|
401
|
+
* resolved from the model catalog. The flag is wire-only — it is never
|
|
402
|
+
* persisted to disk. Unknown (provider, model) pairs default to `true`
|
|
403
|
+
* (fail-open) so image upload remains available for custom / unlisted models.
|
|
404
|
+
*/
|
|
405
|
+
function enrichProfilesWithVisionFlag(config: unknown): void {
|
|
406
|
+
const root = readPlainObject(config);
|
|
407
|
+
if (!root) return;
|
|
408
|
+
const llm = readPlainObject(root.llm);
|
|
409
|
+
if (!llm) return;
|
|
410
|
+
const profiles = readPlainObject(llm.profiles);
|
|
411
|
+
if (!profiles) return;
|
|
412
|
+
|
|
413
|
+
for (const profile of Object.values(profiles)) {
|
|
414
|
+
const entry = readPlainObject(profile);
|
|
415
|
+
if (!entry) continue;
|
|
416
|
+
const provider = entry.provider;
|
|
417
|
+
const model = entry.model;
|
|
418
|
+
if (typeof provider !== "string" || typeof model !== "string") continue;
|
|
419
|
+
|
|
420
|
+
const catalogProvider = PROVIDER_CATALOG.find((p) => p.id === provider);
|
|
421
|
+
const catalogModel = catalogProvider?.models.find((m) => m.id === model);
|
|
422
|
+
entry.supportsVision = catalogModel?.supportsVision ?? true;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
389
426
|
/**
|
|
390
427
|
* Return the JSON Schema for the assistant config (full or scoped).
|
|
391
428
|
*
|
|
@@ -617,6 +654,36 @@ async function handleReplaceInferenceProfile({
|
|
|
617
654
|
);
|
|
618
655
|
}
|
|
619
656
|
}
|
|
657
|
+
// When the UI sends provider but no provider_connection ("Any active X
|
|
658
|
+
// connection"), derive the connection now so the config deep-merge doesn't
|
|
659
|
+
// inherit a stale connection from the default layer.
|
|
660
|
+
const fragment = parsed.data as Record<string, unknown>;
|
|
661
|
+
if (!isManaged && fragment.provider && !fragment.provider_connection) {
|
|
662
|
+
const provider = fragment.provider as string;
|
|
663
|
+
const db = getDb();
|
|
664
|
+
const candidates = listConnections(db, { provider });
|
|
665
|
+
const active = candidates.find((c) => c.status === "active");
|
|
666
|
+
if (active) {
|
|
667
|
+
fragment.provider_connection = active.name;
|
|
668
|
+
} else if (!PROVIDERS_REQUIRING_BASE_URL_AND_MODELS.has(provider)) {
|
|
669
|
+
const connectionName = `${provider}-personal`;
|
|
670
|
+
const isKeyless = provider === "ollama";
|
|
671
|
+
const result = createConnection(db, {
|
|
672
|
+
name: connectionName,
|
|
673
|
+
provider,
|
|
674
|
+
auth: isKeyless
|
|
675
|
+
? { type: "none" }
|
|
676
|
+
: {
|
|
677
|
+
type: "api_key",
|
|
678
|
+
credential: credentialKey(provider, "api_key"),
|
|
679
|
+
},
|
|
680
|
+
});
|
|
681
|
+
if (result.ok) {
|
|
682
|
+
fragment.provider_connection = connectionName;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
620
687
|
const raw = loadRawConfig();
|
|
621
688
|
if (isManaged) {
|
|
622
689
|
// Partial overlay: keep every existing key intact, only update label
|
|
@@ -624,17 +691,9 @@ async function handleReplaceInferenceProfile({
|
|
|
624
691
|
// here would wipe the UI-owned seed fields (provider, model, advanced
|
|
625
692
|
// params) because that function assumes the body carries the full UI
|
|
626
693
|
// surface.
|
|
627
|
-
patchManagedProfileFields(
|
|
628
|
-
raw,
|
|
629
|
-
name,
|
|
630
|
-
parsed.data as Record<string, unknown>,
|
|
631
|
-
);
|
|
694
|
+
patchManagedProfileFields(raw, name, fragment);
|
|
632
695
|
} else {
|
|
633
|
-
replaceInferenceProfileConfig(
|
|
634
|
-
raw,
|
|
635
|
-
name,
|
|
636
|
-
parsed.data as Record<string, unknown>,
|
|
637
|
-
);
|
|
696
|
+
replaceInferenceProfileConfig(raw, name, fragment);
|
|
638
697
|
}
|
|
639
698
|
// Route through `commitConfigWrite` so profile edits flow through the
|
|
640
699
|
// post-write side effects shared with `handlePatchConfig` /
|
|
@@ -101,6 +101,7 @@ import { writeOnboardingSection } from "../../prompts/persona-resolver.js";
|
|
|
101
101
|
import { getConfiguredProvider } from "../../providers/provider-send-message.js";
|
|
102
102
|
import type { Provider } from "../../providers/types.js";
|
|
103
103
|
import { checkIngressForSecrets } from "../../security/secret-ingress.js";
|
|
104
|
+
import { getSubagentManager } from "../../subagent/index.js";
|
|
104
105
|
import { getLogger } from "../../util/logger.js";
|
|
105
106
|
import {
|
|
106
107
|
getInterfacesDir,
|
|
@@ -1492,6 +1493,12 @@ export async function handleSendMessage(
|
|
|
1492
1493
|
// that can service host_browser_request events; we restore that single
|
|
1493
1494
|
// proxy explicitly below without relaxing `hasNoClient`.
|
|
1494
1495
|
conversation.updateClient(broadcastMessage, !isInteractive);
|
|
1496
|
+
if (isInteractive) {
|
|
1497
|
+
getSubagentManager().updateParentSender(
|
|
1498
|
+
mapping.conversationId,
|
|
1499
|
+
broadcastMessage,
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1495
1502
|
|
|
1496
1503
|
// ── Canned first-greeting fast path ──
|
|
1497
1504
|
// On a completely fresh workspace, skip LLM inference for the macOS
|
|
@@ -72,6 +72,7 @@ import { ROUTES as IMAGE_GENERATION_ROUTES } from "./image-generation-routes.js"
|
|
|
72
72
|
import { ROUTES as INFERENCE_PROFILE_SESSION_ROUTES } from "./inference-profile-session-routes.js";
|
|
73
73
|
import { ROUTES as INFERENCE_PROVIDER_CONNECTION_ROUTES } from "./inference-provider-connection-routes.js";
|
|
74
74
|
import { ROUTES as INFERENCE_SEND_ROUTES } from "./inference-send-routes.js";
|
|
75
|
+
import { ROUTES as A2A_ROUTES } from "./integrations/a2a.js";
|
|
75
76
|
import { ROUTES as SLACK_CHANNEL_ROUTES } from "./integrations/slack/channel.js";
|
|
76
77
|
import { ROUTES as SLACK_SHARE_ROUTES } from "./integrations/slack/share.js";
|
|
77
78
|
import { ROUTES as TELEGRAM_ROUTES } from "./integrations/telegram.js";
|
|
@@ -223,6 +224,7 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
223
224
|
...SECRET_ROUTES,
|
|
224
225
|
...SETTINGS_ROUTES,
|
|
225
226
|
...SKILL_ROUTES,
|
|
227
|
+
...A2A_ROUTES,
|
|
226
228
|
...SLACK_CHANNEL_ROUTES,
|
|
227
229
|
...SLACK_SHARE_ROUTES,
|
|
228
230
|
...STT_ROUTES,
|
|
@@ -10,10 +10,13 @@
|
|
|
10
10
|
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
|
|
13
|
+
import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
|
|
13
14
|
import { getConfigReadOnly } from "../../config/loader.js";
|
|
14
15
|
import { getDb } from "../../memory/db-connection.js";
|
|
15
16
|
import {
|
|
16
17
|
AuthSchema,
|
|
18
|
+
type ConnectionModel,
|
|
19
|
+
ConnectionModelSchema,
|
|
17
20
|
ConnectionProviderSchema,
|
|
18
21
|
ConnectionStatusSchema,
|
|
19
22
|
ProviderConnectionSchema,
|
|
@@ -35,6 +38,75 @@ import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
|
|
|
35
38
|
// ---------------------------------------------------------------------------
|
|
36
39
|
|
|
37
40
|
const providerConnectionResponseSchema = ProviderConnectionSchema;
|
|
41
|
+
const OPENAI_COMPATIBLE_ENDPOINTS_FLAG = "openai-compatible-endpoints";
|
|
42
|
+
|
|
43
|
+
function openAICompatibleEndpointsEnabled(): boolean {
|
|
44
|
+
return isAssistantFeatureFlagEnabled(
|
|
45
|
+
OPENAI_COMPATIBLE_ENDPOINTS_FLAG,
|
|
46
|
+
getConfigReadOnly(),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function rejectDisabledOpenAICompatibleProvider(provider: string): void {
|
|
51
|
+
if (provider !== "openai-compatible") return;
|
|
52
|
+
if (openAICompatibleEndpointsEnabled()) return;
|
|
53
|
+
throw new BadRequestError(
|
|
54
|
+
"OpenAI-compatible endpoints are disabled. Enable the openai-compatible-endpoints feature flag to configure this provider.",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Custom provider field parsing (openai-compatible base_url + models)
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
function parseCustomProviderFields(body: Record<string, unknown>): {
|
|
63
|
+
baseUrl?: string | null;
|
|
64
|
+
models?: ConnectionModel[] | null;
|
|
65
|
+
} {
|
|
66
|
+
const out: {
|
|
67
|
+
baseUrl?: string | null;
|
|
68
|
+
models?: ConnectionModel[] | null;
|
|
69
|
+
} = {};
|
|
70
|
+
|
|
71
|
+
if ("base_url" in body) {
|
|
72
|
+
const raw = body.base_url;
|
|
73
|
+
if (raw === null) {
|
|
74
|
+
out.baseUrl = null;
|
|
75
|
+
} else if (typeof raw === "string" && raw.length > 0) {
|
|
76
|
+
try {
|
|
77
|
+
const parsed = new URL(raw);
|
|
78
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
79
|
+
throw new BadRequestError(`Invalid base_url: must be an http(s) URL`);
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (err instanceof BadRequestError) throw err;
|
|
83
|
+
throw new BadRequestError(
|
|
84
|
+
`Invalid base_url: must be a valid http(s) URL`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
out.baseUrl = raw;
|
|
88
|
+
} else {
|
|
89
|
+
throw new BadRequestError(
|
|
90
|
+
`Invalid base_url: must be a non-empty string or null`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if ("models" in body) {
|
|
96
|
+
const raw = body.models;
|
|
97
|
+
if (raw === null) {
|
|
98
|
+
out.models = null;
|
|
99
|
+
} else {
|
|
100
|
+
const parsed = z.array(ConnectionModelSchema).safeParse(raw);
|
|
101
|
+
if (!parsed.success) {
|
|
102
|
+
throw new BadRequestError(`Invalid models: ${parsed.error.message}`);
|
|
103
|
+
}
|
|
104
|
+
out.models = parsed.data;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
38
110
|
|
|
39
111
|
// ---------------------------------------------------------------------------
|
|
40
112
|
// Handlers
|
|
@@ -42,10 +114,18 @@ const providerConnectionResponseSchema = ProviderConnectionSchema;
|
|
|
42
114
|
|
|
43
115
|
function handleListConnections({ queryParams = {} }: RouteHandlerArgs) {
|
|
44
116
|
const provider = queryParams.provider;
|
|
117
|
+
if (provider) rejectDisabledOpenAICompatibleProvider(provider);
|
|
45
118
|
const connections = listConnections(
|
|
46
119
|
getDb(),
|
|
47
120
|
provider ? { provider } : undefined,
|
|
48
121
|
);
|
|
122
|
+
if (!openAICompatibleEndpointsEnabled()) {
|
|
123
|
+
return {
|
|
124
|
+
connections: connections.filter(
|
|
125
|
+
(conn) => conn.provider !== "openai-compatible",
|
|
126
|
+
),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
49
129
|
return { connections };
|
|
50
130
|
}
|
|
51
131
|
|
|
@@ -55,6 +135,12 @@ function handleGetConnection({ pathParams = {} }: RouteHandlerArgs) {
|
|
|
55
135
|
|
|
56
136
|
const conn = getConnection(getDb(), name);
|
|
57
137
|
if (!conn) throw new NotFoundError(`Connection "${name}" not found.`);
|
|
138
|
+
if (
|
|
139
|
+
conn.provider === "openai-compatible" &&
|
|
140
|
+
!openAICompatibleEndpointsEnabled()
|
|
141
|
+
) {
|
|
142
|
+
throw new NotFoundError(`Connection "${name}" not found.`);
|
|
143
|
+
}
|
|
58
144
|
|
|
59
145
|
return conn;
|
|
60
146
|
}
|
|
@@ -74,6 +160,7 @@ function handleCreateConnection({ body = {} }: RouteHandlerArgs) {
|
|
|
74
160
|
`Invalid provider "${String(provider)}". Valid: ${VALID_CONNECTION_PROVIDERS.join(", ")}`,
|
|
75
161
|
);
|
|
76
162
|
}
|
|
163
|
+
rejectDisabledOpenAICompatibleProvider(providerResult.data);
|
|
77
164
|
|
|
78
165
|
const authResult = AuthSchema.safeParse(auth);
|
|
79
166
|
if (!authResult.success) {
|
|
@@ -99,12 +186,15 @@ function handleCreateConnection({ body = {} }: RouteHandlerArgs) {
|
|
|
99
186
|
);
|
|
100
187
|
}
|
|
101
188
|
|
|
189
|
+
const customFields = parseCustomProviderFields(body);
|
|
190
|
+
|
|
102
191
|
const result = createConnection(getDb(), {
|
|
103
192
|
name,
|
|
104
193
|
provider: providerResult.data,
|
|
105
194
|
auth: authResult.data,
|
|
106
195
|
...(statusResult ? { status: statusResult.data } : {}),
|
|
107
196
|
...(labelRaw !== undefined ? { label: labelRaw as string | null } : {}),
|
|
197
|
+
...customFields,
|
|
108
198
|
});
|
|
109
199
|
|
|
110
200
|
if (!result.ok) {
|
|
@@ -118,6 +208,16 @@ function handleCreateConnection({ body = {} }: RouteHandlerArgs) {
|
|
|
118
208
|
`Invalid provider "${result.error.provider}". Valid: ${VALID_CONNECTION_PROVIDERS.join(", ")}`,
|
|
119
209
|
);
|
|
120
210
|
}
|
|
211
|
+
if (result.error.code === "base_url_required") {
|
|
212
|
+
throw new BadRequestError(
|
|
213
|
+
"base_url is required for openai-compatible connections.",
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
if (result.error.code === "models_required") {
|
|
217
|
+
throw new BadRequestError(
|
|
218
|
+
"At least one model is required for openai-compatible connections.",
|
|
219
|
+
);
|
|
220
|
+
}
|
|
121
221
|
throw new BadRequestError("Invalid auth configuration.");
|
|
122
222
|
}
|
|
123
223
|
|
|
@@ -131,6 +231,15 @@ function handleUpdateConnection({
|
|
|
131
231
|
const { name } = pathParams;
|
|
132
232
|
if (!name) throw new BadRequestError("name is required");
|
|
133
233
|
|
|
234
|
+
const existing = getConnection(getDb(), name);
|
|
235
|
+
if (!existing) throw new NotFoundError(`Connection "${name}" not found.`);
|
|
236
|
+
if (
|
|
237
|
+
existing.provider === "openai-compatible" &&
|
|
238
|
+
!openAICompatibleEndpointsEnabled()
|
|
239
|
+
) {
|
|
240
|
+
throw new NotFoundError(`Connection "${name}" not found.`);
|
|
241
|
+
}
|
|
242
|
+
|
|
134
243
|
const auth = body.auth;
|
|
135
244
|
const authResult = AuthSchema.safeParse(auth);
|
|
136
245
|
if (!authResult.success) {
|
|
@@ -169,16 +278,29 @@ function handleUpdateConnection({
|
|
|
169
278
|
);
|
|
170
279
|
}
|
|
171
280
|
|
|
281
|
+
const customFields = parseCustomProviderFields(body);
|
|
282
|
+
|
|
172
283
|
const result = updateConnection(getDb(), name, {
|
|
173
284
|
auth: authResult.data,
|
|
174
285
|
...(statusResult ? { status: statusResult.data } : {}),
|
|
175
286
|
...(labelRaw !== undefined ? { label: labelRaw as string | null } : {}),
|
|
287
|
+
...customFields,
|
|
176
288
|
});
|
|
177
289
|
|
|
178
290
|
if (!result.ok) {
|
|
179
291
|
if (result.error.code === "not_found") {
|
|
180
292
|
throw new NotFoundError(`Connection "${name}" not found.`);
|
|
181
293
|
}
|
|
294
|
+
if (result.error.code === "base_url_required") {
|
|
295
|
+
throw new BadRequestError(
|
|
296
|
+
"base_url is required for openai-compatible connections.",
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
if (result.error.code === "models_required") {
|
|
300
|
+
throw new BadRequestError(
|
|
301
|
+
"At least one model is required for openai-compatible connections.",
|
|
302
|
+
);
|
|
303
|
+
}
|
|
182
304
|
throw new BadRequestError("Invalid auth configuration.");
|
|
183
305
|
}
|
|
184
306
|
|
|
@@ -191,7 +313,14 @@ function handleDeleteConnection({ pathParams = {} }: RouteHandlerArgs) {
|
|
|
191
313
|
|
|
192
314
|
// Existence check first so a stale `llm.default.provider_connection`
|
|
193
315
|
// reference to a missing connection returns 404 (not 409).
|
|
194
|
-
|
|
316
|
+
const existing = getConnection(getDb(), name);
|
|
317
|
+
if (!existing) {
|
|
318
|
+
throw new NotFoundError(`Connection "${name}" not found.`);
|
|
319
|
+
}
|
|
320
|
+
if (
|
|
321
|
+
existing.provider === "openai-compatible" &&
|
|
322
|
+
!openAICompatibleEndpointsEnabled()
|
|
323
|
+
) {
|
|
195
324
|
throw new NotFoundError(`Connection "${name}" not found.`);
|
|
196
325
|
}
|
|
197
326
|
|
|
@@ -301,6 +430,8 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
301
430
|
auth: AuthSchema,
|
|
302
431
|
label: z.string().min(1).optional(),
|
|
303
432
|
status: ConnectionStatusSchema.optional(),
|
|
433
|
+
base_url: z.string().url().nullable().optional(),
|
|
434
|
+
models: z.array(ConnectionModelSchema).nullable().optional(),
|
|
304
435
|
}),
|
|
305
436
|
responseBody: providerConnectionResponseSchema,
|
|
306
437
|
responseStatus: "201",
|
|
@@ -324,6 +455,8 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
324
455
|
auth: AuthSchema,
|
|
325
456
|
status: ConnectionStatusSchema.optional(),
|
|
326
457
|
label: z.string().min(1).nullable().optional(),
|
|
458
|
+
base_url: z.string().url().nullable().optional(),
|
|
459
|
+
models: z.array(ConnectionModelSchema).nullable().optional(),
|
|
327
460
|
}),
|
|
328
461
|
responseBody: providerConnectionResponseSchema,
|
|
329
462
|
additionalResponses: {
|