@vellumai/assistant 0.8.2 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +11 -12
- package/docker-entrypoint.sh +13 -1
- package/docker-init-apt-root.sh +79 -6
- package/openapi.yaml +336 -21
- package/package.json +1 -1
- package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
- package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
- package/src/__tests__/config-get-vision-flag.test.ts +136 -0
- package/src/__tests__/config-loader-backfill.test.ts +115 -18
- package/src/__tests__/context-token-estimator.test.ts +30 -65
- package/src/__tests__/conversation-agent-loop.test.ts +57 -1
- package/src/__tests__/conversation-media-retry.test.ts +19 -8
- package/src/__tests__/conversation-runtime-assembly.test.ts +26 -4
- package/src/__tests__/date-context.test.ts +45 -0
- package/src/__tests__/external-plugin-loader.test.ts +91 -19
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
- package/src/__tests__/guardian-dispatch.test.ts +1 -0
- package/src/__tests__/heartbeat-service.test.ts +24 -164
- package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
- package/src/__tests__/host-app-control-proxy.test.ts +241 -0
- package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
- package/src/__tests__/injector-background-turn.test.ts +153 -0
- package/src/__tests__/injector-chain.test.ts +5 -0
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
- package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
- package/src/__tests__/llm-catalog-parity.test.ts +3 -0
- package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
- package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
- package/src/__tests__/llm-resolver.test.ts +255 -2
- package/src/__tests__/managed-profile-guard.test.ts +10 -0
- package/src/__tests__/notification-decision-fallback.test.ts +0 -91
- package/src/__tests__/notification-decision-strategy.test.ts +14 -31
- package/src/__tests__/notification-deep-link.test.ts +15 -0
- package/src/__tests__/notification-guardian-path.test.ts +1 -2
- package/src/__tests__/notification-platform-adapter.test.ts +5 -4
- package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
- package/src/__tests__/openai-provider.test.ts +218 -3
- package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
- package/src/__tests__/openrouter-provider-only.test.ts +51 -3
- package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
- package/src/__tests__/platform-proxy-context.test.ts +6 -1
- package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
- package/src/__tests__/plugin-types.test.ts +2 -2
- package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
- package/src/__tests__/system-prompt.test.ts +6 -73
- package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
- package/src/a2a/__tests__/agent-card.test.ts +98 -0
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
- package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
- package/src/a2a/__tests__/task-store.test.ts +246 -0
- package/src/a2a/agent-card.ts +58 -0
- package/src/a2a/feature-gate.ts +8 -0
- package/src/a2a/protocol-constants.ts +21 -0
- package/src/a2a/protocol-errors.ts +50 -0
- package/src/a2a/protocol-types.ts +162 -0
- package/src/a2a/task-store.ts +168 -0
- package/src/agent/loop.ts +167 -18
- package/src/channels/config.ts +9 -0
- package/src/channels/types.ts +14 -0
- package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
- package/src/cli/commands/__tests__/schedules.test.ts +469 -0
- package/src/cli/commands/notifications.ts +65 -35
- package/src/cli/commands/plugins.ts +67 -0
- package/src/cli/commands/schedules.ts +297 -5
- package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
- package/src/cli/lib/install-from-github.ts +8 -9
- package/src/cli/lib/search-plugins.ts +163 -0
- package/src/cli/program.ts +14 -0
- package/src/config/assistant-feature-flags.ts +24 -54
- package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
- package/src/config/call-site-defaults.ts +105 -0
- package/src/config/feature-flag-registry.json +21 -29
- package/src/config/llm-resolver.ts +52 -1
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
- package/src/config/schemas/channels.ts +9 -0
- package/src/config/schemas/conversations.ts +10 -0
- package/src/config/schemas/heartbeat.ts +14 -0
- package/src/config/schemas/llm.ts +1 -3
- package/src/config/schemas/memory-retrospective.ts +1 -1
- package/src/config/schemas/memory-v2.ts +4 -4
- package/src/config/schemas/memory.ts +3 -1
- package/src/config/seed-inference-profiles.ts +99 -29
- package/src/context/compactor.ts +72 -12
- package/src/context/token-estimator.ts +32 -34
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
- package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
- package/src/daemon/conversation-agent-loop.ts +29 -2
- package/src/daemon/conversation-runtime-assembly.ts +9 -0
- package/src/daemon/conversation.ts +0 -7
- package/src/daemon/date-context.ts +40 -0
- package/src/daemon/guardian-action-generators.ts +1 -125
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
- package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
- package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
- package/src/daemon/handlers/config-a2a.ts +289 -0
- package/src/daemon/handlers/conversations.ts +1 -0
- package/src/daemon/host-app-control-proxy.ts +69 -18
- package/src/daemon/host-proxy-preactivation.ts +85 -18
- package/src/daemon/lifecycle.ts +49 -61
- package/src/daemon/memory-v2-startup.ts +49 -13
- package/src/daemon/message-types/notifications.ts +21 -0
- package/src/daemon/pkb-reminder-builder.test.ts +10 -53
- package/src/daemon/pkb-reminder-builder.ts +4 -19
- package/src/daemon/process-message.ts +3 -0
- package/src/daemon/skill-memory-refresh.ts +5 -1
- package/src/daemon/wake-target-adapter.ts +2 -0
- package/src/export/__tests__/transcript-formatter.test.ts +121 -0
- package/src/export/transcript-formatter.ts +54 -20
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -0
- package/src/heartbeat/heartbeat-service.ts +34 -191
- package/src/home/__tests__/feed-types.test.ts +40 -0
- package/src/home/feed-types.ts +14 -2
- package/src/ipc/cli-client.ts +147 -45
- package/src/memory/__tests__/conversation-queries.test.ts +220 -0
- package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
- package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
- package/src/memory/conversation-queries.ts +87 -1
- package/src/memory/conversation-title-service.ts +26 -4
- package/src/memory/db-init.ts +6 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
- package/src/memory/graph/conversation-graph-memory.ts +18 -6
- package/src/memory/graph/tools.ts +6 -37
- package/src/memory/invite-store.ts +53 -0
- package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
- package/src/memory/llm-request-log-store.ts +92 -1
- package/src/memory/memory-retrospective-enqueue.ts +1 -20
- package/src/memory/memory-retrospective-job.ts +33 -6
- package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
- package/src/memory/migrations/251-a2a-tasks.ts +49 -0
- package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/a2a.ts +15 -0
- package/src/memory/schema/index.ts +1 -0
- package/src/memory/schema/inference.ts +2 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
- package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
- package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
- package/src/memory/v2/__tests__/injection.test.ts +190 -3
- package/src/memory/v2/__tests__/static-context.test.ts +12 -1
- package/src/memory/v2/activation-store.ts +14 -16
- package/src/memory/v2/cli-command-content.ts +19 -0
- package/src/memory/v2/cli-command-store.ts +304 -0
- package/src/memory/v2/frontmatter-sweep.ts +7 -1
- package/src/memory/v2/injection.ts +49 -20
- package/src/memory/v2/page-index.ts +38 -13
- package/src/memory/v2/static-context.ts +4 -4
- package/src/memory/v2/types.ts +23 -0
- package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
- package/src/messaging/providers/a2a/deliver.ts +156 -0
- package/src/messaging/providers/gmail/client.ts +9 -2
- package/src/messaging/providers/index.ts +11 -2
- package/src/notifications/__tests__/broadcaster.test.ts +203 -0
- package/src/notifications/__tests__/decision-engine.test.ts +283 -0
- package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
- package/src/notifications/adapters/macos.ts +12 -2
- package/src/notifications/broadcaster.ts +29 -4
- package/src/notifications/copy-composer.ts +17 -64
- package/src/notifications/decision-engine.ts +111 -44
- package/src/notifications/deterministic-checks.ts +96 -0
- package/src/notifications/emit-signal.ts +1 -0
- package/src/notifications/home-feed-side-effect.ts +85 -6
- package/src/notifications/signal.ts +0 -4
- package/src/notifications/types.ts +8 -0
- package/src/oauth/platform-connection.test.ts +43 -3
- package/src/oauth/platform-connection.ts +13 -4
- package/src/plugins/defaults/injectors.ts +38 -19
- package/src/plugins/external-plugin-loader.ts +82 -10
- package/src/plugins/types.ts +16 -7
- package/src/prompts/__tests__/system-prompt.test.ts +6 -51
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
- package/src/prompts/system-prompt.ts +0 -8
- package/src/prompts/templates/BOOTSTRAP.md +5 -5
- package/src/prompts/templates/system-sections.ts +0 -9
- package/src/providers/__tests__/inference.test.ts +2 -0
- package/src/providers/call-site-routing.ts +24 -6
- package/src/providers/connection-resolution.ts +63 -13
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
- package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
- package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
- package/src/providers/inference/adapter-factory.ts +9 -20
- package/src/providers/inference/auth.ts +12 -0
- package/src/providers/inference/backfill.ts +14 -1
- package/src/providers/inference/connections.ts +85 -5
- package/src/providers/inference/resolve-auth.ts +2 -0
- package/src/providers/model-catalog.ts +199 -244
- package/src/providers/model-intents.ts +3 -3
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
- package/src/providers/openai/chat-completions-provider.ts +159 -6
- package/src/providers/openrouter/client.ts +42 -4
- package/src/providers/platform-proxy/constants.ts +3 -4
- package/src/providers/provider-catalog-visibility.ts +3 -1
- package/src/providers/provider-send-message.ts +27 -12
- package/src/providers/registry.ts +30 -1
- package/src/runtime/agent-wake.ts +61 -1
- package/src/runtime/auth/route-policy.ts +13 -0
- package/src/runtime/http-server.ts +7 -16
- package/src/runtime/http-types.ts +0 -47
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +66 -4
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
- package/src/runtime/routes/channel-availability-routes.ts +5 -0
- package/src/runtime/routes/consolidation-routes.ts +100 -0
- package/src/runtime/routes/conversation-query-routes.ts +70 -11
- package/src/runtime/routes/conversation-routes.ts +7 -0
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +134 -1
- package/src/runtime/routes/integrations/a2a.ts +235 -0
- package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
- package/src/runtime/routes/subagents-routes.ts +41 -0
- package/src/subagent/manager.ts +2 -0
- package/src/tools/memory/register.ts +1 -9
- package/src/tools/registry.ts +2 -2
- package/src/tools/types.ts +37 -2
- package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
- package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
- package/src/runtime/guardian-action-conversation-turn.ts +0 -99
|
@@ -38,6 +38,10 @@ import {
|
|
|
38
38
|
spreadActivation,
|
|
39
39
|
} from "./activation.js";
|
|
40
40
|
import { hydrate, save } from "./activation-store.js";
|
|
41
|
+
import {
|
|
42
|
+
getCliCommandCapability,
|
|
43
|
+
isCliCommandSlug,
|
|
44
|
+
} from "./cli-command-store.js";
|
|
41
45
|
import { getEdgeIndex } from "./edge-index.js";
|
|
42
46
|
import { readPage, renderPageContent } from "./page-store.js";
|
|
43
47
|
import { runRouter } from "./router.js";
|
|
@@ -355,20 +359,22 @@ async function finalizeInjection(args: {
|
|
|
355
359
|
// on that user message and the agent keeps seeing it across subsequent turns
|
|
356
360
|
// until compaction evicts the turn.
|
|
357
361
|
//
|
|
358
|
-
//
|
|
359
|
-
// between the
|
|
360
|
-
// at an uninstalled skill
|
|
361
|
-
// per-turn runs re-attempt attachment
|
|
362
|
-
// this, the slug would be marked
|
|
363
|
-
// silently dropped it.
|
|
364
|
-
const
|
|
362
|
+
// Synthetic slugs (skills, CLI commands) whose in-process cache entry is
|
|
363
|
+
// missing (e.g. startup race between the seed and the first turn, or stale
|
|
364
|
+
// Qdrant index pointing at an uninstalled skill / removed CLI command) are
|
|
365
|
+
// excluded from `everInjected` so future per-turn runs re-attempt attachment
|
|
366
|
+
// once the cache is populated. Without this, the slug would be marked
|
|
367
|
+
// injected even though `renderInjectionBlock` silently dropped it.
|
|
368
|
+
const missingSyntheticSlugs = new Set(
|
|
365
369
|
slugsToRender.filter(
|
|
366
|
-
(slug) =>
|
|
370
|
+
(slug) =>
|
|
371
|
+
(isSkillSlug(slug) && !getSkillCapability(slug)) ||
|
|
372
|
+
(isCliCommandSlug(slug) && !getCliCommandCapability(slug)),
|
|
367
373
|
),
|
|
368
374
|
);
|
|
369
375
|
const everInjectedSet = new Set(priorEverInjected.map((entry) => entry.slug));
|
|
370
376
|
const newlyInjected = slugsToRender.filter(
|
|
371
|
-
(slug) => !everInjectedSet.has(slug) && !
|
|
377
|
+
(slug) => !everInjectedSet.has(slug) && !missingSyntheticSlugs.has(slug),
|
|
372
378
|
);
|
|
373
379
|
const nextEverInjected: EverInjectedEntry[] = [
|
|
374
380
|
...priorEverInjected,
|
|
@@ -728,18 +734,18 @@ const INJECTION_HEADER =
|
|
|
728
734
|
* distinguish "file vanished" (stale index) from "file is malformed"
|
|
729
735
|
* (data-corruption / programmer error).
|
|
730
736
|
*
|
|
731
|
-
* Skill slugs whose entry the cache no longer
|
|
732
|
-
* mid-run
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
* bug.
|
|
737
|
+
* Skill and CLI-command slugs whose entry the in-process cache no longer
|
|
738
|
+
* knows (e.g. uninstalled mid-run, or a CLI command removed between seeds)
|
|
739
|
+
* are silently dropped, mirroring the missing-pages behavior but without
|
|
740
|
+
* entering `missingSlugs` — the synthetic catalogs are the source of truth
|
|
741
|
+
* for those entries, not on-disk concept pages.
|
|
737
742
|
*
|
|
738
743
|
* Each concept-page section is rendered as a path header followed by either
|
|
739
744
|
* the page's `summary` (when present in frontmatter) or the full page (the
|
|
740
|
-
* fallback for pages predating the summary field). Skills sit
|
|
741
|
-
* under `### Skills You Can Use`,
|
|
742
|
-
*
|
|
745
|
+
* fallback for pages predating the summary field). Skills sit after the
|
|
746
|
+
* concept sections under `### Skills You Can Use`, and CLI subcommands sit
|
|
747
|
+
* after the skills under `### CLI Commands You Can Use`. The leading
|
|
748
|
+
* `**CRITICAL:**` line tells the agent how to read the block.
|
|
743
749
|
*
|
|
744
750
|
* **CRITICAL:** These are page summaries. Read the page file if it looks relevant.
|
|
745
751
|
*
|
|
@@ -758,13 +764,24 @@ const INJECTION_HEADER =
|
|
|
758
764
|
* ### Skills You Can Use
|
|
759
765
|
* - <skill-1 content>
|
|
760
766
|
* - <skill-2 content>
|
|
767
|
+
*
|
|
768
|
+
* ### CLI Commands You Can Use
|
|
769
|
+
* Run `assistant <command> --help` for full usage.
|
|
770
|
+
* - `assistant <name-1>`: <description-1>
|
|
771
|
+
* - `assistant <name-2>`: <description-2>
|
|
761
772
|
*/
|
|
762
773
|
async function renderInjectionBlock(
|
|
763
774
|
workspaceDir: string,
|
|
764
775
|
slugs: string[],
|
|
765
776
|
): Promise<RenderInjectionBlockResult> {
|
|
766
|
-
const conceptSlugs =
|
|
767
|
-
const skillSlugs =
|
|
777
|
+
const conceptSlugs: string[] = [];
|
|
778
|
+
const skillSlugs: string[] = [];
|
|
779
|
+
const cliCommandSlugs: string[] = [];
|
|
780
|
+
for (const slug of slugs) {
|
|
781
|
+
if (isSkillSlug(slug)) skillSlugs.push(slug);
|
|
782
|
+
else if (isCliCommandSlug(slug)) cliCommandSlugs.push(slug);
|
|
783
|
+
else conceptSlugs.push(slug);
|
|
784
|
+
}
|
|
768
785
|
|
|
769
786
|
const settled = await Promise.allSettled(
|
|
770
787
|
conceptSlugs.map((slug) => readPage(workspaceDir, slug)),
|
|
@@ -815,6 +832,18 @@ async function renderInjectionBlock(
|
|
|
815
832
|
sections.push(`### Skills You Can Use\n${skillLines.join("\n")}`);
|
|
816
833
|
}
|
|
817
834
|
|
|
835
|
+
const cliCommandLines: string[] = [];
|
|
836
|
+
for (const slug of cliCommandSlugs) {
|
|
837
|
+
const entry = getCliCommandCapability(slug);
|
|
838
|
+
if (!entry) continue;
|
|
839
|
+
cliCommandLines.push(`- \`assistant ${entry.id}\`: ${entry.description}`);
|
|
840
|
+
}
|
|
841
|
+
if (cliCommandLines.length > 0) {
|
|
842
|
+
sections.push(
|
|
843
|
+
`### CLI Commands You Can Use\nRun \`assistant <command> --help\` for full usage.\n${cliCommandLines.join("\n")}`,
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
|
|
818
847
|
if (sections.length === 0) {
|
|
819
848
|
return { block: null, missingSlugs, corruptSlugs };
|
|
820
849
|
}
|
|
@@ -80,11 +80,12 @@ let cache: CachedIndex | null = null;
|
|
|
80
80
|
/**
|
|
81
81
|
* Return a `PageIndex` for `workspaceDir`. Cached module-locally; the cache
|
|
82
82
|
* is invalidated by `invalidatePageIndex` (called by daemon-side hooks when
|
|
83
|
-
* concept pages or
|
|
83
|
+
* concept pages, skill entries, or CLI-command entries change).
|
|
84
84
|
*
|
|
85
85
|
* Cold builds list every concept page in parallel, drop pages whose read
|
|
86
|
-
* rejects, append seeded skill entries from `listSkillEntries()
|
|
87
|
-
*
|
|
86
|
+
* rejects, append seeded skill entries from `listSkillEntries()` and CLI
|
|
87
|
+
* command entries from `listCliCommandEntries()`, sort by slug for
|
|
88
|
+
* deterministic IDs, then resolve outgoing edges to numeric IDs.
|
|
88
89
|
*/
|
|
89
90
|
export async function getPageIndex(workspaceDir: string): Promise<PageIndex> {
|
|
90
91
|
if (cache && cache.workspaceDir === workspaceDir) {
|
|
@@ -107,20 +108,29 @@ export async function getPageIndex(workspaceDir: string): Promise<PageIndex> {
|
|
|
107
108
|
outgoingSlugs: string[];
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
//
|
|
119
|
-
//
|
|
111
|
+
const [
|
|
112
|
+
{ listSkillEntries, SKILL_SLUG_PREFIX },
|
|
113
|
+
{ listCliCommandEntries, CLI_COMMAND_SLUG_PREFIX },
|
|
114
|
+
] = await Promise.all([
|
|
115
|
+
import("./skill-store.js"),
|
|
116
|
+
import("./cli-command-store.js"),
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
// Build the synthetic-slug sets first so we can drop colliding concept
|
|
120
|
+
// pages. Collision policy: **synthetic entries win**. Skill and CLI rows
|
|
121
|
+
// are seeded from authoritative in-process catalogs; a hand-authored page
|
|
122
|
+
// sitting under `skills/<id>` or `cli-commands/<name>` is either a stale
|
|
123
|
+
// leftover from a prior write or a user mistake. `bySlug` is last-writer-
|
|
124
|
+
// wins, so without explicit dedupe one side would silently shadow the
|
|
125
|
+
// other depending on iteration order.
|
|
120
126
|
const skillEntries = listSkillEntries();
|
|
121
127
|
const skillSlugs = new Set(
|
|
122
128
|
skillEntries.map((entry) => `${SKILL_SLUG_PREFIX}${entry.id}`),
|
|
123
129
|
);
|
|
130
|
+
const cliCommandEntries = listCliCommandEntries();
|
|
131
|
+
const cliCommandSlugs = new Set(
|
|
132
|
+
cliCommandEntries.map((entry) => `${CLI_COMMAND_SLUG_PREFIX}${entry.id}`),
|
|
133
|
+
);
|
|
124
134
|
|
|
125
135
|
const drafts: DraftEntry[] = [];
|
|
126
136
|
for (let i = 0; i < settled.length; i++) {
|
|
@@ -142,6 +152,13 @@ export async function getPageIndex(workspaceDir: string): Promise<PageIndex> {
|
|
|
142
152
|
);
|
|
143
153
|
continue;
|
|
144
154
|
}
|
|
155
|
+
if (cliCommandSlugs.has(slug)) {
|
|
156
|
+
log.warn(
|
|
157
|
+
{ slug },
|
|
158
|
+
"Dropping concept page from index — slug collides with a seeded CLI-command entry; CLI command wins",
|
|
159
|
+
);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
145
162
|
const summarySource = page.frontmatter.summary?.trim() || page.body.trim();
|
|
146
163
|
drafts.push({
|
|
147
164
|
slug,
|
|
@@ -158,6 +175,14 @@ export async function getPageIndex(workspaceDir: string): Promise<PageIndex> {
|
|
|
158
175
|
});
|
|
159
176
|
}
|
|
160
177
|
|
|
178
|
+
for (const entry of cliCommandEntries) {
|
|
179
|
+
drafts.push({
|
|
180
|
+
slug: `${CLI_COMMAND_SLUG_PREFIX}${entry.id}`,
|
|
181
|
+
summary: normalizeSummary(entry.description),
|
|
182
|
+
outgoingSlugs: [],
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
161
186
|
drafts.sort((a, b) => (a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0));
|
|
162
187
|
|
|
163
188
|
// Assign 1-based dense IDs in sort order so entries[i].id === i + 1.
|
|
@@ -35,9 +35,9 @@ const MEMORY_V2_STATIC_BLOCKS: readonly MemoryV2StaticBlock[] = [
|
|
|
35
35
|
];
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* Build the v2 static memory block, gated on `config.memory.
|
|
39
|
-
* Empty/missing files are skipped; returns `null`
|
|
40
|
-
* every file is empty.
|
|
38
|
+
* Build the v2 static memory block, gated on `config.memory.enabled` and
|
|
39
|
+
* `config.memory.v2.enabled`. Empty/missing files are skipped; returns `null`
|
|
40
|
+
* when either gate is off or every file is empty.
|
|
41
41
|
*/
|
|
42
42
|
export function readMemoryV2StaticContent(): string | null {
|
|
43
43
|
let config;
|
|
@@ -46,7 +46,7 @@ export function readMemoryV2StaticContent(): string | null {
|
|
|
46
46
|
} catch {
|
|
47
47
|
return null;
|
|
48
48
|
}
|
|
49
|
-
if (!config.memory.v2.enabled) {
|
|
49
|
+
if (!config.memory.enabled || !config.memory.v2.enabled) {
|
|
50
50
|
return null;
|
|
51
51
|
}
|
|
52
52
|
|
package/src/memory/v2/types.ts
CHANGED
|
@@ -115,3 +115,26 @@ export interface SkillEntry {
|
|
|
115
115
|
id: string;
|
|
116
116
|
content: string;
|
|
117
117
|
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// CLI-command entries (synthetic concept-collection rows, not on-disk pages)
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Per-CLI-subcommand capability snapshot held in-process and embedded into the
|
|
125
|
+
* unified `memory_v2_concept_pages` Qdrant collection under the slug
|
|
126
|
+
* `cli-commands/<name>`. `content` is the full `helpInformation()` output for
|
|
127
|
+
* the top-level subcommand — the embedding target, intentionally uncapped so
|
|
128
|
+
* activation hints in flag descriptions and examples carry semantic weight.
|
|
129
|
+
* `description` is the one-line Commander description, rendered terse in
|
|
130
|
+
* `### CLI Commands You Can Use` so the injection block stays compact even
|
|
131
|
+
* for verbose `--help` outputs.
|
|
132
|
+
*
|
|
133
|
+
* Plain interface (no Zod) — same in-process-only justification as
|
|
134
|
+
* `SkillEntry`.
|
|
135
|
+
*/
|
|
136
|
+
export interface CliCommandEntry {
|
|
137
|
+
id: string;
|
|
138
|
+
description: string;
|
|
139
|
+
content: string;
|
|
140
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { ChannelReplyPayload } from "@vellumai/gateway-client";
|
|
4
|
+
|
|
5
|
+
import type { A2ATask, Artifact } from "../../../../a2a/protocol-types.js";
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Mock state
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
let completedTask: A2ATask | null = null;
|
|
12
|
+
let completeWithArtifactsCalls: Array<{
|
|
13
|
+
taskId: string;
|
|
14
|
+
artifacts: Artifact[];
|
|
15
|
+
}> = [];
|
|
16
|
+
let pushUrlByTaskId: Record<string, string | null> = {};
|
|
17
|
+
let completeError: Error | null = null;
|
|
18
|
+
|
|
19
|
+
const fetchCalls: Array<{
|
|
20
|
+
url: string;
|
|
21
|
+
init: RequestInit;
|
|
22
|
+
}> = [];
|
|
23
|
+
let fetchResponses: Array<{ ok: boolean; status: number; body: string }> = [];
|
|
24
|
+
let fetchCallIndex = 0;
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Mocks
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
const defaultTask: A2ATask = {
|
|
31
|
+
id: "task-123",
|
|
32
|
+
status: { state: "completed", timestamp: new Date().toISOString() },
|
|
33
|
+
artifacts: [
|
|
34
|
+
{
|
|
35
|
+
artifact_id: "art-1",
|
|
36
|
+
parts: [{ kind: "text", text: "Hello from assistant" }],
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
mock.module("../../../../a2a/task-store.js", () => ({
|
|
42
|
+
completeWithArtifacts: (taskId: string, artifacts: Artifact[]): A2ATask => {
|
|
43
|
+
completeWithArtifactsCalls.push({ taskId, artifacts });
|
|
44
|
+
if (completeError) throw completeError;
|
|
45
|
+
return completedTask ?? defaultTask;
|
|
46
|
+
},
|
|
47
|
+
getPushUrl: (taskId: string): string | null => {
|
|
48
|
+
return pushUrlByTaskId[taskId] ?? null;
|
|
49
|
+
},
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
mock.module("../../../../util/logger.js", () => ({
|
|
53
|
+
getLogger: () => ({
|
|
54
|
+
debug: () => {},
|
|
55
|
+
info: () => {},
|
|
56
|
+
warn: () => {},
|
|
57
|
+
error: () => {},
|
|
58
|
+
}),
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
// Intercept global fetch for push notification testing
|
|
62
|
+
const originalFetch = globalThis.fetch;
|
|
63
|
+
|
|
64
|
+
// Import the module under test AFTER mocks are set up
|
|
65
|
+
const { deliverA2AReply } = await import("../deliver.js");
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Setup / teardown
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
completedTask = null;
|
|
73
|
+
completeWithArtifactsCalls = [];
|
|
74
|
+
pushUrlByTaskId = {};
|
|
75
|
+
completeError = null;
|
|
76
|
+
fetchCalls.length = 0;
|
|
77
|
+
fetchResponses = [];
|
|
78
|
+
fetchCallIndex = 0;
|
|
79
|
+
|
|
80
|
+
globalThis.fetch = (async (
|
|
81
|
+
input: string | URL | Request,
|
|
82
|
+
init?: RequestInit,
|
|
83
|
+
) => {
|
|
84
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
85
|
+
fetchCalls.push({ url, init: init ?? {} });
|
|
86
|
+
const responseSpec = fetchResponses[fetchCallIndex++] ?? {
|
|
87
|
+
ok: true,
|
|
88
|
+
status: 200,
|
|
89
|
+
body: "{}",
|
|
90
|
+
};
|
|
91
|
+
return new Response(responseSpec.body, {
|
|
92
|
+
status: responseSpec.status,
|
|
93
|
+
statusText: responseSpec.ok ? "OK" : "Error",
|
|
94
|
+
});
|
|
95
|
+
}) as typeof fetch;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
afterEach(() => {
|
|
99
|
+
globalThis.fetch = originalFetch;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Tests
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
describe("deliverA2AReply", () => {
|
|
107
|
+
const baseCallbackUrl = "https://example.com/deliver/a2a?taskId=task-123";
|
|
108
|
+
|
|
109
|
+
test("completes task with text artifact", async () => {
|
|
110
|
+
const payload: ChannelReplyPayload = {
|
|
111
|
+
chatId: "chat-1",
|
|
112
|
+
text: "Hello from the assistant",
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const result = await deliverA2AReply(baseCallbackUrl, payload);
|
|
116
|
+
|
|
117
|
+
expect(result.ok).toBe(true);
|
|
118
|
+
expect(completeWithArtifactsCalls).toHaveLength(1);
|
|
119
|
+
expect(completeWithArtifactsCalls[0].taskId).toBe("task-123");
|
|
120
|
+
expect(completeWithArtifactsCalls[0].artifacts).toHaveLength(1);
|
|
121
|
+
expect(completeWithArtifactsCalls[0].artifacts[0].parts).toEqual([
|
|
122
|
+
{ kind: "text", text: "Hello from the assistant" },
|
|
123
|
+
]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("completes task with file attachments", async () => {
|
|
127
|
+
const payload: ChannelReplyPayload = {
|
|
128
|
+
chatId: "chat-1",
|
|
129
|
+
text: "Here is a file",
|
|
130
|
+
attachments: [
|
|
131
|
+
{
|
|
132
|
+
id: "att-1",
|
|
133
|
+
filename: "report.pdf",
|
|
134
|
+
mimeType: "application/pdf",
|
|
135
|
+
sizeBytes: 1024,
|
|
136
|
+
kind: "file",
|
|
137
|
+
data: "data:application/pdf;base64,abc123",
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const result = await deliverA2AReply(baseCallbackUrl, payload);
|
|
143
|
+
|
|
144
|
+
expect(result.ok).toBe(true);
|
|
145
|
+
expect(completeWithArtifactsCalls).toHaveLength(1);
|
|
146
|
+
const parts = completeWithArtifactsCalls[0].artifacts[0].parts;
|
|
147
|
+
expect(parts).toHaveLength(2);
|
|
148
|
+
expect(parts[0]).toEqual({
|
|
149
|
+
kind: "text",
|
|
150
|
+
text: "Here is a file",
|
|
151
|
+
});
|
|
152
|
+
expect(parts[1]).toEqual({
|
|
153
|
+
kind: "file",
|
|
154
|
+
filename: "report.pdf",
|
|
155
|
+
media_type: "application/pdf",
|
|
156
|
+
url: "data:application/pdf;base64,abc123",
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("returns ok: false when taskId is missing from URL", async () => {
|
|
161
|
+
const result = await deliverA2AReply("https://example.com/deliver/a2a", {
|
|
162
|
+
chatId: "chat-1",
|
|
163
|
+
text: "Hello",
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(result.ok).toBe(false);
|
|
167
|
+
expect(completeWithArtifactsCalls).toHaveLength(0);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("returns ok: true when payload has no content", async () => {
|
|
171
|
+
const result = await deliverA2AReply(baseCallbackUrl, {
|
|
172
|
+
chatId: "chat-1",
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(result.ok).toBe(true);
|
|
176
|
+
expect(completeWithArtifactsCalls).toHaveLength(0);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("returns ok: false when task completion throws", async () => {
|
|
180
|
+
completeError = new Error("A2A task not found: task-123");
|
|
181
|
+
|
|
182
|
+
const result = await deliverA2AReply(baseCallbackUrl, {
|
|
183
|
+
chatId: "chat-1",
|
|
184
|
+
text: "Hello",
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(result.ok).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("returns ok: false when task is already terminal", async () => {
|
|
191
|
+
completeError = new Error(
|
|
192
|
+
'Cannot transition task task-123 from terminal state "completed" to "completed"',
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const result = await deliverA2AReply(baseCallbackUrl, {
|
|
196
|
+
chatId: "chat-1",
|
|
197
|
+
text: "Hello",
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
expect(result.ok).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("push notifications", () => {
|
|
204
|
+
test("POSTs completed task to push URL", async () => {
|
|
205
|
+
pushUrlByTaskId["task-123"] = "https://requester.example.com/push";
|
|
206
|
+
fetchResponses = [{ ok: true, status: 200, body: "{}" }];
|
|
207
|
+
|
|
208
|
+
const result = await deliverA2AReply(baseCallbackUrl, {
|
|
209
|
+
chatId: "chat-1",
|
|
210
|
+
text: "Done",
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect(result.ok).toBe(true);
|
|
214
|
+
|
|
215
|
+
// Wait for the fire-and-forget push to complete
|
|
216
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
217
|
+
|
|
218
|
+
expect(fetchCalls).toHaveLength(1);
|
|
219
|
+
expect(fetchCalls[0].url).toBe("https://requester.example.com/push");
|
|
220
|
+
expect(fetchCalls[0].init.method).toBe("POST");
|
|
221
|
+
|
|
222
|
+
const headers = fetchCalls[0].init.headers as Record<string, string>;
|
|
223
|
+
expect(headers["Content-Type"]).toBe("application/a2a+json");
|
|
224
|
+
expect(headers["A2A-Version"]).toBe("1.0");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("does not push when no push URL configured", async () => {
|
|
228
|
+
const result = await deliverA2AReply(baseCallbackUrl, {
|
|
229
|
+
chatId: "chat-1",
|
|
230
|
+
text: "Done",
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
expect(result.ok).toBe(true);
|
|
234
|
+
|
|
235
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
236
|
+
|
|
237
|
+
expect(fetchCalls).toHaveLength(0);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("push failure does not affect delivery result", async () => {
|
|
241
|
+
pushUrlByTaskId["task-123"] = "https://requester.example.com/push";
|
|
242
|
+
// All retries fail with 500
|
|
243
|
+
fetchResponses = Array(4).fill({
|
|
244
|
+
ok: false,
|
|
245
|
+
status: 500,
|
|
246
|
+
body: "Internal Server Error",
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const result = await deliverA2AReply(baseCallbackUrl, {
|
|
250
|
+
chatId: "chat-1",
|
|
251
|
+
text: "Done",
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Delivery still succeeds even though push will fail
|
|
255
|
+
expect(result.ok).toBe(true);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("stops retrying on non-retryable client error", async () => {
|
|
259
|
+
pushUrlByTaskId["task-123"] = "https://requester.example.com/push";
|
|
260
|
+
fetchResponses = [{ ok: false, status: 404, body: "Not Found" }];
|
|
261
|
+
|
|
262
|
+
await deliverA2AReply(baseCallbackUrl, {
|
|
263
|
+
chatId: "chat-1",
|
|
264
|
+
text: "Done",
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Wait for the fire-and-forget push to settle
|
|
268
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
269
|
+
|
|
270
|
+
// Should only attempt once on a 4xx (non-429) error
|
|
271
|
+
expect(fetchCalls).toHaveLength(1);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A direct delivery adapter.
|
|
3
|
+
*
|
|
4
|
+
* Completes an A2A task with response artifacts and optionally POSTs the
|
|
5
|
+
* completed task to the requester's push notification URL.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
ChannelDeliveryResult,
|
|
10
|
+
ChannelReplyPayload,
|
|
11
|
+
} from "@vellumai/gateway-client";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
A2A_CONTENT_TYPE,
|
|
15
|
+
A2A_VERSION,
|
|
16
|
+
A2A_VERSION_HEADER,
|
|
17
|
+
} from "../../../a2a/protocol-constants.js";
|
|
18
|
+
import type { Part } from "../../../a2a/protocol-types.js";
|
|
19
|
+
import * as taskStore from "../../../a2a/task-store.js";
|
|
20
|
+
import { getLogger } from "../../../util/logger.js";
|
|
21
|
+
import {
|
|
22
|
+
computeRetryDelay,
|
|
23
|
+
isRetryableStatus,
|
|
24
|
+
sleep,
|
|
25
|
+
} from "../../../util/retry.js";
|
|
26
|
+
|
|
27
|
+
const log = getLogger("a2a-deliver");
|
|
28
|
+
|
|
29
|
+
const MAX_RETRIES = 3;
|
|
30
|
+
const PUSH_TIMEOUT_MS = 15_000;
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Helpers
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/** Extract the `taskId` query parameter from a callback URL. */
|
|
37
|
+
function parseTaskId(callbackUrl: string): string | null {
|
|
38
|
+
try {
|
|
39
|
+
return new URL(callbackUrl).searchParams.get("taskId");
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Build A2A parts from a channel reply payload. */
|
|
46
|
+
function buildParts(payload: ChannelReplyPayload): Part[] {
|
|
47
|
+
const parts: Part[] = [];
|
|
48
|
+
|
|
49
|
+
if (payload.text) {
|
|
50
|
+
parts.push({ kind: "text", text: payload.text });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (payload.attachments) {
|
|
54
|
+
for (const att of payload.attachments) {
|
|
55
|
+
parts.push({
|
|
56
|
+
kind: "file",
|
|
57
|
+
filename: att.filename,
|
|
58
|
+
media_type: att.mimeType,
|
|
59
|
+
url: att.data,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return parts;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** POST the completed task to the requester's push URL with retry. */
|
|
68
|
+
async function pushNotification(
|
|
69
|
+
pushUrl: string,
|
|
70
|
+
taskJson: unknown,
|
|
71
|
+
): Promise<void> {
|
|
72
|
+
let lastError: Error | null = null;
|
|
73
|
+
|
|
74
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
75
|
+
if (attempt > 0) {
|
|
76
|
+
await sleep(computeRetryDelay(attempt - 1));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const response = await fetch(pushUrl, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: {
|
|
83
|
+
"Content-Type": A2A_CONTENT_TYPE,
|
|
84
|
+
[A2A_VERSION_HEADER]: A2A_VERSION,
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify(taskJson),
|
|
87
|
+
signal: AbortSignal.timeout(PUSH_TIMEOUT_MS),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (response.ok) return;
|
|
91
|
+
|
|
92
|
+
const body = await response.text().catch(() => "");
|
|
93
|
+
lastError = new Error(
|
|
94
|
+
`Push notification failed with status ${response.status}: ${body}`,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (!isRetryableStatus(response.status)) {
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
} catch (err) {
|
|
101
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Push failure is logged but doesn't propagate
|
|
106
|
+
log.warn(
|
|
107
|
+
{ pushUrl, error: lastError?.message },
|
|
108
|
+
"A2A push notification failed after retries",
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Public API
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
/** Deliver an assistant reply as an A2A task completion. */
|
|
117
|
+
export async function deliverA2AReply(
|
|
118
|
+
callbackUrl: string,
|
|
119
|
+
payload: ChannelReplyPayload,
|
|
120
|
+
): Promise<ChannelDeliveryResult> {
|
|
121
|
+
const taskId = parseTaskId(callbackUrl);
|
|
122
|
+
if (!taskId) {
|
|
123
|
+
return { ok: false };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const parts = buildParts(payload);
|
|
127
|
+
if (parts.length === 0) {
|
|
128
|
+
log.debug({ taskId }, "No content to deliver; skipping A2A completion");
|
|
129
|
+
return { ok: true };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let completedTask;
|
|
133
|
+
try {
|
|
134
|
+
completedTask = taskStore.completeWithArtifacts(taskId, [
|
|
135
|
+
{ artifact_id: crypto.randomUUID(), parts },
|
|
136
|
+
]);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
139
|
+
log.error({ taskId, error: message }, "Failed to complete A2A task");
|
|
140
|
+
return { ok: false };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Push notification — fire-and-forget
|
|
144
|
+
const pushUrl = taskStore.getPushUrl(taskId);
|
|
145
|
+
if (pushUrl) {
|
|
146
|
+
pushNotification(pushUrl, completedTask).catch((err) => {
|
|
147
|
+
log.error(
|
|
148
|
+
{ taskId, pushUrl, error: String(err) },
|
|
149
|
+
"Unexpected push notification error",
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
log.info({ taskId }, "A2A reply delivered");
|
|
155
|
+
return { ok: true };
|
|
156
|
+
}
|