@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
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
|
|
3
|
+
import type { DrizzleDb } from "../memory/db-connection.js";
|
|
4
|
+
import { getDb } from "../memory/db-connection.js";
|
|
5
|
+
import { rawChanges } from "../memory/raw-query.js";
|
|
6
|
+
import { a2aTasks } from "../memory/schema.js";
|
|
7
|
+
import { TERMINAL_TASK_STATES } from "./protocol-constants.js";
|
|
8
|
+
import type {
|
|
9
|
+
A2AMessage,
|
|
10
|
+
A2ATask,
|
|
11
|
+
Artifact,
|
|
12
|
+
TaskState,
|
|
13
|
+
} from "./protocol-types.js";
|
|
14
|
+
|
|
15
|
+
// ── Internal types ──────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/** Raw database row shape for a2a_tasks. */
|
|
18
|
+
type A2ATaskRow = typeof a2aTasks.$inferSelect;
|
|
19
|
+
|
|
20
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/** Throw if the task doesn't exist or is in a terminal state. */
|
|
23
|
+
function assertNonTerminal(
|
|
24
|
+
db: DrizzleDb,
|
|
25
|
+
taskId: string,
|
|
26
|
+
targetState: TaskState,
|
|
27
|
+
): void {
|
|
28
|
+
const current = db
|
|
29
|
+
.select({ state: a2aTasks.state })
|
|
30
|
+
.from(a2aTasks)
|
|
31
|
+
.where(eq(a2aTasks.id, taskId))
|
|
32
|
+
.get();
|
|
33
|
+
|
|
34
|
+
if (!current) {
|
|
35
|
+
throw new Error(`A2A task not found: ${taskId}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (TERMINAL_TASK_STATES.has(current.state as TaskState)) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Cannot transition task ${taskId} from terminal state "${current.state}" to "${targetState}"`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function rowToTask(row: A2ATaskRow): A2ATask {
|
|
46
|
+
return {
|
|
47
|
+
id: row.id,
|
|
48
|
+
context_id: row.contextId ?? undefined,
|
|
49
|
+
status: {
|
|
50
|
+
state: row.state as TaskState,
|
|
51
|
+
message: row.statusMessage
|
|
52
|
+
? {
|
|
53
|
+
message_id: crypto.randomUUID(),
|
|
54
|
+
role: "agent",
|
|
55
|
+
parts: [{ kind: "text", text: row.statusMessage }],
|
|
56
|
+
}
|
|
57
|
+
: undefined,
|
|
58
|
+
timestamp: new Date(row.updatedAt).toISOString(),
|
|
59
|
+
},
|
|
60
|
+
artifacts: row.artifactsJson
|
|
61
|
+
? (JSON.parse(row.artifactsJson) as Artifact[])
|
|
62
|
+
: undefined,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Store functions ─────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
export function createTask(params: {
|
|
69
|
+
contextId?: string;
|
|
70
|
+
senderAssistantId: string;
|
|
71
|
+
requestMessage: A2AMessage;
|
|
72
|
+
pushUrl?: string;
|
|
73
|
+
}): A2ATask {
|
|
74
|
+
const db = getDb();
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
|
|
77
|
+
const row: A2ATaskRow = {
|
|
78
|
+
id: crypto.randomUUID(),
|
|
79
|
+
contextId: params.contextId ?? null,
|
|
80
|
+
conversationId: null,
|
|
81
|
+
state: "submitted",
|
|
82
|
+
statusMessage: null,
|
|
83
|
+
requestMessageJson: JSON.stringify(params.requestMessage),
|
|
84
|
+
artifactsJson: null,
|
|
85
|
+
pushUrl: params.pushUrl ?? null,
|
|
86
|
+
senderAssistantId: params.senderAssistantId,
|
|
87
|
+
createdAt: now,
|
|
88
|
+
updatedAt: now,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
db.insert(a2aTasks).values(row).run();
|
|
92
|
+
|
|
93
|
+
return rowToTask(row);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function getTask(taskId: string): A2ATask | null {
|
|
97
|
+
const db = getDb();
|
|
98
|
+
const row = db.select().from(a2aTasks).where(eq(a2aTasks.id, taskId)).get();
|
|
99
|
+
return row ? rowToTask(row) : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function updateState(
|
|
103
|
+
taskId: string,
|
|
104
|
+
state: TaskState,
|
|
105
|
+
statusMessage?: string,
|
|
106
|
+
): A2ATask {
|
|
107
|
+
const db = getDb();
|
|
108
|
+
assertNonTerminal(db, taskId, state);
|
|
109
|
+
|
|
110
|
+
db.update(a2aTasks)
|
|
111
|
+
.set({
|
|
112
|
+
state,
|
|
113
|
+
statusMessage: statusMessage ?? null,
|
|
114
|
+
updatedAt: Date.now(),
|
|
115
|
+
})
|
|
116
|
+
.where(eq(a2aTasks.id, taskId))
|
|
117
|
+
.run();
|
|
118
|
+
|
|
119
|
+
return rowToTask(
|
|
120
|
+
db.select().from(a2aTasks).where(eq(a2aTasks.id, taskId)).get()!,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function completeWithArtifacts(
|
|
125
|
+
taskId: string,
|
|
126
|
+
artifacts: Artifact[],
|
|
127
|
+
): A2ATask {
|
|
128
|
+
const db = getDb();
|
|
129
|
+
assertNonTerminal(db, taskId, "completed");
|
|
130
|
+
|
|
131
|
+
db.update(a2aTasks)
|
|
132
|
+
.set({
|
|
133
|
+
state: "completed",
|
|
134
|
+
statusMessage: null,
|
|
135
|
+
artifactsJson: JSON.stringify(artifacts),
|
|
136
|
+
updatedAt: Date.now(),
|
|
137
|
+
})
|
|
138
|
+
.where(eq(a2aTasks.id, taskId))
|
|
139
|
+
.run();
|
|
140
|
+
|
|
141
|
+
return rowToTask(
|
|
142
|
+
db.select().from(a2aTasks).where(eq(a2aTasks.id, taskId)).get()!,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function linkConversation(taskId: string, conversationId: string): void {
|
|
147
|
+
const db = getDb();
|
|
148
|
+
const now = Date.now();
|
|
149
|
+
|
|
150
|
+
db.update(a2aTasks)
|
|
151
|
+
.set({ conversationId, updatedAt: now })
|
|
152
|
+
.where(eq(a2aTasks.id, taskId))
|
|
153
|
+
.run();
|
|
154
|
+
|
|
155
|
+
if (rawChanges() === 0) {
|
|
156
|
+
throw new Error(`A2A task not found: ${taskId}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function getPushUrl(taskId: string): string | null {
|
|
161
|
+
const db = getDb();
|
|
162
|
+
const row = db
|
|
163
|
+
.select({ pushUrl: a2aTasks.pushUrl })
|
|
164
|
+
.from(a2aTasks)
|
|
165
|
+
.where(eq(a2aTasks.id, taskId))
|
|
166
|
+
.get();
|
|
167
|
+
return row?.pushUrl ?? null;
|
|
168
|
+
}
|
package/src/agent/loop.ts
CHANGED
|
@@ -67,6 +67,43 @@ export interface CheckpointInfo {
|
|
|
67
67
|
|
|
68
68
|
export type CheckpointDecision = "continue" | "yield";
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Why an {@link AgentLoop.run} invocation exited its `while (true)` body.
|
|
72
|
+
*
|
|
73
|
+
* Emitted exactly once per run as part of an {@link AgentEvent} of type
|
|
74
|
+
* `agent_loop_exit`, then persisted onto the **final** `llm_request_logs`
|
|
75
|
+
* row of the run. Rows from intermediate turns keep a NULL
|
|
76
|
+
* `agent_loop_exit_reason`, which is how downstream tooling (and the LLM
|
|
77
|
+
* Context Inspector) distinguishes "loop kept going" from "loop is done".
|
|
78
|
+
*
|
|
79
|
+
* Values are stable wire/DB strings — they are written to SQLite and
|
|
80
|
+
* surfaced over the inspector wire format, so renaming any of them is a
|
|
81
|
+
* breaking change.
|
|
82
|
+
*
|
|
83
|
+
* Cardinality matches the nine `break;`/`throw` sites currently inside the
|
|
84
|
+
* loop body. Keep in sync with `emitExit` call sites in
|
|
85
|
+
* {@link AgentLoop.run}.
|
|
86
|
+
*/
|
|
87
|
+
export type AgentLoopExitReason =
|
|
88
|
+
/** `if (signal?.aborted) break;` at the top of the loop. */
|
|
89
|
+
| "aborted_pre_call"
|
|
90
|
+
/** Empty assistant response after the configured retry budget. */
|
|
91
|
+
| "empty_response_exhausted"
|
|
92
|
+
/** Assistant message has no tool-use blocks (or no tool executor). */
|
|
93
|
+
| "no_tool_calls"
|
|
94
|
+
/** Signal aborted while building the user-side tool-results message. */
|
|
95
|
+
| "aborted_post_response"
|
|
96
|
+
/** Signal aborted mid-tool-execution; completed results were pushed. */
|
|
97
|
+
| "aborted_during_tools"
|
|
98
|
+
/** A tool result requested handing back to the user. */
|
|
99
|
+
| "yield_to_user"
|
|
100
|
+
/** The orchestrator's `onCheckpoint` callback returned `"yield"`. */
|
|
101
|
+
| "checkpoint_yield"
|
|
102
|
+
/** Signal aborted while the catch handler was synthesizing an error turn. */
|
|
103
|
+
| "aborted_via_error"
|
|
104
|
+
/** Catch-block fallback: an unhandled error broke the loop. */
|
|
105
|
+
| "error";
|
|
106
|
+
|
|
70
107
|
export type AgentEvent =
|
|
71
108
|
| { type: "text_delta"; text: string }
|
|
72
109
|
| { type: "thinking_delta"; thinking: string }
|
|
@@ -126,6 +163,32 @@ export type AgentEvent =
|
|
|
126
163
|
content?: unknown[];
|
|
127
164
|
}
|
|
128
165
|
| { type: "error"; error: Error }
|
|
166
|
+
| {
|
|
167
|
+
/**
|
|
168
|
+
* Emitted when the `llmCall` pipeline throws — i.e. the provider
|
|
169
|
+
* rejected the request before returning a usable response. Carries
|
|
170
|
+
* the loop-level raw request we attempted to send (messages, tools,
|
|
171
|
+
* system prompt, provider-agnostic config) plus the thrown error.
|
|
172
|
+
* Consumers (`handleProviderError` in the daemon handlers, the
|
|
173
|
+
* `onEvent` in `agent-wake`) persist these as `llm_request_logs`
|
|
174
|
+
* rows so failed calls are queryable in the LLM inspector instead
|
|
175
|
+
* of only surfacing in pino logs.
|
|
176
|
+
*
|
|
177
|
+
* `rawRequest` is the loop-level abstract shape rather than the
|
|
178
|
+
* provider-specific payload (which the provider builds internally
|
|
179
|
+
* and never returns when it throws). `actualProvider` echoes the
|
|
180
|
+
* `ProviderError.provider` tag when available so the persisted row
|
|
181
|
+
* has the same `provider` column value as a successful `usage` row.
|
|
182
|
+
*
|
|
183
|
+
* Re-thrown by the inner LLM-call try/catch after emission so the
|
|
184
|
+
* outer agent-loop catch still handles abort, Sentry capture, the
|
|
185
|
+
* existing `error` event, and the loop break.
|
|
186
|
+
*/
|
|
187
|
+
type: "provider_error";
|
|
188
|
+
rawRequest: unknown;
|
|
189
|
+
error: Error;
|
|
190
|
+
actualProvider?: string;
|
|
191
|
+
}
|
|
129
192
|
| {
|
|
130
193
|
type: "usage";
|
|
131
194
|
inputTokens: number;
|
|
@@ -144,6 +207,18 @@ export type AgentEvent =
|
|
|
144
207
|
* for this call (e.g. legacy/stubbed code paths).
|
|
145
208
|
*/
|
|
146
209
|
estimatedInputTokens?: number;
|
|
210
|
+
}
|
|
211
|
+
| {
|
|
212
|
+
/**
|
|
213
|
+
* Emitted exactly once at the end of {@link AgentLoop.run}, after the
|
|
214
|
+
* loop body has exited (whether via `break;`, an unhandled error in
|
|
215
|
+
* the catch block, or the empty-response throw path). Consumers
|
|
216
|
+
* persist `reason` onto the final `llm_request_logs` row for the run;
|
|
217
|
+
* intermediate rows keep `agent_loop_exit_reason = NULL`, which is the
|
|
218
|
+
* canonical "loop kept going" signal.
|
|
219
|
+
*/
|
|
220
|
+
type: "agent_loop_exit";
|
|
221
|
+
reason: AgentLoopExitReason;
|
|
147
222
|
};
|
|
148
223
|
|
|
149
224
|
const DEFAULT_CONFIG: AgentLoopConfig = {
|
|
@@ -398,8 +473,24 @@ export class AgentLoop {
|
|
|
398
473
|
const substitutionMap = new Map<string, string>();
|
|
399
474
|
let streamingPending = "";
|
|
400
475
|
|
|
476
|
+
// Idempotency guard for `emitExit`. Used so the throw path in the
|
|
477
|
+
// empty-response branch can stamp its reason ("empty_response_exhausted")
|
|
478
|
+
// before throwing — the catch handler that observes the rethrow will
|
|
479
|
+
// then attempt to stamp "error" and harmlessly no-op, preserving the
|
|
480
|
+
// more specific reason. Also defends against accidental future
|
|
481
|
+
// double-emits if a new break site is added without checking this.
|
|
482
|
+
let exitReasonEmitted = false;
|
|
483
|
+
const emitExit = async (reason: AgentLoopExitReason): Promise<void> => {
|
|
484
|
+
if (exitReasonEmitted) return;
|
|
485
|
+
exitReasonEmitted = true;
|
|
486
|
+
await onEvent({ type: "agent_loop_exit", reason });
|
|
487
|
+
};
|
|
488
|
+
|
|
401
489
|
while (true) {
|
|
402
|
-
if (signal?.aborted)
|
|
490
|
+
if (signal?.aborted) {
|
|
491
|
+
await emitExit("aborted_pre_call");
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
403
494
|
|
|
404
495
|
rlog.info(
|
|
405
496
|
{ turn: toolUseTurns, messageCount: history.length },
|
|
@@ -618,23 +709,64 @@ export class AgentLoop {
|
|
|
618
709
|
toolUseTurns,
|
|
619
710
|
);
|
|
620
711
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
712
|
+
// Inner try/catch narrows error-recording scope to the provider
|
|
713
|
+
// call itself. The outer agent-loop catch (below) wraps the entire
|
|
714
|
+
// turn body (tool execution, plugin pipelines, checkpoints), so
|
|
715
|
+
// recording there would risk mis-attributing tool/plugin throws as
|
|
716
|
+
// provider rejections. On provider failure we emit `provider_error`
|
|
717
|
+
// with the loop-level raw request so consumers can persist it as an
|
|
718
|
+
// `llm_request_logs` row, then re-throw so the existing outer catch
|
|
719
|
+
// continues to handle abort sync, Sentry capture, the `error` event,
|
|
720
|
+
// and the loop break unchanged.
|
|
721
|
+
let response: LLMCallResult;
|
|
722
|
+
try {
|
|
723
|
+
response = await runPipeline<LLMCallArgs, LLMCallResult>(
|
|
724
|
+
"llmCall",
|
|
725
|
+
getMiddlewaresFor("llmCall"),
|
|
726
|
+
(args) =>
|
|
727
|
+
args.provider.sendMessage(
|
|
728
|
+
args.messages,
|
|
729
|
+
args.tools,
|
|
730
|
+
args.systemPrompt,
|
|
731
|
+
args.options,
|
|
732
|
+
),
|
|
733
|
+
llmCallArgs,
|
|
734
|
+
turnCtx,
|
|
735
|
+
DEFAULT_TIMEOUTS.llmCall,
|
|
736
|
+
);
|
|
737
|
+
} catch (llmCallError) {
|
|
738
|
+
// Skip recording on abort — the user cancelled the request and
|
|
739
|
+
// there's no provider rejection worth a log row. The outer catch
|
|
740
|
+
// still synthesizes cancellation tool_results.
|
|
741
|
+
if (!signal?.aborted) {
|
|
742
|
+
const errInstance =
|
|
743
|
+
llmCallError instanceof Error
|
|
744
|
+
? llmCallError
|
|
745
|
+
: new Error(String(llmCallError));
|
|
746
|
+
// Strip non-serializable / runtime-only fields from `options`
|
|
747
|
+
// before snapshotting. `onEvent` is a closure with side effects
|
|
748
|
+
// and `signal` is an AbortSignal — neither is meaningful in a
|
|
749
|
+
// persisted log row, and `JSON.stringify` would silently drop or
|
|
750
|
+
// misrepresent both.
|
|
751
|
+
const rawRequest = {
|
|
752
|
+
provider: this.provider.name,
|
|
753
|
+
messages: llmCallArgs.messages,
|
|
754
|
+
tools: llmCallArgs.tools,
|
|
755
|
+
systemPrompt: llmCallArgs.systemPrompt,
|
|
756
|
+
config: llmCallArgs.options?.config,
|
|
757
|
+
};
|
|
758
|
+
onEvent({
|
|
759
|
+
type: "provider_error",
|
|
760
|
+
rawRequest,
|
|
761
|
+
error: errInstance,
|
|
762
|
+
actualProvider:
|
|
763
|
+
errInstance instanceof ProviderError
|
|
764
|
+
? errInstance.provider
|
|
765
|
+
: this.provider.name,
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
throw llmCallError;
|
|
769
|
+
}
|
|
638
770
|
|
|
639
771
|
const providerDurationMs = Date.now() - providerStart;
|
|
640
772
|
|
|
@@ -785,6 +917,11 @@ export class AgentLoop {
|
|
|
785
917
|
{ turn: toolUseTurns, retries: emptyResponseRetries },
|
|
786
918
|
"emptyResponse pipeline requested error surface",
|
|
787
919
|
);
|
|
920
|
+
// Stamp the specific exit reason *before* throwing. The catch
|
|
921
|
+
// handler below will see the rethrown error and attempt to stamp
|
|
922
|
+
// "error" — guarded by `exitReasonEmitted`, that becomes a no-op
|
|
923
|
+
// and the more specific reason wins.
|
|
924
|
+
await emitExit("empty_response_exhausted");
|
|
788
925
|
throw new AssistantError(
|
|
789
926
|
"Model returned empty response after tool results",
|
|
790
927
|
ErrorCode.INTERNAL_ERROR,
|
|
@@ -811,6 +948,7 @@ export class AgentLoop {
|
|
|
811
948
|
await onEvent({ type: "message_complete", message: assistantMessage });
|
|
812
949
|
|
|
813
950
|
if (toolUseBlocks.length === 0 || !this.toolExecutor) {
|
|
951
|
+
await emitExit("no_tool_calls");
|
|
814
952
|
break;
|
|
815
953
|
}
|
|
816
954
|
|
|
@@ -835,6 +973,7 @@ export class AgentLoop {
|
|
|
835
973
|
}),
|
|
836
974
|
);
|
|
837
975
|
history.push({ role: "user", content: cancelledBlocks });
|
|
976
|
+
await emitExit("aborted_post_response");
|
|
838
977
|
break;
|
|
839
978
|
}
|
|
840
979
|
|
|
@@ -1022,6 +1161,7 @@ export class AgentLoop {
|
|
|
1022
1161
|
// If cancelled during execution, push completed results and stop
|
|
1023
1162
|
if (signal?.aborted) {
|
|
1024
1163
|
history.push({ role: "user", content: resultBlocks });
|
|
1164
|
+
await emitExit("aborted_during_tools");
|
|
1025
1165
|
break;
|
|
1026
1166
|
}
|
|
1027
1167
|
|
|
@@ -1029,6 +1169,7 @@ export class AgentLoop {
|
|
|
1029
1169
|
// surface awaiting a button click), push results and stop the loop.
|
|
1030
1170
|
if (toolResults.some(({ result }) => result.yieldToUser)) {
|
|
1031
1171
|
history.push({ role: "user", content: resultBlocks });
|
|
1172
|
+
await emitExit("yield_to_user");
|
|
1032
1173
|
break;
|
|
1033
1174
|
}
|
|
1034
1175
|
|
|
@@ -1095,6 +1236,7 @@ export class AgentLoop {
|
|
|
1095
1236
|
history,
|
|
1096
1237
|
});
|
|
1097
1238
|
if (decision === "yield") {
|
|
1239
|
+
await emitExit("checkpoint_yield");
|
|
1098
1240
|
break;
|
|
1099
1241
|
}
|
|
1100
1242
|
}
|
|
@@ -1114,6 +1256,7 @@ export class AgentLoop {
|
|
|
1114
1256
|
);
|
|
1115
1257
|
history.push({ role: "user", content: cancelledBlocks });
|
|
1116
1258
|
}
|
|
1259
|
+
await emitExit("aborted_via_error");
|
|
1117
1260
|
break;
|
|
1118
1261
|
}
|
|
1119
1262
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
@@ -1125,6 +1268,12 @@ export class AgentLoop {
|
|
|
1125
1268
|
Sentry.captureException(err);
|
|
1126
1269
|
}
|
|
1127
1270
|
onEvent({ type: "error", error: err });
|
|
1271
|
+
// Catch-block fallback. If the rethrow came from the
|
|
1272
|
+
// empty-response throw path above, `emitExit("error")` no-ops
|
|
1273
|
+
// because `emitExit("empty_response_exhausted")` already ran
|
|
1274
|
+
// before the throw. Otherwise, this is the genuine
|
|
1275
|
+
// unhandled-error exit.
|
|
1276
|
+
await emitExit("error");
|
|
1128
1277
|
break;
|
|
1129
1278
|
}
|
|
1130
1279
|
}
|
package/src/channels/config.ts
CHANGED
|
@@ -94,6 +94,15 @@ const CHANNEL_POLICIES = {
|
|
|
94
94
|
codeRedemptionEnabled: false,
|
|
95
95
|
},
|
|
96
96
|
},
|
|
97
|
+
a2a: {
|
|
98
|
+
notification: {
|
|
99
|
+
deliveryEnabled: false,
|
|
100
|
+
conversationStrategy: "continue_existing_conversation",
|
|
101
|
+
},
|
|
102
|
+
invite: {
|
|
103
|
+
codeRedemptionEnabled: false,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
97
106
|
} as const satisfies Record<ChannelId, ChannelNotificationPolicy>;
|
|
98
107
|
|
|
99
108
|
export type ChannelPolicies = typeof CHANNEL_POLICIES;
|
package/src/channels/types.ts
CHANGED
|
@@ -6,6 +6,7 @@ export const CHANNEL_IDS = [
|
|
|
6
6
|
"slack",
|
|
7
7
|
"email",
|
|
8
8
|
"platform",
|
|
9
|
+
"a2a",
|
|
9
10
|
] as const;
|
|
10
11
|
|
|
11
12
|
export type ChannelId = (typeof CHANNEL_IDS)[number];
|
|
@@ -133,6 +134,18 @@ export const CHANNEL_METADATA: Partial<Record<ChannelId, ChannelInfo>> = {
|
|
|
133
134
|
"I'd like to verify a contact's WhatsApp identity. Can you walk me through it?",
|
|
134
135
|
},
|
|
135
136
|
},
|
|
137
|
+
a2a: {
|
|
138
|
+
id: "a2a",
|
|
139
|
+
label: "A2A",
|
|
140
|
+
subtitle: "Agent-to-Agent protocol",
|
|
141
|
+
icon: "bot",
|
|
142
|
+
supportsVerification: false,
|
|
143
|
+
setupMessages: {
|
|
144
|
+
guardian: "Connect with other Vellum assistants via the A2A protocol.",
|
|
145
|
+
contact:
|
|
146
|
+
"I'd like to connect with another assistant via A2A. Can you help me set that up?",
|
|
147
|
+
},
|
|
148
|
+
},
|
|
136
149
|
};
|
|
137
150
|
|
|
138
151
|
export const INTERFACE_IDS = [
|
|
@@ -146,6 +159,7 @@ export const INTERFACE_IDS = [
|
|
|
146
159
|
"slack",
|
|
147
160
|
"email",
|
|
148
161
|
"chrome-extension",
|
|
162
|
+
"a2a",
|
|
149
163
|
] as const;
|
|
150
164
|
|
|
151
165
|
export type InterfaceId = (typeof INTERFACE_IDS)[number];
|