@vellumai/assistant 0.11.1 → 0.11.2-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/Dockerfile +1 -3
- package/README.md +1 -1
- package/eslint.config.mjs +28 -8
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
- package/openapi.yaml +292 -5
- package/package.json +1 -1
- package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
- package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
- package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
- package/src/__tests__/compaction-events.test.ts +139 -2
- package/src/__tests__/config-loader-backfill.test.ts +35 -3
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
- package/src/__tests__/conversation-agent-loop.test.ts +180 -7
- package/src/__tests__/conversation-error.test.ts +211 -4
- package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
- package/src/__tests__/conversation-queue.test.ts +126 -0
- package/src/__tests__/conversation-retry-route.test.ts +46 -7
- package/src/__tests__/conversation-slash.test.ts +4 -2
- package/src/__tests__/conversation-summarize-route.test.ts +7 -1
- package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
- package/src/__tests__/document-create-dedupe.test.ts +2 -1
- package/src/__tests__/document-find-replace.test.ts +2 -1
- package/src/__tests__/document-tool-security.test.ts +2 -1
- package/src/__tests__/document-update-default-surface.test.ts +2 -1
- package/src/__tests__/document-workspace-file.test.ts +467 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
- package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
- package/src/__tests__/http-user-message-parity.test.ts +66 -0
- package/src/__tests__/list-messages-attachments.test.ts +87 -0
- package/src/__tests__/list-messages-provider-error.test.ts +143 -0
- package/src/__tests__/list-messages-system-card.test.ts +100 -0
- package/src/__tests__/llm-resolver.test.ts +19 -9
- package/src/__tests__/managed-profile-guard.test.ts +23 -0
- package/src/__tests__/notification-platform-adapter.test.ts +130 -2
- package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
- package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/provider-error-scenarios.test.ts +140 -0
- package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
- package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
- package/src/__tests__/skills.test.ts +27 -0
- package/src/__tests__/slack-channels-routes.test.ts +0 -2
- package/src/__tests__/slack-share-routes.test.ts +0 -3
- package/src/__tests__/slack-users-routes.test.ts +0 -2
- package/src/__tests__/subagent-tools.test.ts +11 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
- package/src/__tests__/tool-result-spool.test.ts +5 -4
- package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
- package/src/__tests__/turn-events-store.test.ts +26 -0
- package/src/__tests__/ui-visual-surface.test.ts +695 -0
- package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
- package/src/__tests__/visible-app-context.test.ts +189 -0
- package/src/__tests__/workspace-git-service.test.ts +173 -33
- package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
- package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
- package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
- package/src/agent/loop.ts +36 -7
- package/src/api/events/context-window-usage.ts +31 -0
- package/src/api/events/notification-intent.ts +8 -0
- package/src/api/events/ui-surface-pending.ts +35 -0
- package/src/api/index.ts +14 -0
- package/src/api/responses/conversation-message.ts +25 -4
- package/src/api/surfaces.ts +90 -1
- package/src/approvals/guardian-card-withdrawal.ts +66 -31
- package/src/approvals/guardian-decision-primitive.ts +4 -0
- package/src/calls/__tests__/call-setup-router.test.ts +156 -26
- package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
- package/src/calls/call-setup-router.ts +80 -36
- package/src/calls/voice-session-bridge.ts +173 -26
- package/src/cli/commands/inference.help.ts +3 -3
- package/src/cli/commands/notifications.help.ts +3 -2
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
- package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
- package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
- package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
- package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
- package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
- package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
- package/src/cli/commands/platform/index.help.ts +92 -0
- package/src/cli/commands/platform/index.ts +7 -0
- package/src/cli/commands/platform/invoices.ts +132 -0
- package/src/cli/commands/usage.help.ts +1 -1
- package/src/cli/lib/list-installed-plugins.ts +2 -1
- package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
- package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
- package/src/config/assistant-feature-flags.ts +7 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
- package/src/config/bundled-skills/subagent/SKILL.md +3 -1
- package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
- package/src/config/bundled-skills/visualize/SKILL.md +163 -0
- package/src/config/call-site-defaults.ts +3 -2
- package/src/config/default-profile-catalog.ts +75 -70
- package/src/config/default-profile-names.ts +13 -28
- package/src/config/env-registry.ts +1 -0
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/llm-resolver.ts +21 -5
- package/src/config/loader.ts +18 -11
- package/src/config/schemas/llm.ts +1 -9
- package/src/config/schemas/memory-retrospective.ts +9 -0
- package/src/config/schemas/monitoring.ts +28 -2
- package/src/config/schemas/workspace-git.ts +15 -0
- package/src/config/seed-inference-profiles.ts +19 -5
- package/src/context/post-turn-tool-result-truncation.ts +2 -2
- package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
- package/src/conversations/message-consolidation.ts +17 -15
- package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
- package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
- package/src/daemon/conversation-agent-loop.ts +107 -22
- package/src/daemon/conversation-error.ts +169 -48
- package/src/daemon/conversation-messaging.ts +87 -4
- package/src/daemon/conversation-process.ts +57 -45
- package/src/daemon/conversation-runtime-assembly.ts +57 -0
- package/src/daemon/conversation-store.ts +36 -1
- package/src/daemon/conversation-surfaces.ts +68 -6
- package/src/daemon/conversation-turn-finalize.ts +76 -18
- package/src/daemon/conversation.ts +122 -27
- package/src/daemon/lifecycle.ts +9 -0
- package/src/daemon/message-types/conversations.ts +8 -0
- package/src/daemon/message-types/surfaces.ts +3 -0
- package/src/documents/document-store.ts +247 -10
- package/src/home/__tests__/feed-types.test.ts +43 -8
- package/src/home/__tests__/feed-writer.test.ts +59 -0
- package/src/home/feed-types.ts +34 -7
- package/src/home/feed-writer.ts +15 -6
- package/src/live-voice/__tests__/activity-label.test.ts +68 -0
- package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +185 -25
- package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
- package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
- package/src/live-voice/activity-label.ts +141 -0
- package/src/live-voice/live-activity-reporter.ts +35 -6
- package/src/live-voice/live-voice-session.ts +171 -41
- package/src/live-voice/protocol.ts +23 -0
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
- package/src/messaging/providers/slack/adapter.ts +86 -19
- package/src/messaging/providers/slack/api.test.ts +85 -1
- package/src/messaging/providers/slack/api.ts +121 -288
- package/src/messaging/providers/slack/client.ts +20 -251
- package/src/messaging/providers/slack/send.test.ts +4 -9
- package/src/messaging/providers/slack/send.ts +1 -1
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
- package/src/messaging/providers/slack/web-api-transport.ts +367 -0
- package/src/messaging/providers/slack/withdraw.ts +6 -14
- package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
- package/src/messaging/providers/telegram-bot/send.ts +23 -2
- package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
- package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
- package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
- package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
- package/src/monitoring/file-descriptors.ts +262 -0
- package/src/monitoring/process-memory.ts +4 -11
- package/src/monitoring/resource-sampler.ts +5 -0
- package/src/monitoring/worker.ts +12 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
- package/src/notifications/__tests__/broadcaster.test.ts +347 -7
- package/src/notifications/__tests__/copy-composer.test.ts +53 -3
- package/src/notifications/__tests__/decision-engine.test.ts +349 -29
- package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
- package/src/notifications/__tests__/edit-notification.test.ts +330 -0
- package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
- package/src/notifications/adapters/macos.ts +3 -0
- package/src/notifications/adapters/platform.ts +90 -14
- package/src/notifications/adapters/telegram.ts +6 -4
- package/src/notifications/assistant-reply-producer.ts +219 -0
- package/src/notifications/broadcaster.ts +523 -317
- package/src/notifications/copy-composer.ts +27 -6
- package/src/notifications/decision-engine.ts +154 -77
- package/src/notifications/deterministic-checks.ts +6 -7
- package/src/notifications/edit-notification.ts +8 -4
- package/src/notifications/emit-signal.ts +49 -16
- package/src/notifications/guardian-delivery-recorder.ts +8 -2
- package/src/notifications/home-feed-side-effect.ts +43 -6
- package/src/notifications/notification-utils.ts +39 -4
- package/src/notifications/signal.ts +10 -0
- package/src/notifications/types.ts +17 -0
- package/src/persistence/bookmark-crud.ts +18 -9
- package/src/persistence/conversation-attention-store.ts +26 -0
- package/src/persistence/conversation-crud.ts +79 -32
- package/src/persistence/conversation-queries.ts +30 -15
- package/src/persistence/conversation-title-service.ts +5 -170
- package/src/persistence/conversation-types.ts +192 -1
- package/src/persistence/db-init.ts +14 -1
- package/src/persistence/db-maintenance.ts +5 -4
- package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
- package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
- package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
- package/src/persistence/embeddings/embedding-local.ts +2 -1
- package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
- package/src/persistence/embeddings/plugin-index.ts +84 -12
- package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
- package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
- package/src/persistence/planner-statistics.ts +14 -0
- package/src/persistence/schema/documents.ts +26 -11
- package/src/persistence/steps.ts +2 -0
- package/src/platform/client.test.ts +172 -2
- package/src/platform/client.ts +178 -55
- package/src/plugin-api/conversation-turn.ts +13 -0
- package/src/plugin-api/model-profiles.test.ts +6 -2
- package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
- package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
- package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
- package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
- package/src/plugins/defaults/memory/host-utils.ts +5 -0
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
- package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
- package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
- package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
- package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
- package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
- package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
- package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
- package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
- package/src/plugins/defaults/memory/v2/injection.ts +48 -3
- package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
- package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
- package/src/plugins/defaults/turn-context/injectors.ts +1 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
- package/src/plugins/types.ts +17 -0
- package/src/prompts/templates/system-sections.ts +2 -2
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
- package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
- package/src/providers/call-site-routing.ts +77 -14
- package/src/providers/connection-resolution.ts +42 -2
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
- package/src/providers/inference/adapter-factory.ts +70 -12
- package/src/providers/model-catalog.ts +0 -11
- package/src/providers/model-intents.ts +11 -0
- package/src/providers/registry.ts +8 -0
- package/src/providers/retry.ts +85 -17
- package/src/providers/types.ts +8 -1
- package/src/runtime/__tests__/agent-wake.test.ts +181 -0
- package/src/runtime/agent-wake.ts +132 -9
- package/src/runtime/channel-approval-types.ts +25 -0
- package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
- package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
- package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
- package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
- package/src/runtime/routes/consolidation-routes.ts +1 -1
- package/src/runtime/routes/conversation-management-routes.ts +18 -6
- package/src/runtime/routes/conversation-query-routes.ts +14 -23
- package/src/runtime/routes/conversation-routes.ts +113 -8
- package/src/runtime/routes/credential-routes.ts +1 -5
- package/src/runtime/routes/documents-routes.ts +207 -12
- package/src/runtime/routes/identity-routes.ts +3 -93
- package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
- package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
- package/src/runtime/routes/notification-routes.ts +4 -1
- package/src/runtime/routes/platform-routes.ts +238 -3
- package/src/runtime/routes/playground/guard.ts +1 -2
- package/src/runtime/routes/slack-channel-routes.ts +2 -4
- package/src/runtime/routes/workspace-commit-routes.ts +49 -2
- package/src/runtime/routes/workspace-routes.ts +11 -12
- package/src/runtime/routes/workspace-utils.ts +49 -2
- package/src/runtime/services/conversation-serializer.ts +2 -4
- package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
- package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
- package/src/subagent/__tests__/consult-context.test.ts +84 -0
- package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
- package/src/subagent/consult-context.ts +410 -0
- package/src/subagent/consult-prompt.ts +38 -5
- package/src/subagent/manager.ts +9 -4
- package/src/subagent/types.ts +8 -0
- package/src/telemetry/telemetry-event-sources.ts +7 -0
- package/src/telemetry/telemetry-wire-source.json +1 -1
- package/src/telemetry/telemetry-wire.generated.ts +1 -0
- package/src/telemetry/turn-events-store.ts +30 -1
- package/src/telemetry/types.ts +24 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
- package/src/tools/browser/pinned-tabs.ts +3 -1
- package/src/tools/subagent/spawn.ts +23 -1
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/tools/ui-surface/definitions.ts +39 -1
- package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
- package/src/tools/ui-surface/visual-validation.ts +787 -0
- package/src/tools/workflows/run-workflow.test.ts +1 -0
- package/src/util/__tests__/short-title.test.ts +229 -0
- package/src/util/__tests__/worker-compute.test.ts +65 -0
- package/src/util/cgroup-cpu.ts +93 -0
- package/src/util/errors.ts +27 -0
- package/src/util/process-tree.ts +19 -0
- package/src/util/short-title.ts +189 -0
- package/src/util/worker-compute.ts +83 -0
- package/src/workspace/byok-default-profile-ensure.ts +25 -15
- package/src/workspace/git-service.ts +79 -30
- package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
- package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
- package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
package/src/providers/retry.ts
CHANGED
|
@@ -4,7 +4,11 @@ import {
|
|
|
4
4
|
resolveUsageAttribution,
|
|
5
5
|
sanitizeUsageMetadataValue,
|
|
6
6
|
} from "../usage/attribution.js";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
type ProviderCredentialSource,
|
|
9
|
+
ProviderError,
|
|
10
|
+
type ProviderErrorReason,
|
|
11
|
+
} from "../util/errors.js";
|
|
8
12
|
import { getLogger } from "../util/logger.js";
|
|
9
13
|
import {
|
|
10
14
|
computeRetryDelay,
|
|
@@ -824,6 +828,8 @@ function addSanitizedHeader(
|
|
|
824
828
|
export class RetryProvider implements Provider {
|
|
825
829
|
public readonly name: string;
|
|
826
830
|
|
|
831
|
+
private inner: Provider;
|
|
832
|
+
|
|
827
833
|
get tokenEstimationProvider(): string | undefined {
|
|
828
834
|
return this.inner.tokenEstimationProvider;
|
|
829
835
|
}
|
|
@@ -842,24 +848,63 @@ export class RetryProvider implements Provider {
|
|
|
842
848
|
// the wrapper chain (callers gate on its presence). Bound straight to the
|
|
843
849
|
// inner provider — count_tokens is a cheap separate endpoint and its caller
|
|
844
850
|
// already falls back on error, so it needs no retry wrapping.
|
|
851
|
+
// Deliberately not re-bound when a credential refresh swaps `inner`: every
|
|
852
|
+
// outer wrapper snapshots this the same way at construction, so a re-bind
|
|
853
|
+
// here would never reach callers. count_tokens on the pre-refresh credential
|
|
854
|
+
// fails soft — its caller falls back to estimation.
|
|
845
855
|
public readonly countInputTokens?: NonNullable<Provider["countInputTokens"]>;
|
|
846
856
|
|
|
847
857
|
constructor(
|
|
848
|
-
|
|
849
|
-
private readonly options: {
|
|
858
|
+
inner: Provider,
|
|
859
|
+
private readonly options: {
|
|
860
|
+
forwardUsageAttributionHeaders?: boolean;
|
|
861
|
+
credentialSource?: ProviderCredentialSource;
|
|
862
|
+
connectionName?: string;
|
|
863
|
+
refreshCredentialProvider?: () => Promise<Provider | null>;
|
|
864
|
+
} = {},
|
|
850
865
|
) {
|
|
866
|
+
this.inner = inner;
|
|
851
867
|
this.name = inner.name;
|
|
852
868
|
if (inner.countInputTokens) {
|
|
853
869
|
this.countInputTokens = inner.countInputTokens.bind(inner);
|
|
854
870
|
}
|
|
855
871
|
}
|
|
856
872
|
|
|
873
|
+
private shouldRefreshManagedCredential(error: unknown): boolean {
|
|
874
|
+
return (
|
|
875
|
+
this.options.credentialSource === "vellum-managed" &&
|
|
876
|
+
this.options.refreshCredentialProvider !== undefined &&
|
|
877
|
+
error instanceof ProviderError &&
|
|
878
|
+
(error.statusCode === 401 || error.statusCode === 403) &&
|
|
879
|
+
(error.reason === undefined ||
|
|
880
|
+
error.reason === "unknown" ||
|
|
881
|
+
error.reason === "invalid_credentials")
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
private attributeCredential(error: unknown): void {
|
|
886
|
+
const { credentialSource, connectionName } = this.options;
|
|
887
|
+
if (
|
|
888
|
+
!(error instanceof ProviderError) ||
|
|
889
|
+
(!credentialSource && !connectionName)
|
|
890
|
+
) {
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
// Merges under whatever a closer layer already stamped, so a route
|
|
894
|
+
// resolved at dispatch keeps precedence over this adapter's own view.
|
|
895
|
+
error.attachRouteAttribution({
|
|
896
|
+
...(credentialSource ? { credentialSource } : {}),
|
|
897
|
+
...(connectionName ? { connectionName } : {}),
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
857
901
|
async sendMessage(
|
|
858
902
|
messages: Message[],
|
|
859
903
|
options?: SendMessageOptions,
|
|
860
904
|
): Promise<ProviderResponse> {
|
|
861
|
-
let lastError: unknown;
|
|
862
905
|
let didRetry = false;
|
|
906
|
+
let retryAttempt = 0;
|
|
907
|
+
let credentialRefreshAttempted = false;
|
|
863
908
|
let messagesForAttempt = messages;
|
|
864
909
|
|
|
865
910
|
const normalizedOptions = normalizeSendMessageOptions(this.name, options, {
|
|
@@ -867,7 +912,7 @@ export class RetryProvider implements Provider {
|
|
|
867
912
|
this.options.forwardUsageAttributionHeaders === true,
|
|
868
913
|
});
|
|
869
914
|
|
|
870
|
-
|
|
915
|
+
while (true) {
|
|
871
916
|
try {
|
|
872
917
|
const result = await this.inner.sendMessage(
|
|
873
918
|
messagesForAttempt,
|
|
@@ -875,9 +920,37 @@ export class RetryProvider implements Provider {
|
|
|
875
920
|
);
|
|
876
921
|
return result;
|
|
877
922
|
} catch (error) {
|
|
878
|
-
|
|
923
|
+
if (
|
|
924
|
+
!credentialRefreshAttempted &&
|
|
925
|
+
this.shouldRefreshManagedCredential(error)
|
|
926
|
+
) {
|
|
927
|
+
credentialRefreshAttempted = true;
|
|
928
|
+
try {
|
|
929
|
+
const refreshed = await this.options.refreshCredentialProvider?.();
|
|
930
|
+
if (refreshed) {
|
|
931
|
+
this.inner = refreshed;
|
|
932
|
+
log.info(
|
|
933
|
+
{
|
|
934
|
+
provider: this.name,
|
|
935
|
+
connectionName: this.options.connectionName,
|
|
936
|
+
},
|
|
937
|
+
"Retrying managed inference with refreshed assistant credentials",
|
|
938
|
+
);
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
} catch (refreshError) {
|
|
942
|
+
log.warn(
|
|
943
|
+
{
|
|
944
|
+
provider: this.name,
|
|
945
|
+
connectionName: this.options.connectionName,
|
|
946
|
+
refreshError,
|
|
947
|
+
},
|
|
948
|
+
"Failed to reload managed assistant credentials",
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
879
952
|
|
|
880
|
-
if (
|
|
953
|
+
if (retryAttempt < DEFAULT_MAX_RETRIES && isRetryableError(error)) {
|
|
881
954
|
// Malformed tool-argument JSON is conditioned on the request, so
|
|
882
955
|
// resend with the corrective note. Built from the original
|
|
883
956
|
// `messages` each time — the note appears exactly once no matter
|
|
@@ -890,7 +963,8 @@ export class RetryProvider implements Provider {
|
|
|
890
963
|
error instanceof ProviderError ? error.retryAfterMs : undefined;
|
|
891
964
|
const MAX_RETRY_DELAY_MS = 60_000; // Cap server-suggested delays at 60s
|
|
892
965
|
const delay = Math.min(
|
|
893
|
-
retryAfter ??
|
|
966
|
+
retryAfter ??
|
|
967
|
+
computeRetryDelay(retryAttempt, DEFAULT_BASE_DELAY_MS),
|
|
894
968
|
MAX_RETRY_DELAY_MS,
|
|
895
969
|
);
|
|
896
970
|
const errorType =
|
|
@@ -909,7 +983,7 @@ export class RetryProvider implements Provider {
|
|
|
909
983
|
: "network_error";
|
|
910
984
|
log.warn(
|
|
911
985
|
{
|
|
912
|
-
attempt:
|
|
986
|
+
attempt: retryAttempt + 1,
|
|
913
987
|
maxRetries: DEFAULT_MAX_RETRIES,
|
|
914
988
|
delay,
|
|
915
989
|
retryAfterHeader: retryAfter !== undefined,
|
|
@@ -921,6 +995,7 @@ export class RetryProvider implements Provider {
|
|
|
921
995
|
"Retrying after transient error",
|
|
922
996
|
);
|
|
923
997
|
didRetry = true;
|
|
998
|
+
retryAttempt++;
|
|
924
999
|
await sleep(delay);
|
|
925
1000
|
continue;
|
|
926
1001
|
}
|
|
@@ -936,16 +1011,9 @@ export class RetryProvider implements Provider {
|
|
|
936
1011
|
true;
|
|
937
1012
|
}
|
|
938
1013
|
|
|
1014
|
+
this.attributeCredential(error);
|
|
939
1015
|
throw error;
|
|
940
1016
|
}
|
|
941
1017
|
}
|
|
942
|
-
|
|
943
|
-
// Unreachable in practice — the loop body always either returns or throws —
|
|
944
|
-
// but mark the last error in case execution somehow falls through.
|
|
945
|
-
if (lastError instanceof Error && isRetryableError(lastError)) {
|
|
946
|
-
(lastError as Error & { retriesExhausted?: boolean }).retriesExhausted =
|
|
947
|
-
true;
|
|
948
|
-
}
|
|
949
|
-
throw lastError;
|
|
950
1018
|
}
|
|
951
1019
|
}
|
package/src/providers/types.ts
CHANGED
|
@@ -2,7 +2,11 @@ import type { ToolDefinition } from "../tools/tool-types.js";
|
|
|
2
2
|
export type { ToolDefinition };
|
|
3
3
|
|
|
4
4
|
import type { LLMCallSite } from "../config/schemas/llm.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
ProviderError,
|
|
7
|
+
type ProviderErrorReason,
|
|
8
|
+
type ProviderRouteAttribution,
|
|
9
|
+
} from "../util/errors.js";
|
|
6
10
|
|
|
7
11
|
export interface TextContent {
|
|
8
12
|
type: "text";
|
|
@@ -177,6 +181,7 @@ export interface Message {
|
|
|
177
181
|
|
|
178
182
|
export type ModelIntent =
|
|
179
183
|
| "balanced"
|
|
184
|
+
| "cost-optimized"
|
|
180
185
|
| "latency-optimized"
|
|
181
186
|
| "quality-optimized"
|
|
182
187
|
| "vision-optimized";
|
|
@@ -359,6 +364,8 @@ export interface SendMessageOptions {
|
|
|
359
364
|
|
|
360
365
|
export interface Provider {
|
|
361
366
|
name: string;
|
|
367
|
+
/** Connection route whose credentials this provider instance uses. */
|
|
368
|
+
routeAttribution?: ProviderRouteAttribution;
|
|
362
369
|
/**
|
|
363
370
|
* Provider key used by the local token estimator to select model-family
|
|
364
371
|
* specific rules (e.g. Anthropic's `width * height / 750` image sizing).
|
|
@@ -1624,6 +1624,187 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1624
1624
|
expect(conversation.persistedTailCalls.length).toBeGreaterThan(0);
|
|
1625
1625
|
});
|
|
1626
1626
|
|
|
1627
|
+
test("reports run_error when the loop swallows a provider rejection into a no-output stop", async () => {
|
|
1628
|
+
// The loop's catch does not rethrow provider rejections: it emits
|
|
1629
|
+
// `error` + `agent_loop_exit(reason: "error")` and returns the history
|
|
1630
|
+
// unchanged. Without the exit-reason gate this read as a successful
|
|
1631
|
+
// silent no-op, so state-advancing callers (the memory retrospective
|
|
1632
|
+
// watermark) permanently consumed their trigger on a pass that never
|
|
1633
|
+
// ran.
|
|
1634
|
+
const conversation = makeWakeConversation({
|
|
1635
|
+
conversationId: "conv-swallowed-rejection",
|
|
1636
|
+
runImpl: async (input, onEvent) => {
|
|
1637
|
+
await onEvent({
|
|
1638
|
+
type: "error",
|
|
1639
|
+
error: new Error("provider 400: image input not supported"),
|
|
1640
|
+
});
|
|
1641
|
+
await onEvent({ type: "agent_loop_exit", reason: "error" });
|
|
1642
|
+
return runResult([...input]);
|
|
1643
|
+
},
|
|
1644
|
+
});
|
|
1645
|
+
|
|
1646
|
+
const result = await wakeAgentForOpportunity(
|
|
1647
|
+
{ conversationId: "conv-swallowed-rejection", hint: "boom", source: "t" },
|
|
1648
|
+
{ resolveTarget: async () => conversation },
|
|
1649
|
+
);
|
|
1650
|
+
|
|
1651
|
+
expect(result).toEqual({
|
|
1652
|
+
invoked: false,
|
|
1653
|
+
producedToolCalls: false,
|
|
1654
|
+
reason: "run_error",
|
|
1655
|
+
});
|
|
1656
|
+
// Nothing pushed, persisted, or emitted: the failed run left no trace.
|
|
1657
|
+
expect(conversation.pushedMessages).toEqual([]);
|
|
1658
|
+
expect(conversation.persistedTailCalls).toEqual([]);
|
|
1659
|
+
expect(conversation.emittedEvents).toHaveLength(0);
|
|
1660
|
+
});
|
|
1661
|
+
|
|
1662
|
+
test("reports run_error when a no-output run was aborted", async () => {
|
|
1663
|
+
const conversation = makeWakeConversation({
|
|
1664
|
+
conversationId: "conv-aborted-no-output",
|
|
1665
|
+
runImpl: async (input, onEvent) => {
|
|
1666
|
+
await onEvent({ type: "agent_loop_exit", reason: "aborted_pre_call" });
|
|
1667
|
+
return runResult([...input]);
|
|
1668
|
+
},
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1671
|
+
const result = await wakeAgentForOpportunity(
|
|
1672
|
+
{ conversationId: "conv-aborted-no-output", hint: "boom", source: "t" },
|
|
1673
|
+
{ resolveTarget: async () => conversation },
|
|
1674
|
+
);
|
|
1675
|
+
|
|
1676
|
+
expect(result).toEqual({
|
|
1677
|
+
invoked: false,
|
|
1678
|
+
producedToolCalls: false,
|
|
1679
|
+
reason: "run_error",
|
|
1680
|
+
});
|
|
1681
|
+
});
|
|
1682
|
+
|
|
1683
|
+
test("reports run_error when a no-output run exhausted its output budget (max_tokens_reached)", async () => {
|
|
1684
|
+
// Hidden thinking can consume the whole output budget before any
|
|
1685
|
+
// visible text or executable tool call lands. That is "no usable
|
|
1686
|
+
// output", not a clean silent no-op: state-advancing callers must see
|
|
1687
|
+
// a retryable failure. A truncated response that produced SOME output
|
|
1688
|
+
// takes the normal produced-output path and never reaches this check.
|
|
1689
|
+
const conversation = makeWakeConversation({
|
|
1690
|
+
conversationId: "conv-max-tokens-no-output",
|
|
1691
|
+
runImpl: async (input, onEvent) => {
|
|
1692
|
+
await onEvent({
|
|
1693
|
+
type: "agent_loop_exit",
|
|
1694
|
+
reason: "max_tokens_reached",
|
|
1695
|
+
});
|
|
1696
|
+
return runResult([...input]);
|
|
1697
|
+
},
|
|
1698
|
+
});
|
|
1699
|
+
|
|
1700
|
+
const result = await wakeAgentForOpportunity(
|
|
1701
|
+
{
|
|
1702
|
+
conversationId: "conv-max-tokens-no-output",
|
|
1703
|
+
hint: "boom",
|
|
1704
|
+
source: "t",
|
|
1705
|
+
},
|
|
1706
|
+
{ resolveTarget: async () => conversation },
|
|
1707
|
+
);
|
|
1708
|
+
|
|
1709
|
+
expect(result).toEqual({
|
|
1710
|
+
invoked: false,
|
|
1711
|
+
producedToolCalls: false,
|
|
1712
|
+
reason: "run_error",
|
|
1713
|
+
});
|
|
1714
|
+
});
|
|
1715
|
+
|
|
1716
|
+
test("keeps invoked true on a non-clean exit when a checkpoint already went live", async () => {
|
|
1717
|
+
// Mirror of the throw path's went-live guard: once a checkpoint fired,
|
|
1718
|
+
// side effects (e.g. `remember` calls) have already landed, so even a
|
|
1719
|
+
// rebased history whose tail slice reads empty must not report the run
|
|
1720
|
+
// as skipped: callers would re-run a pass whose side effects fired.
|
|
1721
|
+
const assistantToolMsg: Message = {
|
|
1722
|
+
role: "assistant",
|
|
1723
|
+
content: [{ type: "tool_use", id: "tu-1", name: "remember", input: {} }],
|
|
1724
|
+
};
|
|
1725
|
+
const toolResultMsg: Message = {
|
|
1726
|
+
role: "user",
|
|
1727
|
+
content: [{ type: "tool_result", tool_use_id: "tu-1", content: "saved" }],
|
|
1728
|
+
};
|
|
1729
|
+
const conversation = makeWakeConversation({
|
|
1730
|
+
conversationId: "conv-live-then-error",
|
|
1731
|
+
runImpl: async (input, onEvent, runOptions) => {
|
|
1732
|
+
const history = [...input, assistantToolMsg, toolResultMsg];
|
|
1733
|
+
await runOptions?.onCheckpoint?.({
|
|
1734
|
+
turnIndex: 0,
|
|
1735
|
+
toolCount: 1,
|
|
1736
|
+
hasToolUse: true,
|
|
1737
|
+
history,
|
|
1738
|
+
});
|
|
1739
|
+
await onEvent({ type: "agent_loop_exit", reason: "error" });
|
|
1740
|
+
// Return a history rebased below the wake's tail boundary, as the
|
|
1741
|
+
// loop's deep-repair / recovery-hook paths can produce.
|
|
1742
|
+
return runResult([...input]);
|
|
1743
|
+
},
|
|
1744
|
+
});
|
|
1745
|
+
|
|
1746
|
+
const result = await wakeAgentForOpportunity(
|
|
1747
|
+
{ conversationId: "conv-live-then-error", hint: "boom", source: "t" },
|
|
1748
|
+
{ resolveTarget: async () => conversation },
|
|
1749
|
+
);
|
|
1750
|
+
|
|
1751
|
+
expect(result).toEqual({ invoked: true, producedToolCalls: false });
|
|
1752
|
+
// The checkpoint's flush pushed + persisted the partial tail.
|
|
1753
|
+
expect(conversation.pushedMessages.length).toBeGreaterThan(0);
|
|
1754
|
+
expect(conversation.persistedTailCalls.length).toBeGreaterThan(0);
|
|
1755
|
+
});
|
|
1756
|
+
|
|
1757
|
+
test("keeps the silent no-op on a clean no_tool_calls exit with no output", async () => {
|
|
1758
|
+
// A clean model-driven stop that produced nothing stays `invoked: true`:
|
|
1759
|
+
// the pass really ran, the model just had nothing to say.
|
|
1760
|
+
const conversation = makeWakeConversation({
|
|
1761
|
+
conversationId: "conv-clean-empty",
|
|
1762
|
+
runImpl: async (input, onEvent) => {
|
|
1763
|
+
await onEvent({ type: "agent_loop_exit", reason: "no_tool_calls" });
|
|
1764
|
+
return runResult([...input]);
|
|
1765
|
+
},
|
|
1766
|
+
});
|
|
1767
|
+
|
|
1768
|
+
const result = await wakeAgentForOpportunity(
|
|
1769
|
+
{ conversationId: "conv-clean-empty", hint: "hi", source: "t" },
|
|
1770
|
+
{ resolveTarget: async () => conversation },
|
|
1771
|
+
);
|
|
1772
|
+
|
|
1773
|
+
expect(result).toEqual({ invoked: true, producedToolCalls: false });
|
|
1774
|
+
});
|
|
1775
|
+
|
|
1776
|
+
test("reports no_output on a clean empty reply when the caller requires usable output", async () => {
|
|
1777
|
+
// Same clean no_tool_calls empty run as above, but the caller consumes
|
|
1778
|
+
// its trigger on success (memory retrospective): the empty reply must
|
|
1779
|
+
// read as a retryable failure, not a silent success.
|
|
1780
|
+
const conversation = makeWakeConversation({
|
|
1781
|
+
conversationId: "conv-clean-empty-required",
|
|
1782
|
+
runImpl: async (input, onEvent) => {
|
|
1783
|
+
await onEvent({ type: "agent_loop_exit", reason: "no_tool_calls" });
|
|
1784
|
+
return runResult([...input]);
|
|
1785
|
+
},
|
|
1786
|
+
});
|
|
1787
|
+
|
|
1788
|
+
const result = await wakeAgentForOpportunity(
|
|
1789
|
+
{
|
|
1790
|
+
conversationId: "conv-clean-empty-required",
|
|
1791
|
+
hint: "hi",
|
|
1792
|
+
source: "t",
|
|
1793
|
+
requireUsableOutput: true,
|
|
1794
|
+
},
|
|
1795
|
+
{ resolveTarget: async () => conversation },
|
|
1796
|
+
);
|
|
1797
|
+
|
|
1798
|
+
expect(result).toEqual({
|
|
1799
|
+
invoked: false,
|
|
1800
|
+
producedToolCalls: false,
|
|
1801
|
+
reason: "no_output",
|
|
1802
|
+
});
|
|
1803
|
+
// Nothing persisted or emitted by the failed pass.
|
|
1804
|
+
expect(conversation.persistedTailCalls).toEqual([]);
|
|
1805
|
+
expect(conversation.emittedEvents).toHaveLength(0);
|
|
1806
|
+
});
|
|
1807
|
+
|
|
1627
1808
|
test("elevates the turn's trust context before the agent loop runs", async () => {
|
|
1628
1809
|
// Background system jobs (e.g. memory consolidation) need guardian trust to
|
|
1629
1810
|
// clear the side-effect approval gate. The wake must set
|
|
@@ -32,8 +32,16 @@
|
|
|
32
32
|
* text stay on the ephemeral trio above.
|
|
33
33
|
* - Invokes the agent loop with all conversation tools available unless
|
|
34
34
|
* the caller provides an explicit `allowedTools` scope.
|
|
35
|
-
* - No tool calls AND no assistant text
|
|
36
|
-
* nothing emitted). Returns
|
|
35
|
+
* - No tool calls AND no assistant text on a clean model-driven stop →
|
|
36
|
+
* silent no-op (nothing persisted, nothing emitted). Returns
|
|
37
|
+
* `{ invoked: true, producedToolCalls: false }`. When the no-output run
|
|
38
|
+
* instead ended on a non-clean terminal exit (the loop swallowed a
|
|
39
|
+
* provider rejection, unhandled throw, or cancellation into a graceful
|
|
40
|
+
* no-output stop), returns `{ invoked: false, reason: "run_error" }` so
|
|
41
|
+
* state-advancing callers retry instead of recording a phantom pass.
|
|
42
|
+
* Callers that pass `requireUsableOutput: true` additionally get
|
|
43
|
+
* `{ invoked: false, reason: "no_output" }` for the clean-stop empty
|
|
44
|
+
* reply instead of the silent no-op.
|
|
37
45
|
* - Tool calls produced → normal tool execution runs (the conversation's
|
|
38
46
|
* `AgentLoop` has its tool executor already wired). Returns
|
|
39
47
|
* `{ invoked: true, producedToolCalls: true }`.
|
|
@@ -64,6 +72,7 @@
|
|
|
64
72
|
|
|
65
73
|
import type {
|
|
66
74
|
AgentEvent,
|
|
75
|
+
AgentLoopExitReason,
|
|
67
76
|
CheckpointDecision,
|
|
68
77
|
CheckpointInfo,
|
|
69
78
|
} from "../agent/loop.js";
|
|
@@ -175,6 +184,28 @@ function buildBackgroundEventText(
|
|
|
175
184
|
const OVER_WINDOW_REJECTION_LOG_MESSAGE =
|
|
176
185
|
"agent-wake: provider rejected the input as over-window with auto-compaction suppressed; failing the wake";
|
|
177
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Terminal `agent_loop_exit` reasons under which a no-output run is a
|
|
189
|
+
* legitimate silent no-op: the loop reached a real model reply (or a
|
|
190
|
+
* model-driven stop) and the model simply produced nothing worth emitting.
|
|
191
|
+
* Every other reason on a no-output run means the pass never committed
|
|
192
|
+
* usable output, so the wake must report `invoked: false` instead of a
|
|
193
|
+
* phantom success: `error` (the loop's catch swallowed a provider rejection
|
|
194
|
+
* or an unhandled throw into a graceful no-output stop), the `aborted_*`
|
|
195
|
+
* family (cancellation before any output), the overflow terminals, and
|
|
196
|
+
* `max_tokens_reached` (the output budget was exhausted, e.g. by hidden
|
|
197
|
+
* thinking, before any visible text or executable tool call landed; a
|
|
198
|
+
* truncated response that DID produce output takes the normal path and
|
|
199
|
+
* never reaches this check). Membership is an allowlist deliberately: a
|
|
200
|
+
* future exit reason defaults to the failure side, which state-advancing
|
|
201
|
+
* callers recover from by retrying, rather than the data-loss side.
|
|
202
|
+
*/
|
|
203
|
+
const CLEAN_NO_OUTPUT_EXIT_REASONS: ReadonlySet<AgentLoopExitReason> = new Set([
|
|
204
|
+
"no_tool_calls",
|
|
205
|
+
"yield_to_user",
|
|
206
|
+
"checkpoint_handoff",
|
|
207
|
+
]);
|
|
208
|
+
|
|
178
209
|
export interface WakeOptions {
|
|
179
210
|
conversationId: string;
|
|
180
211
|
hint: string;
|
|
@@ -263,6 +294,23 @@ export interface WakeOptions {
|
|
|
263
294
|
* retrospectives whose conversation title already says "(Retrospective)").
|
|
264
295
|
*/
|
|
265
296
|
suppressWakeSurface?: boolean;
|
|
297
|
+
/**
|
|
298
|
+
* Treat a run that commits NO usable output (no executable tool call, no
|
|
299
|
+
* visible assistant text) as a failure even when the loop's terminal exit
|
|
300
|
+
* was a clean model-driven stop, e.g. an HTTP-200 reply with an empty
|
|
301
|
+
* content array or thinking-only output. Default false: generic
|
|
302
|
+
* opportunity wakes (meet chat, scheduled nudges) legitimately let the
|
|
303
|
+
* model decide to stay silent, and their silent no-op stays
|
|
304
|
+
* `invoked: true`.
|
|
305
|
+
*
|
|
306
|
+
* Set by state-advancing callers whose trigger is consumed on success and
|
|
307
|
+
* whose contract requires committed output: the fork-based memory
|
|
308
|
+
* retrospective advances its processed-message watermark and GCs the
|
|
309
|
+
* prior retrospective on `invoked: true`, so for it an empty reply is a
|
|
310
|
+
* failed pass to retry, never a success (LUM-3013). Runs whose output
|
|
311
|
+
* already went live (checkpoint fired / tail persisted) are unaffected.
|
|
312
|
+
*/
|
|
313
|
+
requireUsableOutput?: boolean;
|
|
266
314
|
/**
|
|
267
315
|
* Optional exact tool allowlist for this wake. Used by internal maintenance
|
|
268
316
|
* jobs that need the assistant's judgment but must not execute arbitrary
|
|
@@ -383,16 +431,27 @@ export type WakeSkipReason =
|
|
|
383
431
|
*/
|
|
384
432
|
| "context_overflow"
|
|
385
433
|
/**
|
|
386
|
-
* The agent loop
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
434
|
+
* The agent loop terminated abnormally before producing ANY output: either
|
|
435
|
+
* it threw before a checkpoint fired or a tail message was persisted, or it
|
|
436
|
+
* swallowed a terminal failure (provider rejection, unhandled throw,
|
|
437
|
+
* cancellation) into a graceful no-output stop whose `agent_loop_exit`
|
|
438
|
+
* reason is not a clean model-driven one (typically a provider failure on
|
|
439
|
+
* the run's first LLM call, e.g. rejected input). The wake did no work, so
|
|
440
|
+
* callers that treat `invoked: true` as "the pass ran" (e.g. the memory
|
|
390
441
|
* retrospective, which advances its processed-message watermark and
|
|
391
442
|
* finalizes on success) must see a retryable failure rather than a
|
|
392
|
-
* silent no-op. A throw AFTER output went live keeps `invoked: true
|
|
443
|
+
* silent no-op. A throw AFTER output went live keeps `invoked: true`:
|
|
393
444
|
* side effects have already landed and the run must not read as skipped.
|
|
394
445
|
*/
|
|
395
|
-
| "run_error"
|
|
446
|
+
| "run_error"
|
|
447
|
+
/**
|
|
448
|
+
* The run ended cleanly but committed no usable output (no executable tool
|
|
449
|
+
* call, no visible assistant text: e.g. an HTTP-200 reply with an empty
|
|
450
|
+
* content array, or thinking-only output) and the caller passed
|
|
451
|
+
* `requireUsableOutput: true`. Only possible on such wakes; without the
|
|
452
|
+
* flag the same run is a silent no-op with `invoked: true`.
|
|
453
|
+
*/
|
|
454
|
+
| "no_output";
|
|
396
455
|
|
|
397
456
|
export interface WakeResult {
|
|
398
457
|
invoked: boolean;
|
|
@@ -942,6 +1001,14 @@ export async function wakeAgentForOpportunity(
|
|
|
942
1001
|
// job. Capture the signal here so the wake can fail deterministically
|
|
943
1002
|
// instead (`reason: "context_overflow"`).
|
|
944
1003
|
let suppressedContextOverflow = false;
|
|
1004
|
+
// Terminal exit reason captured from the loop's `agent_loop_exit` event.
|
|
1005
|
+
// `agentLoop.run()`'s returned `exitReason` only distinguishes checkpoint
|
|
1006
|
+
// handoffs, so this event is the wake's only view of HOW the loop ended.
|
|
1007
|
+
// Read by the no-output branch below to tell a genuine silent no-op
|
|
1008
|
+
// (model produced nothing) from a swallowed terminal failure (the loop's
|
|
1009
|
+
// catch turns provider rejections and unhandled throws into a graceful
|
|
1010
|
+
// no-output stop instead of rethrowing).
|
|
1011
|
+
let terminalExitReason: AgentLoopExitReason | null = null;
|
|
945
1012
|
const persistLog = (record: PendingLog): void => {
|
|
946
1013
|
try {
|
|
947
1014
|
recordRequestLog(
|
|
@@ -1072,6 +1139,7 @@ export async function wakeAgentForOpportunity(
|
|
|
1072
1139
|
// reason is stashed and applied in `goLive` after pendingLogs are
|
|
1073
1140
|
// persisted, preserving the same ordering guarantee.
|
|
1074
1141
|
if (event.type === "agent_loop_exit") {
|
|
1142
|
+
terminalExitReason = event.reason;
|
|
1075
1143
|
if (mode === "buffering") {
|
|
1076
1144
|
pendingExitReason = event.reason;
|
|
1077
1145
|
} else {
|
|
@@ -1264,6 +1332,13 @@ export async function wakeAgentForOpportunity(
|
|
|
1264
1332
|
// finally's error log names the reported outcome so the line matches
|
|
1265
1333
|
// what the caller actually saw.
|
|
1266
1334
|
let reportedRunErrorAsFailure = false;
|
|
1335
|
+
// Set when a no-output run was reported as `invoked: false`, either
|
|
1336
|
+
// because it ended on a non-clean terminal exit reason ("run_error") or
|
|
1337
|
+
// because the caller requires usable output ("no_output"). The failure
|
|
1338
|
+
// site logs its own dedicated warn line; the finally skips its generic
|
|
1339
|
+
// "silent no-op" line for these paths so a failed wake never reads as a
|
|
1340
|
+
// successful empty one.
|
|
1341
|
+
let reportedSwallowedStopAsFailure = false;
|
|
1267
1342
|
// Shared failure path for an over-window condition on a
|
|
1268
1343
|
// compaction-suppressed wake (reached from the pre-flight estimate, from
|
|
1269
1344
|
// the run's catch when the rejection escaped as a throw, or post-run when
|
|
@@ -1464,6 +1539,54 @@ export async function wakeAgentForOpportunity(
|
|
|
1464
1539
|
const producedOutput = producedToolCalls || hasVisibleText;
|
|
1465
1540
|
|
|
1466
1541
|
if (!producedOutput || tailMessages.length === 0) {
|
|
1542
|
+
// No output. Whether that reads as success depends on WHY the loop
|
|
1543
|
+
// stopped: the loop's catch swallows provider rejections, unhandled
|
|
1544
|
+
// throws, and cancellation into a graceful no-output stop instead of
|
|
1545
|
+
// rethrowing, and reporting that as a successful silent no-op lets
|
|
1546
|
+
// state-advancing callers (the memory retrospective watermark, the
|
|
1547
|
+
// scheduler's success feed) permanently consume their trigger on a
|
|
1548
|
+
// pass that never ran. Gate on the terminal exit reason: a clean
|
|
1549
|
+
// model-driven stop is a real silent no-op; anything else fails the
|
|
1550
|
+
// wake so the caller retries.
|
|
1551
|
+
// The went-live guard mirrors the throw path above: once a
|
|
1552
|
+
// checkpoint fired or a tail message was persisted, side effects
|
|
1553
|
+
// have already landed and the run must keep `invoked: true` even if
|
|
1554
|
+
// the returned history's tail slice reads empty (the loop's
|
|
1555
|
+
// deep-repair and recovery-hook paths rebase `history`, which can
|
|
1556
|
+
// misalign the wake's external tail indexes).
|
|
1557
|
+
const nothingWentLive =
|
|
1558
|
+
mode === "buffering" && persistedTailIndex === 0;
|
|
1559
|
+
if (
|
|
1560
|
+
nothingWentLive &&
|
|
1561
|
+
terminalExitReason !== null &&
|
|
1562
|
+
!CLEAN_NO_OUTPUT_EXIT_REASONS.has(terminalExitReason)
|
|
1563
|
+
) {
|
|
1564
|
+
reportedSwallowedStopAsFailure = true;
|
|
1565
|
+
log.warn(
|
|
1566
|
+
{ conversationId, source, exitReason: terminalExitReason },
|
|
1567
|
+
"agent-wake: agent loop ended with no output on a non-clean exit; reported as run_error",
|
|
1568
|
+
);
|
|
1569
|
+
return {
|
|
1570
|
+
invoked: false,
|
|
1571
|
+
producedToolCalls: false,
|
|
1572
|
+
reason: "run_error" as const,
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
// Clean stop with no usable output. For callers that consume a
|
|
1576
|
+
// trigger on success (requireUsableOutput), an empty reply is a
|
|
1577
|
+
// failed pass to retry, not a success to record.
|
|
1578
|
+
if (nothingWentLive && opts.requireUsableOutput === true) {
|
|
1579
|
+
reportedSwallowedStopAsFailure = true;
|
|
1580
|
+
log.warn(
|
|
1581
|
+
{ conversationId, source, exitReason: terminalExitReason },
|
|
1582
|
+
"agent-wake: agent loop committed no usable output and the caller requires it; reported as no_output",
|
|
1583
|
+
);
|
|
1584
|
+
return {
|
|
1585
|
+
invoked: false,
|
|
1586
|
+
producedToolCalls: false,
|
|
1587
|
+
reason: "no_output" as const,
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1467
1590
|
// Silent no-op: drop buffered events, push nothing, persist
|
|
1468
1591
|
// nothing, emit nothing. (No checkpoint fired during the run
|
|
1469
1592
|
// since checkpoints only fire after tool turns and there were
|
|
@@ -1540,7 +1663,7 @@ export async function wakeAgentForOpportunity(
|
|
|
1540
1663
|
|
|
1541
1664
|
const durationMs = nowFn() - startedAt;
|
|
1542
1665
|
const suppressWakeSurface = opts.suppressWakeSurface === true;
|
|
1543
|
-
if (failedContextOverflow) {
|
|
1666
|
+
if (failedContextOverflow || reportedSwallowedStopAsFailure) {
|
|
1544
1667
|
// Already logged its own dedicated warn line at the failure site;
|
|
1545
1668
|
// a generic "silent no-op" line here would misclassify a failed
|
|
1546
1669
|
// wake as a successful empty one.
|
|
@@ -102,6 +102,31 @@ export function isParkAction(action: string | undefined): boolean {
|
|
|
102
102
|
return action !== undefined && PARK_ACTION_SET.has(action);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/** Outcome word per terminal guardian-request status on a resolved card. */
|
|
106
|
+
const DECISION_STATUS_WORDS: Record<string, string> = {
|
|
107
|
+
approved: "Approved",
|
|
108
|
+
denied: "Denied",
|
|
109
|
+
expired: "Expired",
|
|
110
|
+
cancelled: "Cancelled",
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The outcome word shown on a resolved guardian-request card, shared by every
|
|
115
|
+
* surface (in-app, Slack, Telegram); surfaces add only their own glyph
|
|
116
|
+
* vocabulary around it. A `denied` status reached by a park action reads as
|
|
117
|
+
* the neutral {@link PARK_STATUS_LABEL} rather than "Denied": a parked
|
|
118
|
+
* contact was neither trusted nor kept out.
|
|
119
|
+
*/
|
|
120
|
+
export function resolveDecisionStatusWord(
|
|
121
|
+
status: string,
|
|
122
|
+
decidedAction?: string,
|
|
123
|
+
): string {
|
|
124
|
+
if (status === "denied" && isParkAction(decidedAction)) {
|
|
125
|
+
return PARK_STATUS_LABEL;
|
|
126
|
+
}
|
|
127
|
+
return DECISION_STATUS_WORDS[status] ?? "Resolved";
|
|
128
|
+
}
|
|
129
|
+
|
|
105
130
|
/**
|
|
106
131
|
* Map `GuardianDecisionAction[]` to `ApprovalActionOption[]` so channel
|
|
107
132
|
* prompt payloads can be derived from the unified decision action set.
|
|
@@ -99,8 +99,8 @@ describe("CLI vs HTTP route parity", () => {
|
|
|
99
99
|
|
|
100
100
|
test("platform connection: CLI createConnection and HTTP POST produce identical DB rows", async () => {
|
|
101
101
|
const payload = {
|
|
102
|
-
name: "parity-
|
|
103
|
-
provider: "
|
|
102
|
+
name: "parity-vellum-managed",
|
|
103
|
+
provider: "vellum" as const,
|
|
104
104
|
auth: { type: "platform" as const },
|
|
105
105
|
};
|
|
106
106
|
|
|
@@ -1681,6 +1681,17 @@ describe("config invariant flag enrichment", () => {
|
|
|
1681
1681
|
expect(profiles.custom!).not.toHaveProperty("invariant");
|
|
1682
1682
|
});
|
|
1683
1683
|
|
|
1684
|
+
test("every managed default is invariant on the wire, Speed included", async () => {
|
|
1685
|
+
// The clients drive their read-only lock off this flag, so a default that
|
|
1686
|
+
// resolves from the catalog with no workspace stub must still carry it.
|
|
1687
|
+
const body = await configGetRoute.handler({});
|
|
1688
|
+
const profiles = wireProfiles(body);
|
|
1689
|
+
|
|
1690
|
+
for (const name of ["balanced", "quality-optimized", "latency-optimized"]) {
|
|
1691
|
+
expect(profiles[name]!.invariant).toBe(true);
|
|
1692
|
+
}
|
|
1693
|
+
});
|
|
1694
|
+
|
|
1684
1695
|
test("PATCH /v1/config stamps the flag on the response but never persists it", async () => {
|
|
1685
1696
|
const body = await configPatchRoute.handler({
|
|
1686
1697
|
body: { memory: { enabled: true } },
|