@vellumai/assistant 0.8.9-staging.2 → 0.8.9-staging.4
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/docs/activation-funnel-telemetry.md +310 -0
- package/openapi.yaml +16 -115
- package/package.json +1 -1
- package/src/__tests__/activation-early-marking.test.ts +120 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +13 -13
- package/src/__tests__/anthropic-provider.test.ts +23 -8
- package/src/__tests__/approval-cascade.test.ts +1 -1
- package/src/__tests__/compaction-direct.test.ts +32 -18
- package/src/__tests__/compaction-events.test.ts +2 -2
- package/src/__tests__/compaction.benchmark.test.ts +1 -1
- package/src/__tests__/context-overflow-reducer.test.ts +5 -5
- package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-history-web-search.test.ts +5 -0
- package/src/__tests__/conversation-media-retry.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +40 -0
- package/src/__tests__/conversation-process-callsite.test.ts +1 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +71 -0
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
- package/src/__tests__/conversation-speed-override.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +44 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
- package/src/__tests__/conversation-undo.test.ts +2 -2
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/cu-unified-flow.test.ts +36 -0
- package/src/__tests__/history-repair-hook.test.ts +2 -0
- package/src/__tests__/llm-resolver.test.ts +73 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +1 -2
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
- package/src/__tests__/persist-unsendable-image.test.ts +97 -1
- package/src/__tests__/plugin-external-api.test.ts +68 -0
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
- package/src/__tests__/published-app-updater.test.ts +138 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +5 -7
- package/src/__tests__/title-generate-hook.test.ts +2 -0
- package/src/__tests__/web-fetch.test.ts +45 -0
- package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
- package/src/acp/resolve-agent.test.ts +0 -56
- package/src/acp/resolve-agent.ts +10 -38
- package/src/agent/loop.ts +13 -27
- package/src/api/responses/memory-v3-selection-log.ts +19 -10
- package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
- package/src/cli/commands/memory-v3.ts +57 -199
- package/src/cli/lib/__tests__/install-from-github.test.ts +232 -29
- package/src/cli/lib/__tests__/plugin-details.test.ts +28 -19
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +57 -7
- package/src/cli/lib/__tests__/search-plugins.test.ts +17 -10
- package/src/cli/lib/install-from-github.ts +258 -41
- package/src/cli/lib/plugin-details.ts +20 -13
- package/src/cli/lib/plugin-marketplace.ts +23 -5
- package/src/cli/lib/search-plugins.ts +14 -8
- package/src/config/acp-defaults.ts +3 -3
- package/src/config/acp-schema.ts +1 -7
- package/src/config/bundled-skills/acp/SKILL.md +4 -17
- package/src/config/bundled-skills/acp/TOOLS.json +2 -2
- package/src/config/call-site-defaults.ts +0 -1
- package/src/config/feature-flag-registry.json +3 -18
- package/src/config/llm-resolver.ts +39 -7
- package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
- package/src/config/schemas/call-site-catalog.ts +0 -7
- package/src/config/schemas/llm.ts +17 -1
- package/src/config/schemas/memory-v3.ts +58 -8
- package/src/config/seed-inference-profiles.ts +18 -0
- package/src/context/post-turn-tool-result-truncation.ts +39 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +8 -1
- package/src/daemon/conversation-agent-loop.ts +87 -95
- package/src/daemon/conversation-error.ts +31 -4
- package/src/daemon/conversation-history.ts +1 -1
- package/src/daemon/conversation-media-retry.ts +19 -6
- package/src/daemon/conversation-messaging.ts +17 -0
- package/src/daemon/conversation-process.ts +14 -5
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +37 -1
- package/src/daemon/conversation-surfaces.ts +141 -3
- package/src/daemon/conversation.ts +48 -13
- package/src/daemon/external-plugins-bootstrap.ts +8 -3
- package/src/daemon/persist-unsendable-image.ts +62 -25
- package/src/daemon/process-message.ts +1 -1
- package/src/daemon/tool-side-effects.ts +15 -0
- package/src/memory/__tests__/activation-session-store.test.ts +41 -0
- package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
- package/src/memory/activation-session-store.ts +43 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
- package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/onboarding-events-store.ts +66 -18
- package/src/memory/schema/infrastructure.ts +13 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
- package/src/memory/v2/__tests__/page-store.test.ts +22 -0
- package/src/memory/v2/consolidation-job.ts +2 -72
- package/src/memory/v2/types.ts +5 -0
- package/src/messaging/providers/telegram-bot/api.ts +14 -5
- package/src/notifications/adapters/telegram.ts +7 -1
- package/src/plugin-api/constants.ts +2 -2
- package/src/plugin-api/index.ts +2 -2
- package/src/plugin-api/types.ts +19 -5
- package/src/plugins/defaults/compaction/compact.ts +24 -15
- package/src/plugins/defaults/compaction/context-overflow-reducer.ts +4 -4
- package/src/plugins/defaults/compaction/manager-store.ts +1 -1
- package/src/{context → plugins/defaults/compaction}/window-manager.ts +68 -12
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +12 -18
- package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +19 -66
- package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +139 -129
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +332 -164
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +516 -293
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +51 -21
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +66 -11
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +271 -110
- package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +0 -22
- package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +26 -62
- package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
- package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +3 -2
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +415 -181
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +196 -56
- package/src/plugins/defaults/memory-v3-shadow/page-content.ts +39 -2
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
- package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +15 -7
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
- package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
- package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +75 -3
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +111 -78
- package/src/plugins/defaults/memory-v3-shadow/types.ts +52 -19
- package/src/plugins/defaults/memory-v3-shadow/working-set.ts +4 -1
- package/src/plugins/external-api.ts +114 -0
- package/src/prompts/system-prompt.ts +61 -10
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +37 -2
- package/src/providers/anthropic/client.ts +9 -10
- package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
- package/src/providers/inference/logit-bias.ts +55 -0
- package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -1
- package/src/providers/retry.ts +22 -0
- package/src/providers/types.ts +6 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
- package/src/runtime/routes/acp-routes.test.ts +3 -20
- package/src/runtime/routes/app-management-routes.ts +3 -0
- package/src/runtime/routes/conversation-routes.ts +2 -0
- package/src/runtime/routes/memory-v3-routes.ts +64 -314
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
- package/src/runtime/routes/stt-routes.ts +45 -12
- package/src/runtime/routes/workspace-routes.ts +50 -15
- package/src/services/published-app-updater.ts +30 -8
- package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
- package/src/telemetry/activation-funnel.ts +167 -0
- package/src/telemetry/types.ts +13 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
- package/src/telemetry/usage-telemetry-reporter.ts +26 -1
- package/src/tools/acp/list-agents.test.ts +2 -18
- package/src/tools/acp/list-agents.ts +3 -15
- package/src/tools/acp/spawn.test.ts +0 -10
- package/src/tools/browser/browser-execution.ts +12 -2
- package/src/tools/network/web-fetch.ts +65 -24
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/ui-surface/definitions.ts +7 -0
- package/src/acp/feature-gate.test.ts +0 -48
- package/src/acp/feature-gate.ts +0 -34
- package/src/plugins/defaults/memory-v3-shadow/__tests__/assign.test.ts +0 -242
- package/src/plugins/defaults/memory-v3-shadow/__tests__/core.test.ts +0 -39
- package/src/plugins/defaults/memory-v3-shadow/__tests__/health.test.ts +0 -219
- package/src/plugins/defaults/memory-v3-shadow/__tests__/needle.test.ts +0 -107
- package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +0 -13
- package/src/plugins/defaults/memory-v3-shadow/__tests__/reconcile.test.ts +0 -274
- package/src/plugins/defaults/memory-v3-shadow/__tests__/router.test.ts +0 -337
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selector.test.ts +0 -470
- package/src/plugins/defaults/memory-v3-shadow/__tests__/snapshot.test.ts +0 -168
- package/src/plugins/defaults/memory-v3-shadow/__tests__/tree.test.ts +0 -192
- package/src/plugins/defaults/memory-v3-shadow/assign.ts +0 -272
- package/src/plugins/defaults/memory-v3-shadow/core.ts +0 -26
- package/src/plugins/defaults/memory-v3-shadow/health.ts +0 -0
- package/src/plugins/defaults/memory-v3-shadow/needle.ts +0 -115
- package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +0 -26
- package/src/plugins/defaults/memory-v3-shadow/reconcile.ts +0 -527
- package/src/plugins/defaults/memory-v3-shadow/router.ts +0 -190
- package/src/plugins/defaults/memory-v3-shadow/selector.ts +0 -226
- package/src/plugins/defaults/memory-v3-shadow/snapshot.ts +0 -209
- package/src/plugins/defaults/memory-v3-shadow/tree.ts +0 -174
- package/src/util/map-limit.ts +0 -27
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
TARGET_CHARS,
|
|
13
13
|
THRESHOLD_CHARS,
|
|
14
14
|
TOOL_RESULT_DIR,
|
|
15
|
+
TRUNCATION_EXEMPT_TOOLS,
|
|
15
16
|
TRUNCATION_MARKER,
|
|
16
17
|
} from "../context/post-turn-tool-result-truncation.js";
|
|
17
18
|
import type { ContentBlock, Message } from "../providers/types.js";
|
|
@@ -151,6 +152,74 @@ describe("postTurnTruncateToolResults", () => {
|
|
|
151
152
|
expect(stub).toContain(filePath);
|
|
152
153
|
});
|
|
153
154
|
|
|
155
|
+
test("skill_load result above threshold is NOT truncated (durable instructions exempt)", () => {
|
|
156
|
+
// Regression for JARVIS-1000: a hosted assistant lost its app-builder skill
|
|
157
|
+
// workflow when the large skill_load result was middle-truncated between the
|
|
158
|
+
// turn that loaded the skill and the turn that used it, then fell back to a
|
|
159
|
+
// local-dev (vite/localhost) build path.
|
|
160
|
+
const toolUseId = "tool_skill_load";
|
|
161
|
+
const skillBody = "S".repeat(THRESHOLD_CHARS + 5_000);
|
|
162
|
+
const messages: Message[] = [
|
|
163
|
+
{
|
|
164
|
+
role: "assistant",
|
|
165
|
+
content: [
|
|
166
|
+
{
|
|
167
|
+
type: "tool_use" as const,
|
|
168
|
+
id: toolUseId,
|
|
169
|
+
name: "skill_load",
|
|
170
|
+
input: { skill: "app-builder" },
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
{ role: "user", content: [makeToolResult(skillBody, toolUseId)] },
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
const { messages: result, truncatedCount } =
|
|
178
|
+
postTurnTruncateToolResults(messages, { conversationDir: convDir });
|
|
179
|
+
|
|
180
|
+
expect(truncatedCount).toBe(0);
|
|
181
|
+
expect(result).toBe(messages); // same reference — no copy
|
|
182
|
+
expect(existsSync(join(convDir, TOOL_RESULT_DIR))).toBe(false);
|
|
183
|
+
|
|
184
|
+
const block = result[1].content[0] as {
|
|
185
|
+
type: "tool_result";
|
|
186
|
+
content: string;
|
|
187
|
+
};
|
|
188
|
+
expect(block.content).toBe(skillBody);
|
|
189
|
+
expect(TRUNCATION_EXEMPT_TOOLS.has("skill_load")).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("non-exempt tool result above threshold is still truncated when paired with a tool_use", () => {
|
|
193
|
+
// Control for the exemption: same shape as the skill_load case, but a tool
|
|
194
|
+
// that is NOT exempt must still be truncated.
|
|
195
|
+
const toolUseId = "tool_bash_1";
|
|
196
|
+
const longContent = "B".repeat(THRESHOLD_CHARS + 5_000);
|
|
197
|
+
const messages: Message[] = [
|
|
198
|
+
{
|
|
199
|
+
role: "assistant",
|
|
200
|
+
content: [
|
|
201
|
+
{
|
|
202
|
+
type: "tool_use" as const,
|
|
203
|
+
id: toolUseId,
|
|
204
|
+
name: "bash",
|
|
205
|
+
input: { command: "cat big.log" },
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
{ role: "user", content: [makeToolResult(longContent, toolUseId)] },
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
const { messages: result, truncatedCount } =
|
|
213
|
+
postTurnTruncateToolResults(messages, { conversationDir: convDir });
|
|
214
|
+
|
|
215
|
+
expect(truncatedCount).toBe(1);
|
|
216
|
+
const block = result[1].content[0] as {
|
|
217
|
+
type: "tool_result";
|
|
218
|
+
content: string;
|
|
219
|
+
};
|
|
220
|
+
expect(block.content).toContain(TRUNCATION_MARKER);
|
|
221
|
+
});
|
|
222
|
+
|
|
154
223
|
test("file path is deterministic for the same toolUseId", () => {
|
|
155
224
|
const id = "tool_use_deterministic";
|
|
156
225
|
const path1 = getToolResultFilePath("/some/dir", id);
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests that the auto-redeploy path deploys the app's *effective* HTML
|
|
3
|
+
* (dist/index.html for multifile apps) rather than the empty `htmlDefinition`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
7
|
+
|
|
8
|
+
import type { AppDefinition } from "../memory/app-store.js";
|
|
9
|
+
|
|
10
|
+
// ── Mocks ───────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
let mockApp: AppDefinition | null = null;
|
|
13
|
+
let mockIsMultifile = false;
|
|
14
|
+
let mockEffectiveHtml = "";
|
|
15
|
+
// A directory that does not exist on disk, so the multifile dist-existence
|
|
16
|
+
// guard reads `false` from the real fs without mocking node:fs (which would
|
|
17
|
+
// leak across test files).
|
|
18
|
+
let mockAppDir = "/tmp/__vellum_test_nonexistent__/app-1";
|
|
19
|
+
|
|
20
|
+
mock.module("../memory/app-store.js", () => ({
|
|
21
|
+
getApp: () => mockApp,
|
|
22
|
+
getAppDirPath: () => mockAppDir,
|
|
23
|
+
isMultifileApp: () => mockIsMultifile,
|
|
24
|
+
resolveEffectiveAppHtml: () => mockEffectiveHtml,
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
let mockPublishedPage: {
|
|
28
|
+
id: string;
|
|
29
|
+
projectSlug?: string;
|
|
30
|
+
htmlHash: string;
|
|
31
|
+
} | null = null;
|
|
32
|
+
const updatePublishedPageSpy = mock(() => {});
|
|
33
|
+
|
|
34
|
+
mock.module("../memory/published-pages-store.js", () => ({
|
|
35
|
+
getActivePublishedPageByAppId: () => mockPublishedPage,
|
|
36
|
+
updatePublishedPage: updatePublishedPageSpy,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
const deploySpy = mock(async (_args: { html: string; name: string }) => ({
|
|
40
|
+
deploymentId: "dep-1",
|
|
41
|
+
url: "https://example.vercel.app",
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
mock.module("../services/vercel-deploy.js", () => ({
|
|
45
|
+
deployHtmlToVercel: deploySpy,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
// credentialBroker.serverUse invokes execute(token) and reports success.
|
|
49
|
+
mock.module("../tools/credentials/broker.js", () => ({
|
|
50
|
+
credentialBroker: {
|
|
51
|
+
serverUse: async ({
|
|
52
|
+
execute,
|
|
53
|
+
}: {
|
|
54
|
+
execute: (token: string) => Promise<unknown>;
|
|
55
|
+
}) => {
|
|
56
|
+
await execute("test-token");
|
|
57
|
+
return { success: true };
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
const { updatePublishedAppDeployment } =
|
|
63
|
+
await import("../services/published-app-updater.js");
|
|
64
|
+
|
|
65
|
+
function makeApp(overrides: Partial<AppDefinition> = {}): AppDefinition {
|
|
66
|
+
return {
|
|
67
|
+
id: "app-1",
|
|
68
|
+
name: "App",
|
|
69
|
+
schemaJson: "{}",
|
|
70
|
+
htmlDefinition: "",
|
|
71
|
+
createdAt: 1,
|
|
72
|
+
updatedAt: 2,
|
|
73
|
+
...overrides,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Tests ───────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
describe("updatePublishedAppDeployment", () => {
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
mockApp = makeApp();
|
|
82
|
+
mockIsMultifile = false;
|
|
83
|
+
mockEffectiveHtml = "";
|
|
84
|
+
mockPublishedPage = { id: "pp-1", projectSlug: "slug", htmlHash: "old" };
|
|
85
|
+
mockAppDir = "/tmp/__vellum_test_nonexistent__/app-1";
|
|
86
|
+
deploySpy.mockClear();
|
|
87
|
+
updatePublishedPageSpy.mockClear();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("deploys the resolved effective HTML, not the empty htmlDefinition", async () => {
|
|
91
|
+
// htmlDefinition is "" (as it is for every multifile app); the real
|
|
92
|
+
// content comes from resolveEffectiveAppHtml. isMultifile=false here keeps
|
|
93
|
+
// the dist guard out of the way so the assertion targets the html source.
|
|
94
|
+
mockApp = makeApp({ htmlDefinition: "" });
|
|
95
|
+
mockEffectiveHtml = "<html><body>real app</body></html>";
|
|
96
|
+
|
|
97
|
+
await updatePublishedAppDeployment("app-1");
|
|
98
|
+
|
|
99
|
+
expect(deploySpy).toHaveBeenCalledTimes(1);
|
|
100
|
+
expect(deploySpy.mock.calls[0][0].html).toBe(
|
|
101
|
+
"<html><body>real app</body></html>",
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("skips deploy when a multifile app has no compiled output", async () => {
|
|
106
|
+
mockIsMultifile = true;
|
|
107
|
+
mockApp = makeApp({ formatVersion: 2 });
|
|
108
|
+
mockEffectiveHtml = "<p>App compilation failed.</p>";
|
|
109
|
+
// mockAppDir does not exist → dist/index.html is absent.
|
|
110
|
+
|
|
111
|
+
await updatePublishedAppDeployment("app-1");
|
|
112
|
+
|
|
113
|
+
expect(deploySpy).not.toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("skips deploy when content hash is unchanged", async () => {
|
|
117
|
+
const html = "<html>same</html>";
|
|
118
|
+
mockEffectiveHtml = html;
|
|
119
|
+
const { createHash } = await import("node:crypto");
|
|
120
|
+
mockPublishedPage = {
|
|
121
|
+
id: "pp-1",
|
|
122
|
+
htmlHash: createHash("sha256").update(html).digest("hex"),
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
await updatePublishedAppDeployment("app-1");
|
|
126
|
+
|
|
127
|
+
expect(deploySpy).not.toHaveBeenCalled();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("skips deploy when the app has no active published page", async () => {
|
|
131
|
+
mockPublishedPage = null;
|
|
132
|
+
mockEffectiveHtml = "<html>x</html>";
|
|
133
|
+
|
|
134
|
+
await updatePublishedAppDeployment("app-1");
|
|
135
|
+
|
|
136
|
+
expect(deploySpy).not.toHaveBeenCalled();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -233,10 +233,9 @@ describe("frontmatter feature-flag integration", () => {
|
|
|
233
233
|
// ---------------------------------------------------------------------------
|
|
234
234
|
|
|
235
235
|
describe("bundled acp skill discoverability", () => {
|
|
236
|
-
test("acp skill resolves with
|
|
237
|
-
// The ACP skill carries its own first-time-setup instructions
|
|
238
|
-
//
|
|
239
|
-
// Runtime enforcement happens in the ACP tools via isAcpEnabled instead.
|
|
236
|
+
test("acp skill resolves with no frontmatter flag gate", () => {
|
|
237
|
+
// The ACP skill carries its own first-time-setup instructions and is
|
|
238
|
+
// always discoverable: it has no frontmatter feature-flag gate.
|
|
240
239
|
const skillMdPath = fileURLToPath(
|
|
241
240
|
new URL("../config/bundled-skills/acp/SKILL.md", import.meta.url),
|
|
242
241
|
);
|
|
@@ -247,10 +246,9 @@ describe("bundled acp skill discoverability", () => {
|
|
|
247
246
|
expect(skill!.featureFlag).toBeUndefined();
|
|
248
247
|
expect(skillFlagKey(skill!)).toBeUndefined();
|
|
249
248
|
|
|
250
|
-
// acp flag at its registry default (off) and config.acp disabled.
|
|
251
249
|
const config = makeConfig({
|
|
252
|
-
acp: {
|
|
253
|
-
}
|
|
250
|
+
acp: { maxConcurrentSessions: 4, agents: {} },
|
|
251
|
+
});
|
|
254
252
|
|
|
255
253
|
const resolved = resolveSkillStates([skill!], config);
|
|
256
254
|
expect(resolved.length).toBe(1);
|
|
@@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
|
5
5
|
import {
|
|
6
6
|
buildFetchResponseFromNodeResponse,
|
|
7
7
|
executeWebFetch,
|
|
8
|
+
getUpstreamStatus,
|
|
8
9
|
} from "../tools/network/web-fetch.js";
|
|
9
10
|
|
|
10
11
|
describe("web_fetch tool", () => {
|
|
@@ -62,6 +63,50 @@ describe("web_fetch tool", () => {
|
|
|
62
63
|
expect(await response.text()).toBe("");
|
|
63
64
|
});
|
|
64
65
|
|
|
66
|
+
test("buildFetchResponseFromNodeResponse does not throw on non-standard status codes", async () => {
|
|
67
|
+
const stream = new PassThrough() as PassThrough & {
|
|
68
|
+
statusCode?: number;
|
|
69
|
+
statusMessage?: string;
|
|
70
|
+
headers: IncomingHttpHeaders;
|
|
71
|
+
};
|
|
72
|
+
stream.statusCode = 999;
|
|
73
|
+
stream.statusMessage = "Request Denied";
|
|
74
|
+
stream.headers = { "content-type": "text/html; charset=utf-8" };
|
|
75
|
+
stream.end("blocked by anti-bot gateway");
|
|
76
|
+
|
|
77
|
+
const response = buildFetchResponseFromNodeResponse(stream);
|
|
78
|
+
// The Response object clamps to a constructable code, but the real upstream
|
|
79
|
+
// status is preserved for reporting.
|
|
80
|
+
expect(response.status).toBe(502);
|
|
81
|
+
expect(getUpstreamStatus(response)).toBe(999);
|
|
82
|
+
expect(response.ok).toBe(false);
|
|
83
|
+
expect(await response.text()).toBe("blocked by anti-bot gateway");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("surfaces a non-standard status as a tool error instead of crashing", async () => {
|
|
87
|
+
const result = await executeWithMockFetch(
|
|
88
|
+
{ url: "https://www.linkedin.com/jobs/view/123" },
|
|
89
|
+
{
|
|
90
|
+
requestExecutor: async () => {
|
|
91
|
+
const stream = new PassThrough() as PassThrough & {
|
|
92
|
+
statusCode?: number;
|
|
93
|
+
statusMessage?: string;
|
|
94
|
+
headers: IncomingHttpHeaders;
|
|
95
|
+
};
|
|
96
|
+
stream.statusCode = 999;
|
|
97
|
+
stream.statusMessage = "Request Denied";
|
|
98
|
+
stream.headers = { "content-type": "text/html; charset=utf-8" };
|
|
99
|
+
stream.end("<html><body>blocked</body></html>");
|
|
100
|
+
return buildFetchResponseFromNodeResponse(stream);
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
expect(result.isError).toBe(true);
|
|
106
|
+
expect(result.content).toContain("HTTP 999");
|
|
107
|
+
expect(result.activityMetadata?.webFetch?.status).toBe(999);
|
|
108
|
+
});
|
|
109
|
+
|
|
65
110
|
test("rejects missing url", async () => {
|
|
66
111
|
const result = await executeWithMockFetch({});
|
|
67
112
|
expect(result.isError).toBe(true);
|
|
@@ -20,13 +20,11 @@ import { mock } from "bun:test";
|
|
|
20
20
|
import type { AcpAgentConfig } from "../../../config/acp-schema.js";
|
|
21
21
|
|
|
22
22
|
export interface MockAcpConfig {
|
|
23
|
-
enabled: boolean;
|
|
24
23
|
maxConcurrentSessions: number;
|
|
25
24
|
agents: Record<string, AcpAgentConfig>;
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
const DEFAULT_CONFIG: MockAcpConfig = {
|
|
29
|
-
enabled: true,
|
|
30
28
|
maxConcurrentSessions: 4,
|
|
31
29
|
agents: {},
|
|
32
30
|
};
|
|
@@ -1,25 +1,19 @@
|
|
|
1
1
|
import { afterAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import { setOverridesForTesting } from "../__tests__/feature-flag-test-helpers.js";
|
|
4
3
|
import { installAcpConfigStub } from "./__tests__/helpers/acp-config-stub.js";
|
|
5
4
|
import { installWhichStub } from "./__tests__/helpers/which-stub.js";
|
|
6
|
-
import { ACP_FLAG_KEY } from "./feature-gate.js";
|
|
7
5
|
|
|
8
6
|
const config = await installAcpConfigStub();
|
|
9
7
|
const which = installWhichStub();
|
|
10
8
|
|
|
11
9
|
afterAll(() => {
|
|
12
10
|
which.restore();
|
|
13
|
-
setOverridesForTesting({});
|
|
14
11
|
});
|
|
15
12
|
|
|
16
13
|
const { resolveAcpAgent, listAcpAgents } = await import("./resolve-agent.js");
|
|
17
14
|
|
|
18
15
|
beforeEach(() => {
|
|
19
16
|
config.setConfig({});
|
|
20
|
-
// Default: no flag overrides, so the `acp` flag falls back to its registry
|
|
21
|
-
// default (false) and enablement comes from the config stub alone.
|
|
22
|
-
setOverridesForTesting({});
|
|
23
17
|
// Default: every command on PATH so binary preflight passes unless a test
|
|
24
18
|
// explicitly says otherwise.
|
|
25
19
|
which.setWhich((cmd) => `/usr/local/bin/${cmd}`);
|
|
@@ -30,32 +24,6 @@ beforeEach(() => {
|
|
|
30
24
|
// ---------------------------------------------------------------------------
|
|
31
25
|
|
|
32
26
|
describe("resolveAcpAgent", () => {
|
|
33
|
-
test("returns acp_disabled when both the feature flag and config.acp.enabled are off", () => {
|
|
34
|
-
config.setConfig({ enabled: false });
|
|
35
|
-
|
|
36
|
-
const result = resolveAcpAgent("claude");
|
|
37
|
-
|
|
38
|
-
expect(result.ok).toBe(false);
|
|
39
|
-
if (result.ok) return;
|
|
40
|
-
expect(result.reason).toBe("acp_disabled");
|
|
41
|
-
if (result.reason !== "acp_disabled") return;
|
|
42
|
-
expect(result.hint).toContain("ACP Coding Agents");
|
|
43
|
-
expect(result.hint).toContain("feature flag");
|
|
44
|
-
expect(result.hint).toContain("acp.enabled");
|
|
45
|
-
expect(result.hint).toContain("config.json");
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test("resolution proceeds when the acp feature flag is on and config.acp.enabled is false", () => {
|
|
49
|
-
config.setConfig({ enabled: false });
|
|
50
|
-
setOverridesForTesting({ [ACP_FLAG_KEY]: true });
|
|
51
|
-
|
|
52
|
-
const result = resolveAcpAgent("claude");
|
|
53
|
-
|
|
54
|
-
expect(result.ok).toBe(true);
|
|
55
|
-
if (!result.ok) return;
|
|
56
|
-
expect(result.agent.command).toBe("claude-agent-acp");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
27
|
test("user config wins over default profile", () => {
|
|
60
28
|
config.setConfig({
|
|
61
29
|
agents: {
|
|
@@ -359,35 +327,11 @@ describe("resolveAcpAgent - missing binary", () => {
|
|
|
359
327
|
// ---------------------------------------------------------------------------
|
|
360
328
|
|
|
361
329
|
describe("listAcpAgents", () => {
|
|
362
|
-
test("returns enabled: false with empty agents when both the flag and config are off", () => {
|
|
363
|
-
config.setConfig({ enabled: false });
|
|
364
|
-
|
|
365
|
-
const result = listAcpAgents();
|
|
366
|
-
|
|
367
|
-
expect(result.enabled).toBe(false);
|
|
368
|
-
expect(result.agents).toEqual([]);
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
test("returns the catalog when the acp feature flag is on and config.acp.enabled is false", () => {
|
|
372
|
-
config.setConfig({ enabled: false });
|
|
373
|
-
setOverridesForTesting({ [ACP_FLAG_KEY]: true });
|
|
374
|
-
|
|
375
|
-
const result = listAcpAgents();
|
|
376
|
-
|
|
377
|
-
expect(result.enabled).toBe(true);
|
|
378
|
-
expect(result.agents.map((a) => a.id)).toEqual([
|
|
379
|
-
"claude",
|
|
380
|
-
"codex",
|
|
381
|
-
"gemini",
|
|
382
|
-
]);
|
|
383
|
-
});
|
|
384
|
-
|
|
385
330
|
test("includes all bundled defaults when user config is empty", () => {
|
|
386
331
|
config.setConfig({ agents: {} });
|
|
387
332
|
|
|
388
333
|
const result = listAcpAgents();
|
|
389
334
|
|
|
390
|
-
expect(result.enabled).toBe(true);
|
|
391
335
|
const ids = result.agents.map((a) => a.id);
|
|
392
336
|
expect(ids).toEqual(["claude", "codex", "gemini"]);
|
|
393
337
|
for (const entry of result.agents) {
|
package/src/acp/resolve-agent.ts
CHANGED
|
@@ -3,15 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `resolveAcpAgent(id)` merges user-provided `config.acp.agents[id]` (wins on
|
|
5
5
|
* overlap) with the bundled `DEFAULT_ACP_AGENT_PROFILES` so common agents like
|
|
6
|
-
* `claude` and `codex` Just Work
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* acp_list_agents, and the `/v1/acp/spawn` HTTP route) get a single source
|
|
14
|
-
* of truth and matching actionable hints.
|
|
6
|
+
* `claude` and `codex` Just Work with no per-user config required. Natural
|
|
7
|
+
* names ("claude code", "Gemini CLI") resolve via `AGENT_ID_ALIASES` when the
|
|
8
|
+
* raw id misses both maps. The result is a discriminated union covering every
|
|
9
|
+
* reason a spawn might fail before we even start the agent process: unknown
|
|
10
|
+
* agent id, or binary missing from PATH. Callers (acp_spawn, acp_list_agents,
|
|
11
|
+
* and the `/v1/acp/spawn` HTTP route) get a single source of truth and
|
|
12
|
+
* matching actionable hints.
|
|
15
13
|
*
|
|
16
14
|
* The resolver NEVER fetches or runs packages in the (untrusted) task cwd.
|
|
17
15
|
* When the adapter binary is missing, resolution simply fails with
|
|
@@ -33,7 +31,6 @@ import {
|
|
|
33
31
|
} from "../config/acp-defaults.js";
|
|
34
32
|
import type { AcpAgentConfig } from "../config/acp-schema.js";
|
|
35
33
|
import { getConfig } from "../config/loader.js";
|
|
36
|
-
import { isAcpEnabled } from "./feature-gate.js";
|
|
37
34
|
|
|
38
35
|
/**
|
|
39
36
|
* Whether this agent's entry came from user config (wins over default) or
|
|
@@ -47,7 +44,6 @@ export type ResolveAcpAgentResult =
|
|
|
47
44
|
| ResolveAcpAgentFailure;
|
|
48
45
|
|
|
49
46
|
export type ResolveAcpAgentFailure =
|
|
50
|
-
| { ok: false; reason: "acp_disabled"; hint: string }
|
|
51
47
|
| { ok: false; reason: "unknown_agent"; available: string[] }
|
|
52
48
|
| {
|
|
53
49
|
ok: false;
|
|
@@ -68,8 +64,6 @@ export function formatResolveFailure(
|
|
|
68
64
|
failure: ResolveAcpAgentFailure,
|
|
69
65
|
): string {
|
|
70
66
|
switch (failure.reason) {
|
|
71
|
-
case "acp_disabled":
|
|
72
|
-
return failure.hint;
|
|
73
67
|
case "unknown_agent":
|
|
74
68
|
return `Unknown agent "${agentId}". Available: ${failure.available.join(", ")}.`;
|
|
75
69
|
case "binary_not_found":
|
|
@@ -93,14 +87,6 @@ interface AcpAgentEntry {
|
|
|
93
87
|
setupHint?: string;
|
|
94
88
|
}
|
|
95
89
|
|
|
96
|
-
/**
|
|
97
|
-
* Single-source-of-truth hint for "ACP is disabled". Exported so any caller
|
|
98
|
-
* that surfaces a disabled-state message (resolver, list-agents tool) reads
|
|
99
|
-
* the same string instead of duplicating near-identical copy.
|
|
100
|
-
*/
|
|
101
|
-
export const ACP_DISABLED_HINT =
|
|
102
|
-
"Enable the \"ACP Coding Agents\" feature flag in the client's feature flags UI (or set 'acp.enabled': true in ~/.vellum/workspace/config.json).";
|
|
103
|
-
|
|
104
90
|
function installHintFor(command: string): string {
|
|
105
91
|
const pkg = DEFAULT_AGENT_NPM_PACKAGES[command];
|
|
106
92
|
return pkg
|
|
@@ -209,9 +195,8 @@ function mergedAgentIds(userAgents: Record<string, AcpAgentConfig>): string[] {
|
|
|
209
195
|
* Resolve an ACP agent id to its config + binary preflight result.
|
|
210
196
|
*
|
|
211
197
|
* Order of checks:
|
|
212
|
-
* 1.
|
|
213
|
-
* 2. The
|
|
214
|
-
* 3. The agent must be runnable: its `command` on PATH (see
|
|
198
|
+
* 1. The id must resolve to an agent (user config wins; falls back to defaults).
|
|
199
|
+
* 2. The agent must be runnable: its `command` on PATH (see
|
|
215
200
|
* `resolveRunnableAgent`).
|
|
216
201
|
*
|
|
217
202
|
* Each failure mode carries an actionable hint so callers can surface a
|
|
@@ -219,10 +204,6 @@ function mergedAgentIds(userAgents: Record<string, AcpAgentConfig>): string[] {
|
|
|
219
204
|
*/
|
|
220
205
|
export function resolveAcpAgent(id: string): ResolveAcpAgentResult {
|
|
221
206
|
const config = getConfig();
|
|
222
|
-
if (!isAcpEnabled(config)) {
|
|
223
|
-
return { ok: false, reason: "acp_disabled", hint: ACP_DISABLED_HINT };
|
|
224
|
-
}
|
|
225
|
-
|
|
226
207
|
const userAgents = config.acp.agents;
|
|
227
208
|
const found = lookupAgent(userAgents, id);
|
|
228
209
|
if (!found) {
|
|
@@ -252,20 +233,11 @@ export function resolveAcpAgent(id: string): ResolveAcpAgentResult {
|
|
|
252
233
|
* plus any user-only entries — with per-entry availability info. Used by the
|
|
253
234
|
* `acp_list_agents` tool to render setup steps when an agent's binary isn't
|
|
254
235
|
* installed yet.
|
|
255
|
-
*
|
|
256
|
-
* `enabled: false` short-circuits and returns an empty catalog so the tool
|
|
257
|
-
* can render a single "ACP is disabled" hint instead of advertising agents
|
|
258
|
-
* the user can't actually run.
|
|
259
236
|
*/
|
|
260
237
|
export function listAcpAgents(): {
|
|
261
|
-
enabled: boolean;
|
|
262
238
|
agents: AcpAgentEntry[];
|
|
263
239
|
} {
|
|
264
240
|
const config = getConfig();
|
|
265
|
-
if (!isAcpEnabled(config)) {
|
|
266
|
-
return { enabled: false, agents: [] };
|
|
267
|
-
}
|
|
268
|
-
|
|
269
241
|
const userAgents = config.acp.agents;
|
|
270
242
|
const agents: AcpAgentEntry[] = mergedAgentIds(userAgents).map((id) => {
|
|
271
243
|
// Non-null: ids come from `mergedAgentIds` so the lookup always resolves.
|
|
@@ -288,5 +260,5 @@ export function listAcpAgents(): {
|
|
|
288
260
|
return entry;
|
|
289
261
|
});
|
|
290
262
|
|
|
291
|
-
return {
|
|
263
|
+
return { agents };
|
|
292
264
|
}
|
package/src/agent/loop.ts
CHANGED
|
@@ -10,27 +10,22 @@ import {
|
|
|
10
10
|
estimateToolsTokens,
|
|
11
11
|
getCalibrationProviderKey,
|
|
12
12
|
} from "../context/token-estimator.js";
|
|
13
|
-
import type { ContextWindowResult } from "../context/window-manager.js";
|
|
14
13
|
import type { InboundActorContext } from "../daemon/conversation-runtime-assembly.js";
|
|
15
14
|
import type { ToolActivityMetadata } from "../daemon/message-types/web-activity.js";
|
|
16
15
|
import type { TrustContext } from "../daemon/trust-context.js";
|
|
17
16
|
import { stripHistoricalWebSearchResults } from "../daemon/web-search-history.js";
|
|
18
17
|
import { HOOKS } from "../plugin-api/constants.js";
|
|
19
18
|
import type {
|
|
20
|
-
|
|
19
|
+
PostModelCallContext,
|
|
21
20
|
PostToolUseContext,
|
|
22
21
|
PreModelCallContext,
|
|
23
22
|
StopContext,
|
|
24
23
|
} from "../plugin-api/types.js";
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
defaultCompact,
|
|
28
|
-
} from "../plugins/defaults/compaction/compact.js";
|
|
29
|
-
import { getContextWindowManager } from "../plugins/defaults/compaction/manager-store.js";
|
|
24
|
+
import { defaultCompact } from "../plugins/defaults/compaction/compact.js";
|
|
25
|
+
import type { ContextWindowResult } from "../plugins/defaults/compaction/window-manager.js";
|
|
30
26
|
import postCompact from "../plugins/defaults/memory-retrieval/hooks/post-compact.js";
|
|
31
27
|
import { runHook } from "../plugins/pipeline.js";
|
|
32
28
|
import type { CompactionCircuitEvent } from "../plugins/types.js";
|
|
33
|
-
import { PluginExecutionError } from "../plugins/types.js";
|
|
34
29
|
import { normalizeThinkingConfigForWire } from "../providers/thinking-config.js";
|
|
35
30
|
import type {
|
|
36
31
|
ContentBlock,
|
|
@@ -730,17 +725,8 @@ export class AgentLoop {
|
|
|
730
725
|
// Record the history-stripped marker right after stripping, before the
|
|
731
726
|
// pipeline runs.
|
|
732
727
|
await onEvent({ type: "history_stripped" });
|
|
733
|
-
// The compaction module owns the per-conversation manager;
|
|
734
|
-
//
|
|
735
|
-
// without a compaction path (agent wakes, standalone unit tests), which
|
|
736
|
-
// never reach this gate.
|
|
737
|
-
const manager = getContextWindowManager(this.conversationId);
|
|
738
|
-
if (manager == null) {
|
|
739
|
-
throw new PluginExecutionError(
|
|
740
|
-
`default-compaction: no ContextWindowManager registered for conversation ${this.conversationId} — the compaction store must construct one before compaction runs`,
|
|
741
|
-
DEFAULT_COMPACTION_PLUGIN_NAME,
|
|
742
|
-
);
|
|
743
|
-
}
|
|
728
|
+
// The compaction module owns the per-conversation manager; pass the
|
|
729
|
+
// conversation id and let `defaultCompact` resolve it from the store.
|
|
744
730
|
// The mid-loop budget gate is reached only when this turn decides to
|
|
745
731
|
// compact in place, so `force` past the auto-threshold check.
|
|
746
732
|
// `actorTrustClass` comes from the turn's trust snapshot (the actor whose
|
|
@@ -748,7 +734,7 @@ export class AgentLoop {
|
|
|
748
734
|
// guardian-only attachments for untrusted actors. `overrideProfile` is the
|
|
749
735
|
// turn's resolved inference-profile override for the summary call.
|
|
750
736
|
const compactResult = await defaultCompact({
|
|
751
|
-
|
|
737
|
+
conversationId: this.conversationId,
|
|
752
738
|
messages: rawHistory,
|
|
753
739
|
signal,
|
|
754
740
|
force: true,
|
|
@@ -1114,7 +1100,7 @@ export class AgentLoop {
|
|
|
1114
1100
|
|
|
1115
1101
|
// A `pre-model-call` hook (below) can defer this turn's assistant
|
|
1116
1102
|
// output; when set, the live text stream is held so an
|
|
1117
|
-
// `
|
|
1103
|
+
// `post-model-call` hook can emit the finalized (transformed) text
|
|
1118
1104
|
// instead. Reset per model call.
|
|
1119
1105
|
let deferAssistantOutput = false;
|
|
1120
1106
|
|
|
@@ -1128,7 +1114,7 @@ export class AgentLoop {
|
|
|
1128
1114
|
onEvent: (event) => {
|
|
1129
1115
|
if (event.type === "text_delta") {
|
|
1130
1116
|
// Held when the turn's output is deferred — the final text is
|
|
1131
|
-
// emitted once, after the `
|
|
1117
|
+
// emitted once, after the `post-model-call` hook runs.
|
|
1132
1118
|
if (deferAssistantOutput) return;
|
|
1133
1119
|
// Apply sensitive-output placeholder substitution (chunk-safe)
|
|
1134
1120
|
if (substitutionMap.size > 0) {
|
|
@@ -1298,7 +1284,7 @@ export class AgentLoop {
|
|
|
1298
1284
|
streamingPending = "";
|
|
1299
1285
|
}
|
|
1300
1286
|
|
|
1301
|
-
// Run the `
|
|
1287
|
+
// Run the `post-model-call` hook on a finalized message and, when
|
|
1302
1288
|
// output was deferred, emit the finalized text once (with sensitive-output
|
|
1303
1289
|
// substitution applied, matching the live stream). Fail-open: the hook
|
|
1304
1290
|
// receives a clone, so a throw — even mid in-place mutation — leaves the
|
|
@@ -1308,19 +1294,19 @@ export class AgentLoop {
|
|
|
1308
1294
|
): Promise<Message> => {
|
|
1309
1295
|
let finalized = message;
|
|
1310
1296
|
try {
|
|
1311
|
-
const ctx:
|
|
1297
|
+
const ctx: PostModelCallContext = {
|
|
1312
1298
|
conversationId: this.conversationId,
|
|
1313
1299
|
callSite,
|
|
1314
1300
|
content: structuredClone(message.content),
|
|
1315
1301
|
stopReason: response.stopReason,
|
|
1316
1302
|
logger: rlog,
|
|
1317
1303
|
};
|
|
1318
|
-
const result = await runHook(HOOKS.
|
|
1304
|
+
const result = await runHook(HOOKS.POST_MODEL_CALL, ctx);
|
|
1319
1305
|
finalized = { role: "assistant", content: result.content };
|
|
1320
1306
|
} catch (assistantMessageError) {
|
|
1321
1307
|
rlog.error(
|
|
1322
1308
|
{ err: assistantMessageError },
|
|
1323
|
-
"
|
|
1309
|
+
"post-model-call hook failed — keeping the original content",
|
|
1324
1310
|
);
|
|
1325
1311
|
finalized = message;
|
|
1326
1312
|
}
|
|
@@ -1460,7 +1446,7 @@ export class AgentLoop {
|
|
|
1460
1446
|
}
|
|
1461
1447
|
}
|
|
1462
1448
|
|
|
1463
|
-
// Run the `
|
|
1449
|
+
// Run the `post-model-call` hook + emit any deferred final text.
|
|
1464
1450
|
// On a no-tool turn this point is reached only after the `stop` hook
|
|
1465
1451
|
// resolves to "stop" (a `continue` already re-queried above), so a
|
|
1466
1452
|
// re-queried reply is never transformed-then-discarded.
|