@vellumai/assistant 0.5.7 → 0.5.9
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/Dockerfile +2 -1
- package/docker-entrypoint.sh +9 -0
- package/docs/architecture/memory.md +13 -11
- package/eslint.config.mjs +0 -31
- package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +1 -1
- package/package.json +1 -1
- package/src/__tests__/approval-cascade.test.ts +0 -1
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/ces-rpc-credential-backend.test.ts +3 -3
- package/src/__tests__/ces-startup-timeout.test.ts +40 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -1
- package/src/__tests__/config-schema.test.ts +2 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop.test.ts +2 -4
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
- package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
- package/src/__tests__/conversation-queue.test.ts +0 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +227 -0
- package/src/__tests__/conversation-slash-queue.test.ts +0 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
- package/src/__tests__/credential-execution-client.test.ts +5 -2
- package/src/__tests__/credential-execution-feature-gates.test.ts +31 -16
- package/src/__tests__/credential-execution-managed-contract.test.ts +2 -2
- package/src/__tests__/credential-security-e2e.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +2 -5
- package/src/__tests__/credentials-cli.test.ts +4 -3
- package/src/__tests__/daemon-credential-client.test.ts +123 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
- package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
- package/src/__tests__/journal-context.test.ts +335 -0
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
- package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
- package/src/__tests__/memory-recall-quality.test.ts +48 -17
- package/src/__tests__/memory-regressions.test.ts +408 -363
- package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
- package/src/__tests__/non-member-access-request.test.ts +2 -2
- package/src/__tests__/notification-decision-strategy.test.ts +71 -0
- package/src/__tests__/oauth-cli.test.ts +5 -1
- package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
- package/src/__tests__/provider-error-scenarios.test.ts +0 -267
- package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
- package/src/__tests__/relay-server.test.ts +1 -2
- package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -1
- package/src/__tests__/secure-keys.test.ts +18 -15
- package/src/__tests__/skill-memory.test.ts +17 -3
- package/src/__tests__/stale-approval-dedup.test.ts +171 -0
- package/src/__tests__/stt-hints.test.ts +437 -0
- package/src/__tests__/task-memory-cleanup.test.ts +14 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
- package/src/__tests__/voice-quality.test.ts +58 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
- package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -3
- package/src/acp/agent-process.ts +9 -1
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-request-resolvers.ts +164 -38
- package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
- package/src/calls/call-controller.ts +9 -5
- package/src/calls/fish-audio-client.ts +26 -14
- package/src/calls/stt-hints.ts +189 -0
- package/src/calls/tts-text-sanitizer.ts +61 -0
- package/src/calls/twilio-routes.ts +32 -4
- package/src/calls/voice-quality.ts +15 -3
- package/src/calls/voice-session-bridge.ts +1 -0
- package/src/cli/commands/avatar.ts +2 -2
- package/src/cli/commands/credentials.ts +110 -94
- package/src/cli/commands/doctor.ts +2 -2
- package/src/cli/commands/keys.ts +7 -7
- package/src/cli/commands/memory.ts +1 -1
- package/src/cli/commands/oauth/connections.ts +11 -29
- package/src/cli/commands/oauth/platform.ts +389 -43
- package/src/cli/lib/daemon-credential-client.ts +284 -0
- package/src/cli.ts +1 -1
- package/src/config/bundled-skills/AGENTS.md +34 -0
- package/src/config/bundled-skills/acp/SKILL.md +10 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
- package/src/config/bundled-skills/settings/SKILL.md +15 -2
- package/src/config/bundled-skills/settings/TOOLS.json +46 -1
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
- package/src/config/bundled-skills/slack/SKILL.md +1 -1
- package/src/config/bundled-tool-registry.ts +4 -0
- package/src/config/defaults.ts +0 -2
- package/src/config/env-registry.ts +4 -4
- package/src/config/env.ts +14 -1
- package/src/config/feature-flag-registry.json +1 -1
- package/src/config/loader.ts +8 -11
- package/src/config/schema.ts +5 -16
- package/src/config/schemas/calls.ts +17 -0
- package/src/config/schemas/inference.ts +2 -2
- package/src/config/schemas/journal.ts +16 -0
- package/src/config/schemas/memory-processing.ts +2 -2
- package/src/config/types.ts +1 -0
- package/src/contacts/contact-store.ts +2 -2
- package/src/credential-execution/executable-discovery.ts +1 -1
- package/src/credential-execution/startup-timeout.ts +36 -0
- package/src/daemon/approval-generators.ts +3 -9
- package/src/daemon/conversation-agent-loop.ts +6 -0
- package/src/daemon/conversation-error.ts +13 -1
- package/src/daemon/conversation-memory.ts +1 -2
- package/src/daemon/conversation-process.ts +18 -1
- package/src/daemon/conversation-runtime-assembly.ts +61 -1
- package/src/daemon/conversation-surfaces.ts +30 -1
- package/src/daemon/conversation.ts +20 -9
- package/src/daemon/guardian-action-generators.ts +3 -9
- package/src/daemon/lifecycle.ts +18 -11
- package/src/daemon/message-types/conversations.ts +1 -0
- package/src/daemon/server.ts +2 -3
- package/src/memory/app-store.ts +31 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/indexer.ts +19 -10
- package/src/memory/items-extractor.ts +315 -322
- package/src/memory/job-handlers/summarization.ts +26 -16
- package/src/memory/jobs-store.ts +33 -1
- package/src/memory/journal-memory.ts +214 -0
- package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/retriever.test.ts +37 -25
- package/src/memory/retriever.ts +24 -49
- package/src/memory/schema/memory-core.ts +2 -0
- package/src/memory/search/formatting.ts +7 -44
- package/src/memory/search/staleness.ts +4 -0
- package/src/memory/search/tier-classifier.ts +10 -2
- package/src/memory/search/types.ts +2 -5
- package/src/memory/task-memory-cleanup.ts +4 -3
- package/src/notifications/adapters/slack.ts +168 -6
- package/src/notifications/broadcaster.ts +1 -0
- package/src/notifications/copy-composer.ts +59 -2
- package/src/notifications/signal.ts +2 -0
- package/src/notifications/types.ts +2 -0
- package/src/prompts/journal-context.ts +133 -0
- package/src/prompts/persona-resolver.ts +80 -24
- package/src/prompts/system-prompt.ts +30 -0
- package/src/prompts/templates/NOW.md +26 -0
- package/src/prompts/templates/SOUL.md +20 -0
- package/src/prompts/update-bulletin-format.ts +0 -2
- package/src/providers/provider-send-message.ts +3 -32
- package/src/providers/registry.ts +2 -139
- package/src/providers/types.ts +1 -1
- package/src/runtime/access-request-helper.ts +4 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
- package/src/runtime/auth/route-policy.ts +2 -0
- package/src/runtime/gateway-client.ts +47 -4
- package/src/runtime/guardian-decision-types.ts +45 -4
- package/src/runtime/http-server.ts +5 -2
- package/src/runtime/routes/access-request-decision.ts +2 -2
- package/src/runtime/routes/app-management-routes.ts +2 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
- package/src/runtime/routes/channel-readiness-routes.ts +9 -4
- package/src/runtime/routes/debug-routes.ts +12 -9
- package/src/runtime/routes/guardian-approval-interception.ts +168 -11
- package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
- package/src/runtime/routes/identity-routes.ts +1 -1
- package/src/runtime/routes/inbound-message-handler.ts +31 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
- package/src/runtime/routes/integrations/twilio.ts +52 -10
- package/src/runtime/routes/memory-item-routes.test.ts +3 -3
- package/src/runtime/routes/memory-item-routes.ts +25 -11
- package/src/runtime/routes/secret-routes.ts +141 -10
- package/src/runtime/routes/tts-routes.ts +11 -1
- package/src/security/ces-credential-client.ts +18 -9
- package/src/security/ces-rpc-credential-backend.ts +4 -3
- package/src/security/credential-backend.ts +10 -4
- package/src/security/secure-keys.ts +21 -4
- package/src/skills/catalog-install.ts +4 -36
- package/src/skills/inline-command-expansions.ts +7 -7
- package/src/skills/skill-memory.ts +1 -0
- package/src/subagent/manager.ts +2 -5
- package/src/tools/acp/spawn.ts +78 -1
- package/src/tools/credentials/vault.ts +5 -3
- package/src/tools/memory/definitions.ts +3 -2
- package/src/tools/memory/handlers.ts +10 -7
- package/src/tools/sensitive-output-placeholders.ts +2 -2
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/util/browser.ts +15 -0
- package/src/util/platform.ts +1 -1
- package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +4 -4
- package/src/workspace/migrations/017-seed-persona-dirs.ts +2 -1
- package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
- package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -4
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/workspace/provider-commit-message-generator.ts +12 -21
- package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
- package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
- package/src/memory/search/lexical.ts +0 -48
- package/src/providers/failover.ts +0 -186
|
@@ -20,7 +20,10 @@ import { initializeProviders } from "../../providers/registry.js";
|
|
|
20
20
|
import { credentialKey } from "../../security/credential-key.js";
|
|
21
21
|
import {
|
|
22
22
|
deleteSecureKeyAsync,
|
|
23
|
+
getActiveBackendName,
|
|
23
24
|
getSecureKeyAsync,
|
|
25
|
+
getSecureKeyResultAsync,
|
|
26
|
+
listSecureKeysAsync,
|
|
24
27
|
setSecureKeyAsync,
|
|
25
28
|
} from "../../security/secure-keys.js";
|
|
26
29
|
import {
|
|
@@ -103,11 +106,16 @@ export async function handleAddSecret(
|
|
|
103
106
|
req: Request,
|
|
104
107
|
getCesClient?: () => CesClient | undefined,
|
|
105
108
|
): Promise<Response> {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
let body: { type?: string; name?: string; value?: string };
|
|
110
|
+
try {
|
|
111
|
+
body = (await req.json()) as {
|
|
112
|
+
type?: string;
|
|
113
|
+
name?: string;
|
|
114
|
+
value?: string;
|
|
115
|
+
};
|
|
116
|
+
} catch {
|
|
117
|
+
return httpError("BAD_REQUEST", "Request body must be valid JSON", 400);
|
|
118
|
+
}
|
|
111
119
|
|
|
112
120
|
const { type, name, value } = body;
|
|
113
121
|
|
|
@@ -178,7 +186,7 @@ export async function handleAddSecret(
|
|
|
178
186
|
if (!stored) {
|
|
179
187
|
return httpError(
|
|
180
188
|
"INTERNAL_ERROR",
|
|
181
|
-
|
|
189
|
+
`Failed to store API key in secure storage (backend: ${getActiveBackendName()})`,
|
|
182
190
|
500,
|
|
183
191
|
);
|
|
184
192
|
}
|
|
@@ -190,7 +198,7 @@ export async function handleAddSecret(
|
|
|
190
198
|
}
|
|
191
199
|
|
|
192
200
|
if (type === "credential") {
|
|
193
|
-
const colonIdx = name.
|
|
201
|
+
const colonIdx = name.lastIndexOf(":");
|
|
194
202
|
if (colonIdx < 1 || colonIdx === name.length - 1) {
|
|
195
203
|
return httpError(
|
|
196
204
|
"BAD_REQUEST",
|
|
@@ -243,7 +251,7 @@ export async function handleAddSecret(
|
|
|
243
251
|
if (!stored) {
|
|
244
252
|
return httpError(
|
|
245
253
|
"INTERNAL_ERROR",
|
|
246
|
-
|
|
254
|
+
`Failed to store credential in secure storage (backend: ${getActiveBackendName()})`,
|
|
247
255
|
500,
|
|
248
256
|
);
|
|
249
257
|
}
|
|
@@ -308,12 +316,90 @@ export async function handleAddSecret(
|
|
|
308
316
|
}
|
|
309
317
|
}
|
|
310
318
|
|
|
311
|
-
export async function
|
|
319
|
+
export async function handleReadSecret(req: Request): Promise<Response> {
|
|
312
320
|
const body = (await req.json()) as {
|
|
313
321
|
type?: string;
|
|
314
322
|
name?: string;
|
|
323
|
+
reveal?: boolean;
|
|
315
324
|
};
|
|
316
325
|
|
|
326
|
+
const { type, name, reveal } = body;
|
|
327
|
+
|
|
328
|
+
if (!type || typeof type !== "string") {
|
|
329
|
+
return httpError("BAD_REQUEST", "type is required", 400);
|
|
330
|
+
}
|
|
331
|
+
if (!name || typeof name !== "string") {
|
|
332
|
+
return httpError("BAD_REQUEST", "name is required", 400);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
let accountKey: string;
|
|
337
|
+
|
|
338
|
+
if (type === "api_key") {
|
|
339
|
+
if (
|
|
340
|
+
!API_KEY_PROVIDERS.includes(name as (typeof API_KEY_PROVIDERS)[number])
|
|
341
|
+
) {
|
|
342
|
+
return httpError(
|
|
343
|
+
"BAD_REQUEST",
|
|
344
|
+
`Unknown API key provider: ${name}. Valid providers: ${API_KEY_PROVIDERS.join(
|
|
345
|
+
", ",
|
|
346
|
+
)}`,
|
|
347
|
+
400,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
accountKey = name;
|
|
351
|
+
} else if (type === "credential") {
|
|
352
|
+
const colonIdx = name.lastIndexOf(":");
|
|
353
|
+
if (colonIdx < 1 || colonIdx === name.length - 1) {
|
|
354
|
+
return httpError(
|
|
355
|
+
"BAD_REQUEST",
|
|
356
|
+
'For credential type, name must be in "service:field" format (e.g. "github:api_token")',
|
|
357
|
+
400,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
const service = name.slice(0, colonIdx);
|
|
361
|
+
const field = name.slice(colonIdx + 1);
|
|
362
|
+
accountKey = credentialKey(service, field);
|
|
363
|
+
} else {
|
|
364
|
+
return httpError(
|
|
365
|
+
"BAD_REQUEST",
|
|
366
|
+
`Unknown secret type: ${type}. Valid types: api_key, credential`,
|
|
367
|
+
400,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const { value, unreachable } = await getSecureKeyResultAsync(accountKey);
|
|
372
|
+
if (value === undefined) {
|
|
373
|
+
return Response.json({ found: false, unreachable });
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (reveal) {
|
|
377
|
+
return Response.json({ found: true, value, unreachable: false });
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Mask the value: show first 10 chars and last 4, hiding at least 3
|
|
381
|
+
const minHidden = 3;
|
|
382
|
+
const maxVisible = Math.max(1, value.length - minHidden);
|
|
383
|
+
const prefixLen = Math.min(10, maxVisible);
|
|
384
|
+
const suffixLen = Math.min(4, Math.max(0, maxVisible - prefixLen));
|
|
385
|
+
const masked = `${value.slice(0, prefixLen)}...${suffixLen > 0 ? value.slice(-suffixLen) : ""}`;
|
|
386
|
+
|
|
387
|
+
return Response.json({ found: true, masked, unreachable: false });
|
|
388
|
+
} catch (err) {
|
|
389
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
390
|
+
log.error({ err, type, name }, "Failed to read secret via HTTP");
|
|
391
|
+
return httpError("INTERNAL_ERROR", message, 500);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export async function handleDeleteSecret(req: Request): Promise<Response> {
|
|
396
|
+
let body: { type?: string; name?: string };
|
|
397
|
+
try {
|
|
398
|
+
body = (await req.json()) as { type?: string; name?: string };
|
|
399
|
+
} catch {
|
|
400
|
+
return httpError("BAD_REQUEST", "Request body must be valid JSON", 400);
|
|
401
|
+
}
|
|
402
|
+
|
|
317
403
|
const { type, name } = body;
|
|
318
404
|
|
|
319
405
|
if (!type || typeof type !== "string") {
|
|
@@ -358,7 +444,7 @@ export async function handleDeleteSecret(req: Request): Promise<Response> {
|
|
|
358
444
|
}
|
|
359
445
|
|
|
360
446
|
if (type === "credential") {
|
|
361
|
-
const colonIdx = name.
|
|
447
|
+
const colonIdx = name.lastIndexOf(":");
|
|
362
448
|
if (colonIdx < 1 || colonIdx === name.length - 1) {
|
|
363
449
|
return httpError(
|
|
364
450
|
"BAD_REQUEST",
|
|
@@ -418,6 +504,41 @@ export async function handleDeleteSecret(req: Request): Promise<Response> {
|
|
|
418
504
|
}
|
|
419
505
|
}
|
|
420
506
|
|
|
507
|
+
const CREDENTIAL_KEY_PREFIX = "credential/";
|
|
508
|
+
|
|
509
|
+
export async function handleListSecrets(): Promise<Response> {
|
|
510
|
+
try {
|
|
511
|
+
const { accounts, unreachable } = await listSecureKeysAsync();
|
|
512
|
+
if (unreachable) {
|
|
513
|
+
return Response.json(
|
|
514
|
+
{ error: "Credential store is unreachable" },
|
|
515
|
+
{ status: 503 },
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const secrets = accounts.map((account) => {
|
|
520
|
+
if (account.startsWith(CREDENTIAL_KEY_PREFIX)) {
|
|
521
|
+
// credential/{service}/{field} → service:field
|
|
522
|
+
const rest = account.slice(CREDENTIAL_KEY_PREFIX.length);
|
|
523
|
+
const slashIdx = rest.indexOf("/");
|
|
524
|
+
if (slashIdx > 0 && slashIdx < rest.length - 1) {
|
|
525
|
+
return {
|
|
526
|
+
type: "credential" as const,
|
|
527
|
+
name: `${rest.slice(0, slashIdx)}:${rest.slice(slashIdx + 1)}`,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
// API key providers are stored with their raw provider name
|
|
532
|
+
return { type: "api_key" as const, name: account };
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
return Response.json({ secrets });
|
|
536
|
+
} catch (err) {
|
|
537
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
538
|
+
return httpError("INTERNAL_ERROR", message, 500);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
421
542
|
// ---------------------------------------------------------------------------
|
|
422
543
|
// Route definitions
|
|
423
544
|
// ---------------------------------------------------------------------------
|
|
@@ -441,5 +562,15 @@ export function secretRouteDefinitions(
|
|
|
441
562
|
method: "DELETE",
|
|
442
563
|
handler: async ({ req }) => handleDeleteSecret(req),
|
|
443
564
|
},
|
|
565
|
+
{
|
|
566
|
+
endpoint: "secrets",
|
|
567
|
+
method: "GET",
|
|
568
|
+
handler: async () => handleListSecrets(),
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
endpoint: "secrets/read",
|
|
572
|
+
method: "POST",
|
|
573
|
+
handler: async ({ req }) => handleReadSecret(req),
|
|
574
|
+
},
|
|
444
575
|
];
|
|
445
576
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { synthesizeWithFishAudio } from "../../calls/fish-audio-client.js";
|
|
11
|
+
import { sanitizeForTts } from "../../calls/tts-text-sanitizer.js";
|
|
11
12
|
import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
|
|
12
13
|
import { getConfig } from "../../config/loader.js";
|
|
13
14
|
import { getMessageContent } from "../../daemon/handlers/conversation-history.js";
|
|
@@ -49,6 +50,15 @@ export function ttsRouteDefinitions(): RouteDefinition[] {
|
|
|
49
50
|
return httpError("BAD_REQUEST", "Message has no text content", 400);
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
const sanitizedText = sanitizeForTts(result.text);
|
|
54
|
+
if (!sanitizedText.trim()) {
|
|
55
|
+
return httpError(
|
|
56
|
+
"BAD_REQUEST",
|
|
57
|
+
"Message has no speakable text content",
|
|
58
|
+
400,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
52
62
|
const { fishAudio } = config;
|
|
53
63
|
if (!fishAudio?.referenceId) {
|
|
54
64
|
return httpError(
|
|
@@ -60,7 +70,7 @@ export function ttsRouteDefinitions(): RouteDefinition[] {
|
|
|
60
70
|
|
|
61
71
|
try {
|
|
62
72
|
const audioBuffer = await synthesizeWithFishAudio(
|
|
63
|
-
|
|
73
|
+
sanitizedText,
|
|
64
74
|
fishAudio,
|
|
65
75
|
);
|
|
66
76
|
|
|
@@ -19,6 +19,7 @@ import { getLogger } from "../util/logger.js";
|
|
|
19
19
|
import type {
|
|
20
20
|
CredentialBackend,
|
|
21
21
|
CredentialGetResult,
|
|
22
|
+
CredentialListResult,
|
|
22
23
|
DeleteResult,
|
|
23
24
|
} from "./credential-backend.js";
|
|
24
25
|
|
|
@@ -51,7 +52,13 @@ async function cesRequest(
|
|
|
51
52
|
): Promise<Response | null> {
|
|
52
53
|
const baseUrl = getBaseUrl();
|
|
53
54
|
const token = getServiceToken();
|
|
54
|
-
if (!baseUrl || !token)
|
|
55
|
+
if (!baseUrl || !token) {
|
|
56
|
+
log.warn(
|
|
57
|
+
{ method, path, hasBaseUrl: !!baseUrl, hasToken: !!token },
|
|
58
|
+
"CES credential request skipped — missing config",
|
|
59
|
+
);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
55
62
|
|
|
56
63
|
const url = `${baseUrl.replace(/\/+$/, "")}${path}`;
|
|
57
64
|
const headers: Record<string, string> = {
|
|
@@ -128,16 +135,17 @@ export class CesCredentialBackend implements CredentialBackend {
|
|
|
128
135
|
return false;
|
|
129
136
|
}
|
|
130
137
|
if (!res.ok) {
|
|
138
|
+
const detail = await res.text().catch(() => "");
|
|
131
139
|
if (attempt < SET_MAX_RETRIES - 1) {
|
|
132
140
|
log.warn(
|
|
133
|
-
{ account, status: res.status, attempt },
|
|
141
|
+
{ account, status: res.status, detail, attempt },
|
|
134
142
|
"CES credential set returned non-OK status, retrying",
|
|
135
143
|
);
|
|
136
144
|
await new Promise((r) => setTimeout(r, SET_RETRY_DELAY_MS));
|
|
137
145
|
continue;
|
|
138
146
|
}
|
|
139
147
|
log.warn(
|
|
140
|
-
{ account, status: res.status },
|
|
148
|
+
{ account, status: res.status, detail },
|
|
141
149
|
"CES credential set returned non-OK status",
|
|
142
150
|
);
|
|
143
151
|
return false;
|
|
@@ -168,8 +176,9 @@ export class CesCredentialBackend implements CredentialBackend {
|
|
|
168
176
|
if (!res) return "error";
|
|
169
177
|
if (res.status === 404) return "not-found";
|
|
170
178
|
if (!res.ok) {
|
|
179
|
+
const detail = await res.text().catch(() => "");
|
|
171
180
|
log.warn(
|
|
172
|
-
{ account, status: res.status },
|
|
181
|
+
{ account, status: res.status, detail },
|
|
173
182
|
"CES credential delete returned non-OK status",
|
|
174
183
|
);
|
|
175
184
|
return "error";
|
|
@@ -181,22 +190,22 @@ export class CesCredentialBackend implements CredentialBackend {
|
|
|
181
190
|
}
|
|
182
191
|
}
|
|
183
192
|
|
|
184
|
-
async list(): Promise<
|
|
193
|
+
async list(): Promise<CredentialListResult> {
|
|
185
194
|
try {
|
|
186
195
|
const res = await cesRequest("GET", "/v1/credentials");
|
|
187
|
-
if (!res) return [];
|
|
196
|
+
if (!res) return { accounts: [], unreachable: true };
|
|
188
197
|
if (!res.ok) {
|
|
189
198
|
log.warn(
|
|
190
199
|
{ status: res.status },
|
|
191
200
|
"CES credential list returned non-OK status",
|
|
192
201
|
);
|
|
193
|
-
return [];
|
|
202
|
+
return { accounts: [], unreachable: true };
|
|
194
203
|
}
|
|
195
204
|
const data = (await res.json()) as { accounts?: string[] };
|
|
196
|
-
return data.accounts ?? [];
|
|
205
|
+
return { accounts: data.accounts ?? [], unreachable: false };
|
|
197
206
|
} catch (err) {
|
|
198
207
|
log.warn({ err }, "CES credential list threw unexpectedly");
|
|
199
|
-
return [];
|
|
208
|
+
return { accounts: [], unreachable: true };
|
|
200
209
|
}
|
|
201
210
|
}
|
|
202
211
|
}
|
|
@@ -14,6 +14,7 @@ import { getLogger } from "../util/logger.js";
|
|
|
14
14
|
import type {
|
|
15
15
|
CredentialBackend,
|
|
16
16
|
CredentialGetResult,
|
|
17
|
+
CredentialListResult,
|
|
17
18
|
DeleteResult,
|
|
18
19
|
} from "./credential-backend.js";
|
|
19
20
|
|
|
@@ -70,16 +71,16 @@ export class CesRpcCredentialBackend implements CredentialBackend {
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
async list(): Promise<
|
|
74
|
+
async list(): Promise<CredentialListResult> {
|
|
74
75
|
try {
|
|
75
76
|
const result = await this.client.call(
|
|
76
77
|
CesRpcMethod.ListCredentials,
|
|
77
78
|
{},
|
|
78
79
|
);
|
|
79
|
-
return result.accounts;
|
|
80
|
+
return { accounts: result.accounts, unreachable: false };
|
|
80
81
|
} catch (err) {
|
|
81
82
|
log.warn({ err }, "CES RPC credential list failed");
|
|
82
|
-
return [];
|
|
83
|
+
return { accounts: [], unreachable: true };
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
}
|
|
@@ -19,6 +19,12 @@ export interface CredentialGetResult {
|
|
|
19
19
|
unreachable: boolean;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/** Result of a list operation — distinguishes unreachable from empty. */
|
|
23
|
+
export interface CredentialListResult {
|
|
24
|
+
accounts: string[];
|
|
25
|
+
unreachable: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
22
28
|
// ---------------------------------------------------------------------------
|
|
23
29
|
// Interface
|
|
24
30
|
// ---------------------------------------------------------------------------
|
|
@@ -40,7 +46,7 @@ export interface CredentialBackend {
|
|
|
40
46
|
delete(account: string): Promise<DeleteResult>;
|
|
41
47
|
|
|
42
48
|
/** List all account names. */
|
|
43
|
-
list(): Promise<
|
|
49
|
+
list(): Promise<CredentialListResult>;
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
// ---------------------------------------------------------------------------
|
|
@@ -78,11 +84,11 @@ export class EncryptedStoreBackend implements CredentialBackend {
|
|
|
78
84
|
}
|
|
79
85
|
}
|
|
80
86
|
|
|
81
|
-
async list(): Promise<
|
|
87
|
+
async list(): Promise<CredentialListResult> {
|
|
82
88
|
try {
|
|
83
|
-
return encryptedStore.listKeys();
|
|
89
|
+
return { accounts: encryptedStore.listKeys(), unreachable: false };
|
|
84
90
|
} catch {
|
|
85
|
-
return [];
|
|
91
|
+
return { accounts: [], unreachable: true };
|
|
86
92
|
}
|
|
87
93
|
}
|
|
88
94
|
}
|
|
@@ -25,10 +25,17 @@ import type { CesClient } from "../credential-execution/client.js";
|
|
|
25
25
|
import { getLogger } from "../util/logger.js";
|
|
26
26
|
import { createCesCredentialBackend } from "./ces-credential-client.js";
|
|
27
27
|
import { CesRpcCredentialBackend } from "./ces-rpc-credential-backend.js";
|
|
28
|
-
import type {
|
|
28
|
+
import type {
|
|
29
|
+
CredentialBackend,
|
|
30
|
+
CredentialListResult,
|
|
31
|
+
DeleteResult,
|
|
32
|
+
} from "./credential-backend.js";
|
|
29
33
|
import { createEncryptedStoreBackend } from "./credential-backend.js";
|
|
30
34
|
|
|
31
|
-
export type {
|
|
35
|
+
export type {
|
|
36
|
+
CredentialListResult,
|
|
37
|
+
DeleteResult,
|
|
38
|
+
} from "./credential-backend.js";
|
|
32
39
|
|
|
33
40
|
/**
|
|
34
41
|
* Re-export shared-package secure-key abstractions so downstream consumers
|
|
@@ -89,7 +96,9 @@ async function doResolveBackend(): Promise<CredentialBackend> {
|
|
|
89
96
|
_resolvedBackend = cesRpc;
|
|
90
97
|
return cesRpc;
|
|
91
98
|
}
|
|
92
|
-
log.warn(
|
|
99
|
+
log.warn(
|
|
100
|
+
"CES RPC client is set but not ready — falling back to local credential store",
|
|
101
|
+
);
|
|
93
102
|
}
|
|
94
103
|
|
|
95
104
|
// 2. CES HTTP — containerized / Docker / managed mode
|
|
@@ -115,7 +124,7 @@ async function doResolveBackend(): Promise<CredentialBackend> {
|
|
|
115
124
|
*
|
|
116
125
|
* Queries exactly one backend — no cross-store merge.
|
|
117
126
|
*/
|
|
118
|
-
export async function listSecureKeysAsync(): Promise<
|
|
127
|
+
export async function listSecureKeysAsync(): Promise<CredentialListResult> {
|
|
119
128
|
const backend = await resolveBackendAsync();
|
|
120
129
|
return backend.list();
|
|
121
130
|
}
|
|
@@ -241,6 +250,14 @@ export async function getMaskedProviderKey(
|
|
|
241
250
|
// Test helpers
|
|
242
251
|
// ---------------------------------------------------------------------------
|
|
243
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Return the name of the currently resolved credential backend.
|
|
255
|
+
* Useful for diagnostic messages when credential operations fail.
|
|
256
|
+
*/
|
|
257
|
+
export function getActiveBackendName(): string {
|
|
258
|
+
return _resolvedBackend?.name ?? "none";
|
|
259
|
+
}
|
|
260
|
+
|
|
244
261
|
/** @internal Test-only: reset the cached backends so they're re-created. */
|
|
245
262
|
export function _resetBackend(): void {
|
|
246
263
|
_cesClient = undefined;
|
|
@@ -13,12 +13,9 @@ import { homedir } from "node:os";
|
|
|
13
13
|
import { dirname, join, posix, resolve, sep } from "node:path";
|
|
14
14
|
import { gunzipSync } from "node:zlib";
|
|
15
15
|
|
|
16
|
+
import { getPlatformBaseUrl } from "../config/env.js";
|
|
16
17
|
import { getLogger } from "../util/logger.js";
|
|
17
|
-
import {
|
|
18
|
-
getWorkspaceConfigPath,
|
|
19
|
-
getWorkspaceSkillsDir,
|
|
20
|
-
readPlatformToken,
|
|
21
|
-
} from "../util/platform.js";
|
|
18
|
+
import { getWorkspaceSkillsDir, readPlatformToken } from "../util/platform.js";
|
|
22
19
|
import { deleteSkillCapabilityMemory } from "./skill-memory.js";
|
|
23
20
|
|
|
24
21
|
const log = getLogger("catalog-install");
|
|
@@ -70,27 +67,6 @@ export function getRepoSkillsDir(): string | undefined {
|
|
|
70
67
|
|
|
71
68
|
// ─── Platform API ────────────────────────────────────────────────────────────
|
|
72
69
|
|
|
73
|
-
function getConfigPlatformUrl(): string | undefined {
|
|
74
|
-
try {
|
|
75
|
-
const configPath = getWorkspaceConfigPath();
|
|
76
|
-
if (!existsSync(configPath)) return undefined;
|
|
77
|
-
const raw = JSON.parse(readFileSync(configPath, "utf-8")) as Record<
|
|
78
|
-
string,
|
|
79
|
-
unknown
|
|
80
|
-
>;
|
|
81
|
-
const platform = raw.platform as Record<string, unknown> | undefined;
|
|
82
|
-
const baseUrl = platform?.baseUrl;
|
|
83
|
-
if (typeof baseUrl === "string" && baseUrl.trim()) return baseUrl.trim();
|
|
84
|
-
} catch {
|
|
85
|
-
// ignore
|
|
86
|
-
}
|
|
87
|
-
return undefined;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function getPlatformUrl(): string {
|
|
91
|
-
return process.env.VELLUM_PLATFORM_URL ?? getConfigPlatformUrl() ?? "";
|
|
92
|
-
}
|
|
93
|
-
|
|
94
70
|
function buildHeaders(): Record<string, string> {
|
|
95
71
|
const headers: Record<string, string> = {};
|
|
96
72
|
const token = readPlatformToken();
|
|
@@ -103,10 +79,7 @@ function buildHeaders(): Record<string, string> {
|
|
|
103
79
|
// ─── Catalog operations ──────────────────────────────────────────────────────
|
|
104
80
|
|
|
105
81
|
export async function fetchCatalog(): Promise<CatalogSkill[]> {
|
|
106
|
-
const platformUrl =
|
|
107
|
-
if (!platformUrl) {
|
|
108
|
-
return [];
|
|
109
|
-
}
|
|
82
|
+
const platformUrl = getPlatformBaseUrl();
|
|
110
83
|
const url = `${platformUrl}/v1/skills/`;
|
|
111
84
|
const response = await fetch(url, {
|
|
112
85
|
headers: buildHeaders(),
|
|
@@ -214,12 +187,7 @@ export async function fetchAndExtractSkill(
|
|
|
214
187
|
skillId: string,
|
|
215
188
|
destDir: string,
|
|
216
189
|
): Promise<void> {
|
|
217
|
-
const platformUrl =
|
|
218
|
-
if (!platformUrl) {
|
|
219
|
-
throw new Error(
|
|
220
|
-
`Cannot fetch skill "${skillId}": VELLUM_PLATFORM_URL is not configured.`,
|
|
221
|
-
);
|
|
222
|
-
}
|
|
190
|
+
const platformUrl = getPlatformBaseUrl();
|
|
223
191
|
const url = `${platformUrl}/v1/skills/${encodeURIComponent(skillId)}/`;
|
|
224
192
|
const response = await fetch(url, {
|
|
225
193
|
headers: buildHeaders(),
|
|
@@ -61,8 +61,8 @@ function buildFencedCodeRanges(body: string): Array<[number, number]> {
|
|
|
61
61
|
const fenceRe = /^(`{3,}|~{3,})(.*)?$/gm;
|
|
62
62
|
let openFence: { index: number; delimiter: string } | undefined;
|
|
63
63
|
|
|
64
|
-
let match: RegExpExecArray |
|
|
65
|
-
while ((match = fenceRe.exec(body)
|
|
64
|
+
let match: RegExpExecArray | null;
|
|
65
|
+
while ((match = fenceRe.exec(body)) !== null) {
|
|
66
66
|
const delimiter = match[1];
|
|
67
67
|
if (openFence === undefined) {
|
|
68
68
|
// Opening fence
|
|
@@ -125,10 +125,10 @@ export function parseInlineCommandExpansions(
|
|
|
125
125
|
// We use a non-greedy match to find the first closing backtick.
|
|
126
126
|
const tokenRe = /!\`([^`]*)\`/g;
|
|
127
127
|
|
|
128
|
-
let match: RegExpExecArray |
|
|
128
|
+
let match: RegExpExecArray | null;
|
|
129
129
|
let placeholderCounter = 0;
|
|
130
130
|
|
|
131
|
-
while ((match = tokenRe.exec(body)
|
|
131
|
+
while ((match = tokenRe.exec(body)) !== null) {
|
|
132
132
|
const startOffset = match.index;
|
|
133
133
|
const endOffset = startOffset + match[0].length;
|
|
134
134
|
const rawCommand = match[1];
|
|
@@ -172,12 +172,12 @@ export function parseInlineCommandExpansions(
|
|
|
172
172
|
const matchedStarts = new Set<number>();
|
|
173
173
|
// Re-run the token regex to collect all matched positions
|
|
174
174
|
tokenRe.lastIndex = 0;
|
|
175
|
-
while ((match = tokenRe.exec(body)
|
|
175
|
+
while ((match = tokenRe.exec(body)) !== null) {
|
|
176
176
|
matchedStarts.add(match.index);
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
let unmatchedMatch: RegExpExecArray |
|
|
180
|
-
while ((unmatchedMatch = unmatchedRe.exec(body)
|
|
179
|
+
let unmatchedMatch: RegExpExecArray | null;
|
|
180
|
+
while ((unmatchedMatch = unmatchedRe.exec(body)) !== null) {
|
|
181
181
|
const offset = unmatchedMatch.index;
|
|
182
182
|
|
|
183
183
|
// Skip if this was already matched as a complete token
|
package/src/subagent/manager.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import type { ServerMessage } from "../daemon/message-protocol.js";
|
|
19
19
|
import { bootstrapConversation } from "../memory/conversation-bootstrap.js";
|
|
20
20
|
import { RateLimitProvider } from "../providers/ratelimit.js";
|
|
21
|
-
import {
|
|
21
|
+
import { getProvider } from "../providers/registry.js";
|
|
22
22
|
import { getLogger } from "../util/logger.js";
|
|
23
23
|
import { getSandboxWorkingDir } from "../util/platform.js";
|
|
24
24
|
import {
|
|
@@ -129,10 +129,7 @@ export class SubagentManager {
|
|
|
129
129
|
|
|
130
130
|
// ── Build conversation dependencies ─────────────────────────────
|
|
131
131
|
const appConfig = getConfig();
|
|
132
|
-
let provider =
|
|
133
|
-
appConfig.services.inference.provider,
|
|
134
|
-
appConfig.providerOrder,
|
|
135
|
-
);
|
|
132
|
+
let provider = getProvider(appConfig.services.inference.provider);
|
|
136
133
|
const { rateLimit } = appConfig;
|
|
137
134
|
if (rateLimit.maxRequestsPerMinute > 0) {
|
|
138
135
|
provider = new RateLimitProvider(
|
package/src/tools/acp/spawn.ts
CHANGED
|
@@ -1,7 +1,68 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
|
|
1
4
|
import { getAcpSessionManager } from "../../acp/index.js";
|
|
2
5
|
import { getConfig } from "../../config/loader.js";
|
|
6
|
+
import { getLogger } from "../../util/logger.js";
|
|
3
7
|
import type { ToolContext, ToolExecutionResult } from "../types.js";
|
|
4
8
|
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
const log = getLogger("acp:spawn");
|
|
11
|
+
|
|
12
|
+
/** Cache so we only check once per process lifetime. */
|
|
13
|
+
let adapterVersionChecked = false;
|
|
14
|
+
|
|
15
|
+
interface AdapterVersionInfo {
|
|
16
|
+
outdated: true;
|
|
17
|
+
installed: string;
|
|
18
|
+
latest: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Checks if the globally-installed claude-agent-acp adapter is outdated.
|
|
23
|
+
* Runs at most once per process lifetime. Does NOT auto-update — returns
|
|
24
|
+
* version info so the caller can ask the user first.
|
|
25
|
+
*/
|
|
26
|
+
async function checkAdapterVersion(
|
|
27
|
+
command: string,
|
|
28
|
+
): Promise<AdapterVersionInfo | null> {
|
|
29
|
+
if (adapterVersionChecked || command !== "claude-agent-acp") {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const { stdout: installedRaw } = await execFileAsync("npm", [
|
|
35
|
+
"ls",
|
|
36
|
+
"-g",
|
|
37
|
+
"--json",
|
|
38
|
+
"@zed-industries/claude-agent-acp",
|
|
39
|
+
]);
|
|
40
|
+
const { stdout: latestRaw } = await execFileAsync("npm", [
|
|
41
|
+
"view",
|
|
42
|
+
"@zed-industries/claude-agent-acp",
|
|
43
|
+
"version",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const installed =
|
|
47
|
+
JSON.parse(installedRaw)?.dependencies?.[
|
|
48
|
+
"@zed-industries/claude-agent-acp"
|
|
49
|
+
]?.version;
|
|
50
|
+
const latest = latestRaw.trim();
|
|
51
|
+
|
|
52
|
+
adapterVersionChecked = true;
|
|
53
|
+
|
|
54
|
+
if (!installed || !latest || installed === latest) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
log.info({ installed, latest }, "claude-agent-acp is outdated");
|
|
59
|
+
return { outdated: true, installed, latest };
|
|
60
|
+
} catch (err) {
|
|
61
|
+
log.warn({ err }, "Failed to check claude-agent-acp version");
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
5
66
|
export async function executeAcpSpawn(
|
|
6
67
|
input: Record<string, unknown>,
|
|
7
68
|
context: ToolContext,
|
|
@@ -37,6 +98,17 @@ export async function executeAcpSpawn(
|
|
|
37
98
|
};
|
|
38
99
|
}
|
|
39
100
|
|
|
101
|
+
// Check if the ACP adapter is outdated before spawning
|
|
102
|
+
const versionInfo = await checkAdapterVersion(agentConfig.command);
|
|
103
|
+
if (versionInfo) {
|
|
104
|
+
return {
|
|
105
|
+
content:
|
|
106
|
+
`claude-agent-acp is outdated (installed: ${versionInfo.installed}, latest: ${versionInfo.latest}). ` +
|
|
107
|
+
`Ask the user if they'd like to update. If yes, run: npm install -g @zed-industries/claude-agent-acp@${versionInfo.latest} — then retry acp_spawn.`,
|
|
108
|
+
isError: true,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
40
112
|
try {
|
|
41
113
|
const manager = getAcpSessionManager();
|
|
42
114
|
const cwd = (input.cwd as string) || context.workingDir;
|
|
@@ -64,7 +136,12 @@ export async function executeAcpSpawn(
|
|
|
64
136
|
isError: false,
|
|
65
137
|
};
|
|
66
138
|
} catch (err) {
|
|
67
|
-
const msg =
|
|
139
|
+
const msg =
|
|
140
|
+
err instanceof Error
|
|
141
|
+
? err.message
|
|
142
|
+
: typeof err === "object" && err !== undefined
|
|
143
|
+
? JSON.stringify(err)
|
|
144
|
+
: String(err);
|
|
68
145
|
return { content: `Failed to spawn ACP agent: ${msg}`, isError: true };
|
|
69
146
|
}
|
|
70
147
|
}
|