@vellumai/assistant 0.10.7-dev.202607100720.fba456e → 0.10.7-dev.202607101106.c87f4e3
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/openapi.yaml +0 -4
- package/package.json +1 -1
- package/src/__tests__/compaction-trail-store.test.ts +2 -1
- package/src/__tests__/conversation-app-control-instantiation.test.ts +20 -3
- package/src/__tests__/conversation-runtime-assembly.test.ts +2 -0
- package/src/__tests__/conversation-skill-tools.test.ts +31 -6
- package/src/__tests__/list-messages-queued.test.ts +9 -3
- package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +9 -9
- package/src/__tests__/llm-request-log-call-site.test.ts +5 -5
- package/src/__tests__/llm-request-log-disabled-writes.test.ts +91 -0
- package/src/__tests__/media-generate-image.test.ts +20 -7
- package/src/__tests__/platform-bash-auto-approve.test.ts +19 -2
- package/src/__tests__/registry.test.ts +18 -0
- package/src/__tests__/require-fresh-approval.test.ts +15 -0
- package/src/__tests__/skill-projection-feature-flag.test.ts +35 -8
- package/src/__tests__/skill-projection.benchmark.test.ts +7 -2
- package/src/__tests__/subagent-detail.test.ts +1 -0
- package/src/__tests__/summarize-boundary.test.ts +1 -0
- package/src/__tests__/tool-approval-handler.test.ts +1 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +21 -17
- package/src/__tests__/tool-executor.test.ts +36 -18
- package/src/__tests__/tool-grant-request-escalation.test.ts +1 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -0
- package/src/__tests__/verification-control-plane-policy.test.ts +13 -0
- package/src/api/responses/conversation-message.ts +0 -15
- package/src/config/schemas/__tests__/llm-request-logs.test.ts +33 -5
- package/src/config/schemas/llm-request-logs.ts +78 -27
- package/src/daemon/conversation-skill-tools.ts +27 -9
- package/src/export/__tests__/transcript-formatter.test.ts +5 -0
- package/src/permissions/checker.test.ts +3 -6
- package/src/permissions/checker.ts +2 -2
- package/src/persistence/__tests__/message-content-file.test.ts +150 -0
- package/src/persistence/__tests__/slow-query-log.test.ts +125 -0
- package/src/persistence/conversation-crud.ts +11 -1
- package/src/persistence/llm-request-log-store.ts +36 -2
- package/src/persistence/message-content-file.ts +190 -0
- package/src/persistence/migrations/324-message-finalized-column.ts +26 -0
- package/src/persistence/migrations/__tests__/297-move-llm-request-logs.test.ts +2 -2
- package/src/persistence/schema/conversations.ts +15 -0
- package/src/persistence/slow-query-log.ts +58 -21
- package/src/persistence/steps.ts +2 -0
- package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +31 -2
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +67 -1
- package/src/runtime/routes/conversation-compaction-routes.ts +5 -0
- package/src/runtime/routes/conversation-query-routes.ts +5 -0
- package/src/runtime/routes/conversation-routes.ts +2 -8
- package/src/runtime/routes/errors.ts +17 -0
- package/src/runtime/routes/llm-request-logs-access.ts +19 -0
- package/src/tasks/tool-sanitizer.ts +3 -1
- package/src/telemetry/turn-trace-store.test.ts +4 -0
- package/src/telemetry/turn-trace-store.ts +1 -4
- package/src/tools/registry.ts +89 -11
- package/src/tools/run-standalone.ts +5 -13
- package/src/tools/workspace-tools/loader.ts +24 -8
- package/src/util/sqlite-query-label.ts +51 -0
- package/src/util/sqlite-retry.ts +14 -2
- package/src/workflows/capabilities.ts +12 -4
package/openapi.yaml
CHANGED
|
@@ -17727,10 +17727,6 @@ paths:
|
|
|
17727
17727
|
enum:
|
|
17728
17728
|
- user
|
|
17729
17729
|
- assistant
|
|
17730
|
-
content:
|
|
17731
|
-
type: string
|
|
17732
|
-
deprecated: true
|
|
17733
|
-
description: "Deprecated: superseded by contentBlocks. Flat plain-text body (joined text segments)."
|
|
17734
17730
|
timestamp:
|
|
17735
17731
|
type: string
|
|
17736
17732
|
attachments:
|
package/package.json
CHANGED
|
@@ -60,6 +60,7 @@ function insertLogAt(
|
|
|
60
60
|
callSite: "mainAgent" | "compactionAgent" | null,
|
|
61
61
|
requestPayload = "{}",
|
|
62
62
|
): string {
|
|
63
|
+
// Logging is enabled in these tests, so the write always returns an id.
|
|
63
64
|
const id = recordRequestLog(
|
|
64
65
|
conversationId,
|
|
65
66
|
requestPayload,
|
|
@@ -67,7 +68,7 @@ function insertLogAt(
|
|
|
67
68
|
undefined,
|
|
68
69
|
"anthropic",
|
|
69
70
|
callSite ?? undefined,
|
|
70
|
-
)
|
|
71
|
+
)!;
|
|
71
72
|
// Use the Drizzle update builder rather than `db.run("UPDATE … ?")` —
|
|
72
73
|
// the drizzle wrapper doesn't accept positional parameters the same
|
|
73
74
|
// way `bun:sqlite` does, and a silent no-op there manifests as zero
|
|
@@ -51,7 +51,9 @@ mock.module("../util/logger.js", () => ({
|
|
|
51
51
|
let appControlFlagEnabled = false;
|
|
52
52
|
mock.module("../config/assistant-feature-flags.js", () => ({
|
|
53
53
|
isAssistantFeatureFlagEnabled: (key: string) => {
|
|
54
|
-
if (key === "app-control")
|
|
54
|
+
if (key === "app-control") {
|
|
55
|
+
return appControlFlagEnabled;
|
|
56
|
+
}
|
|
55
57
|
return true;
|
|
56
58
|
},
|
|
57
59
|
loadDefaultsRegistry: () => ({}),
|
|
@@ -143,7 +145,20 @@ mock.module("../tools/registry.js", () => ({
|
|
|
143
145
|
let found: Tool | undefined;
|
|
144
146
|
for (const tools of mockRegisteredTools.values()) {
|
|
145
147
|
for (const tool of tools) {
|
|
146
|
-
if (tool.name === name)
|
|
148
|
+
if (tool.name === name) {
|
|
149
|
+
found = tool;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return found;
|
|
154
|
+
},
|
|
155
|
+
resolveTool: (name: string) => {
|
|
156
|
+
let found: Tool | undefined;
|
|
157
|
+
for (const tools of mockRegisteredTools.values()) {
|
|
158
|
+
for (const tool of tools) {
|
|
159
|
+
if (tool.name === name) {
|
|
160
|
+
found = tool;
|
|
161
|
+
}
|
|
147
162
|
}
|
|
148
163
|
}
|
|
149
164
|
return found;
|
|
@@ -158,7 +173,9 @@ mock.module("../tools/registry.js", () => ({
|
|
|
158
173
|
let ownerSkillId: string | undefined;
|
|
159
174
|
for (const [skillId, tools] of mockRegisteredTools.entries()) {
|
|
160
175
|
for (const tool of tools) {
|
|
161
|
-
if (tool.name === name)
|
|
176
|
+
if (tool.name === name) {
|
|
177
|
+
ownerSkillId = skillId;
|
|
178
|
+
}
|
|
162
179
|
}
|
|
163
180
|
}
|
|
164
181
|
return ownerSkillId === undefined
|
|
@@ -3220,6 +3220,7 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
3220
3220
|
createdAt: opts.createdAt,
|
|
3221
3221
|
metadata: Object.keys(outer).length > 0 ? JSON.stringify(outer) : null,
|
|
3222
3222
|
clientMessageId: null,
|
|
3223
|
+
finalized: 1,
|
|
3223
3224
|
};
|
|
3224
3225
|
}
|
|
3225
3226
|
|
|
@@ -3241,6 +3242,7 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
3241
3242
|
createdAt: opts.createdAt,
|
|
3242
3243
|
metadata: Object.keys(outer).length > 0 ? JSON.stringify(outer) : null,
|
|
3243
3244
|
clientMessageId: null,
|
|
3245
|
+
finalized: 1,
|
|
3244
3246
|
};
|
|
3245
3247
|
}
|
|
3246
3248
|
|
|
@@ -57,7 +57,9 @@ mock.module("../config/skills.js", () => ({
|
|
|
57
57
|
? skills
|
|
58
58
|
: skills.filter((skill) => {
|
|
59
59
|
const owner = skill.owner;
|
|
60
|
-
if (owner?.kind !== "plugin")
|
|
60
|
+
if (owner?.kind !== "plugin") {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
61
63
|
return effectiveEnabledPluginSet.has(owner.id);
|
|
62
64
|
}),
|
|
63
65
|
}));
|
|
@@ -82,10 +84,16 @@ mock.module("../skills/active-skill-tools.js", () => {
|
|
|
82
84
|
const entries: Array<{ id: string; version?: string }> = [];
|
|
83
85
|
for (const msg of messages) {
|
|
84
86
|
for (const block of msg.content) {
|
|
85
|
-
if (block.type !== "tool_result")
|
|
86
|
-
|
|
87
|
+
if (block.type !== "tool_result") {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (!skillLoadUseIds.has(block.tool_use_id)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
87
93
|
const text = block.content;
|
|
88
|
-
if (!text)
|
|
94
|
+
if (!text) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
89
97
|
for (const match of text.matchAll(re)) {
|
|
90
98
|
if (!seen.has(match[1])) {
|
|
91
99
|
seen.add(match[1]);
|
|
@@ -170,7 +178,22 @@ mock.module("../tools/registry.js", () => ({
|
|
|
170
178
|
let found: Tool | undefined;
|
|
171
179
|
for (const tools of mockRegisteredTools.values()) {
|
|
172
180
|
for (const tool of tools) {
|
|
173
|
-
if (tool.name === name)
|
|
181
|
+
if (tool.name === name) {
|
|
182
|
+
found = tool;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return found;
|
|
187
|
+
},
|
|
188
|
+
// Sync peek mirrors getTool — the skill-tool projection path reads via
|
|
189
|
+
// resolveTool synchronously.
|
|
190
|
+
resolveTool: (name: string): Tool | undefined => {
|
|
191
|
+
let found: Tool | undefined;
|
|
192
|
+
for (const tools of mockRegisteredTools.values()) {
|
|
193
|
+
for (const tool of tools) {
|
|
194
|
+
if (tool.name === name) {
|
|
195
|
+
found = tool;
|
|
196
|
+
}
|
|
174
197
|
}
|
|
175
198
|
}
|
|
176
199
|
return found;
|
|
@@ -184,7 +207,9 @@ mock.module("../tools/registry.js", () => ({
|
|
|
184
207
|
let ownerSkillId: string | undefined;
|
|
185
208
|
for (const [skillId, tools] of mockRegisteredTools.entries()) {
|
|
186
209
|
for (const tool of tools) {
|
|
187
|
-
if (tool.name === name)
|
|
210
|
+
if (tool.name === name) {
|
|
211
|
+
ownerSkillId = skillId;
|
|
212
|
+
}
|
|
188
213
|
}
|
|
189
214
|
}
|
|
190
215
|
return ownerSkillId === undefined
|
|
@@ -54,7 +54,7 @@ function resetTables() {
|
|
|
54
54
|
interface MessagePayload {
|
|
55
55
|
id: string;
|
|
56
56
|
role: string;
|
|
57
|
-
|
|
57
|
+
contentBlocks?: Array<{ type: string; text?: string }>;
|
|
58
58
|
clientMessageId?: string;
|
|
59
59
|
queueStatus?: "queued" | "processing";
|
|
60
60
|
queuePosition?: number;
|
|
@@ -123,7 +123,10 @@ describe("handleListMessages in-memory queue", () => {
|
|
|
123
123
|
expect(first.queuePosition).toBe(1);
|
|
124
124
|
expect(first.role).toBe("user");
|
|
125
125
|
expect(first.id).toBe("req-queued-1");
|
|
126
|
-
expect(first.
|
|
126
|
+
expect(first.contentBlocks?.[0]).toEqual({
|
|
127
|
+
type: "text",
|
|
128
|
+
text: "please also do this",
|
|
129
|
+
});
|
|
127
130
|
expect(first.clientMessageId).toBe("nonce-queued");
|
|
128
131
|
|
|
129
132
|
const second = body.messages[2];
|
|
@@ -148,7 +151,10 @@ describe("handleListMessages in-memory queue", () => {
|
|
|
148
151
|
const body = response as { messages: MessagePayload[] };
|
|
149
152
|
|
|
150
153
|
expect(body.messages).toHaveLength(1);
|
|
151
|
-
expect(body.messages[0].
|
|
154
|
+
expect(body.messages[0].contentBlocks?.[0]).toEqual({
|
|
155
|
+
type: "text",
|
|
156
|
+
text: "what the user actually typed",
|
|
157
|
+
});
|
|
152
158
|
});
|
|
153
159
|
|
|
154
160
|
test("renders queued attachments with a derived kind", async () => {
|
|
@@ -46,7 +46,7 @@ describe("setAgentLoopExitReasonOnLatestLog", () => {
|
|
|
46
46
|
|
|
47
47
|
test("recordRequestLog leaves agentLoopExitReason NULL", () => {
|
|
48
48
|
const id = recordRequestLog("conv-1", '{"req":1}', '{"res":1}');
|
|
49
|
-
const row = getRequestLogById(id);
|
|
49
|
+
const row = getRequestLogById(id!);
|
|
50
50
|
expect(row).not.toBeNull();
|
|
51
51
|
expect(row!.agentLoopExitReason).toBeNull();
|
|
52
52
|
});
|
|
@@ -61,8 +61,8 @@ describe("setAgentLoopExitReasonOnLatestLog", () => {
|
|
|
61
61
|
|
|
62
62
|
setAgentLoopExitReasonOnLatestLog("conv-1", "no_tool_calls");
|
|
63
63
|
|
|
64
|
-
expect(getRequestLogById(first)?.agentLoopExitReason).toBeNull();
|
|
65
|
-
expect(getRequestLogById(second)?.agentLoopExitReason).toBe(
|
|
64
|
+
expect(getRequestLogById(first!)?.agentLoopExitReason).toBeNull();
|
|
65
|
+
expect(getRequestLogById(second!)?.agentLoopExitReason).toBe(
|
|
66
66
|
"no_tool_calls",
|
|
67
67
|
);
|
|
68
68
|
});
|
|
@@ -74,10 +74,10 @@ describe("setAgentLoopExitReasonOnLatestLog", () => {
|
|
|
74
74
|
|
|
75
75
|
setAgentLoopExitReasonOnLatestLog("conv-a", "yield_to_user");
|
|
76
76
|
|
|
77
|
-
expect(getRequestLogById(a)?.agentLoopExitReason).toBe("yield_to_user");
|
|
77
|
+
expect(getRequestLogById(a!)?.agentLoopExitReason).toBe("yield_to_user");
|
|
78
78
|
// conv-b is later overall but belongs to a different conversation —
|
|
79
79
|
// must stay NULL.
|
|
80
|
-
expect(getRequestLogById(b)?.agentLoopExitReason).toBeNull();
|
|
80
|
+
expect(getRequestLogById(b!)?.agentLoopExitReason).toBeNull();
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
test("no-op when conversation has no logs", () => {
|
|
@@ -90,12 +90,12 @@ describe("setAgentLoopExitReasonOnLatestLog", () => {
|
|
|
90
90
|
// Previous run: completes, lands a log, gets stamped.
|
|
91
91
|
const prev = recordRequestLog("conv-1", '{"prev_req":1}', '{"prev_res":1}');
|
|
92
92
|
setAgentLoopExitReasonOnLatestLog("conv-1", "no_tool_calls");
|
|
93
|
-
expect(getRequestLogById(prev)?.agentLoopExitReason).toBe("no_tool_calls");
|
|
93
|
+
expect(getRequestLogById(prev!)?.agentLoopExitReason).toBe("no_tool_calls");
|
|
94
94
|
|
|
95
95
|
// Current run aborts pre-call (or similar) before any LLM call lands.
|
|
96
96
|
// The helper must NOT overwrite the previous run's row.
|
|
97
97
|
setAgentLoopExitReasonOnLatestLog("conv-1", "aborted_pre_call");
|
|
98
|
-
expect(getRequestLogById(prev)?.agentLoopExitReason).toBe("no_tool_calls");
|
|
98
|
+
expect(getRequestLogById(prev!)?.agentLoopExitReason).toBe("no_tool_calls");
|
|
99
99
|
});
|
|
100
100
|
|
|
101
101
|
test("stamps the current run's newest row even when a prior row is already stamped", () => {
|
|
@@ -112,8 +112,8 @@ describe("setAgentLoopExitReasonOnLatestLog", () => {
|
|
|
112
112
|
);
|
|
113
113
|
setAgentLoopExitReasonOnLatestLog("conv-1", "yield_to_user");
|
|
114
114
|
|
|
115
|
-
expect(getRequestLogById(prev)?.agentLoopExitReason).toBe("no_tool_calls");
|
|
116
|
-
expect(getRequestLogById(current)?.agentLoopExitReason).toBe(
|
|
115
|
+
expect(getRequestLogById(prev!)?.agentLoopExitReason).toBe("no_tool_calls");
|
|
116
|
+
expect(getRequestLogById(current!)?.agentLoopExitReason).toBe(
|
|
117
117
|
"yield_to_user",
|
|
118
118
|
);
|
|
119
119
|
});
|
|
@@ -52,14 +52,14 @@ describe("recordRequestLog call_site stamping", () => {
|
|
|
52
52
|
"anthropic",
|
|
53
53
|
"mainAgent",
|
|
54
54
|
);
|
|
55
|
-
const row = getRequestLogById(id);
|
|
55
|
+
const row = getRequestLogById(id!);
|
|
56
56
|
expect(row).not.toBeNull();
|
|
57
57
|
expect(row!.callSite).toBe("mainAgent");
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
test("leaves callSite NULL when omitted (backward compat)", () => {
|
|
61
61
|
const id = recordRequestLog("conv-1", '{"req":1}', '{"res":1}');
|
|
62
|
-
const row = getRequestLogById(id);
|
|
62
|
+
const row = getRequestLogById(id!);
|
|
63
63
|
expect(row).not.toBeNull();
|
|
64
64
|
expect(row!.callSite).toBeNull();
|
|
65
65
|
});
|
|
@@ -73,7 +73,7 @@ describe("recordRequestLog call_site stamping", () => {
|
|
|
73
73
|
"anthropic",
|
|
74
74
|
"compactionAgent",
|
|
75
75
|
);
|
|
76
|
-
expect(getRequestLogById(id)?.callSite).toBe("compactionAgent");
|
|
76
|
+
expect(getRequestLogById(id!)?.callSite).toBe("compactionAgent");
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
test("two rows in the same conversation can carry different callSites", () => {
|
|
@@ -93,8 +93,8 @@ describe("recordRequestLog call_site stamping", () => {
|
|
|
93
93
|
"anthropic",
|
|
94
94
|
"compactionAgent",
|
|
95
95
|
);
|
|
96
|
-
expect(getRequestLogById(mainId)?.callSite).toBe("mainAgent");
|
|
97
|
-
expect(getRequestLogById(compactId)?.callSite).toBe("compactionAgent");
|
|
96
|
+
expect(getRequestLogById(mainId!)?.callSite).toBe("mainAgent");
|
|
97
|
+
expect(getRequestLogById(compactId!)?.callSite).toBe("compactionAgent");
|
|
98
98
|
});
|
|
99
99
|
});
|
|
100
100
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests the master switch for LLM request logging (`llmRequestLogs.enabled`).
|
|
3
|
+
* When logging is off, the store's insert paths (`recordRequestLog`,
|
|
4
|
+
* `recordSyntheticAgentErrorMessageLog`) must skip the write entirely — no
|
|
5
|
+
* prompt/completion payload lands on disk — and return `null`. The read-side
|
|
6
|
+
* 4xx is exercised separately at the route layer.
|
|
7
|
+
*/
|
|
8
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
9
|
+
|
|
10
|
+
mock.module("../util/logger.js", () => ({
|
|
11
|
+
getLogger: () =>
|
|
12
|
+
new Proxy({} as Record<string, unknown>, {
|
|
13
|
+
get: () => () => {},
|
|
14
|
+
}),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// Mutable so each test toggles the flag the store reads via `getConfigReadOnly`.
|
|
18
|
+
let enabled = true;
|
|
19
|
+
mock.module("../config/loader.js", () => ({
|
|
20
|
+
getConfig: () => ({
|
|
21
|
+
llmRequestLogs: { readSource: "local", enabled },
|
|
22
|
+
}),
|
|
23
|
+
getConfigReadOnly: () => ({
|
|
24
|
+
llmRequestLogs: { readSource: "local", enabled },
|
|
25
|
+
}),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
// `mock.module()` persists process-wide; reset so other files don't inherit a
|
|
29
|
+
// stale value.
|
|
30
|
+
afterAll(() => {
|
|
31
|
+
enabled = true;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
import { getLogsDb } from "../persistence/db-connection.js";
|
|
35
|
+
import { initializeDb } from "../persistence/db-init.js";
|
|
36
|
+
import {
|
|
37
|
+
getRequestLogById,
|
|
38
|
+
getRequestLogsByConversationId,
|
|
39
|
+
recordRequestLog,
|
|
40
|
+
recordSyntheticAgentErrorMessageLog,
|
|
41
|
+
} from "../persistence/llm-request-log-store.js";
|
|
42
|
+
import { llmRequestLogs } from "../persistence/schema/index.js";
|
|
43
|
+
|
|
44
|
+
await initializeDb();
|
|
45
|
+
|
|
46
|
+
function resetLogs(): void {
|
|
47
|
+
getLogsDb()!.delete(llmRequestLogs).run();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("llmRequestLogs.enabled write gate", () => {
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
resetLogs();
|
|
53
|
+
enabled = true;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("recordRequestLog writes normally when logging is enabled", () => {
|
|
57
|
+
const id = recordRequestLog("conv-1", '{"req":1}', '{"res":1}');
|
|
58
|
+
expect(id).not.toBeNull();
|
|
59
|
+
expect(getRequestLogById(id!)).not.toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("recordRequestLog skips the write when logging is disabled", () => {
|
|
63
|
+
enabled = false;
|
|
64
|
+
const id = recordRequestLog("conv-1", '{"req":1}', '{"res":1}');
|
|
65
|
+
expect(id).toBeNull();
|
|
66
|
+
expect(getRequestLogsByConversationId("conv-1")).toEqual([]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("recordSyntheticAgentErrorMessageLog skips the write when disabled", () => {
|
|
70
|
+
enabled = false;
|
|
71
|
+
const id = recordSyntheticAgentErrorMessageLog({
|
|
72
|
+
conversationId: "conv-2",
|
|
73
|
+
messageId: "msg-2",
|
|
74
|
+
exitReason: "budget_yield_unrecovered",
|
|
75
|
+
noticeText: "Out of budget.",
|
|
76
|
+
preparedRequest: null,
|
|
77
|
+
createdAt: Date.now(),
|
|
78
|
+
});
|
|
79
|
+
expect(id).toBeNull();
|
|
80
|
+
expect(getRequestLogsByConversationId("conv-2")).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("re-enabling logging restores writes", () => {
|
|
84
|
+
enabled = false;
|
|
85
|
+
expect(recordRequestLog("conv-3", '{"req":1}', '{"res":1}')).toBeNull();
|
|
86
|
+
enabled = true;
|
|
87
|
+
const id = recordRequestLog("conv-3", '{"req":2}', '{"res":2}');
|
|
88
|
+
expect(id).not.toBeNull();
|
|
89
|
+
expect(getRequestLogsByConversationId("conv-3")).toHaveLength(1);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -44,13 +44,21 @@ mock.module("../config/loader.js", () => ({
|
|
|
44
44
|
|
|
45
45
|
mock.module("../security/secure-keys.js", () => ({
|
|
46
46
|
getSecureKeyAsync: async (account: string) => {
|
|
47
|
-
if (account === "gemini")
|
|
48
|
-
|
|
47
|
+
if (account === "gemini") {
|
|
48
|
+
return mockGeminiKey;
|
|
49
|
+
}
|
|
50
|
+
if (account === "openai") {
|
|
51
|
+
return mockOpenAIKey;
|
|
52
|
+
}
|
|
49
53
|
return undefined;
|
|
50
54
|
},
|
|
51
55
|
getProviderKeyAsync: async (provider: string) => {
|
|
52
|
-
if (provider === "gemini")
|
|
53
|
-
|
|
56
|
+
if (provider === "gemini") {
|
|
57
|
+
return mockGeminiKey;
|
|
58
|
+
}
|
|
59
|
+
if (provider === "openai") {
|
|
60
|
+
return mockOpenAIKey;
|
|
61
|
+
}
|
|
54
62
|
return undefined;
|
|
55
63
|
},
|
|
56
64
|
}));
|
|
@@ -63,13 +71,16 @@ mock.module("../media/image-service.js", () => ({
|
|
|
63
71
|
) => {
|
|
64
72
|
lastGenerateProvider = provider;
|
|
65
73
|
lastGenerateCredentials = credentials;
|
|
66
|
-
if (mockGenerateError)
|
|
74
|
+
if (mockGenerateError) {
|
|
75
|
+
throw mockGenerateError;
|
|
76
|
+
}
|
|
67
77
|
return mockGenerateResult;
|
|
68
78
|
},
|
|
69
79
|
mapImageGenError: (provider: unknown, error: unknown) => {
|
|
70
80
|
const providerLabel = provider === "openai" ? "OpenAI" : "Gemini";
|
|
71
|
-
if (error instanceof Error)
|
|
81
|
+
if (error instanceof Error) {
|
|
72
82
|
return `Mock ${providerLabel} error: ${error.message}`;
|
|
83
|
+
}
|
|
73
84
|
return `Mock ${providerLabel} unknown error`;
|
|
74
85
|
},
|
|
75
86
|
}));
|
|
@@ -394,7 +405,9 @@ describe("image-studio skill script wrapper", () => {
|
|
|
394
405
|
expect(result.content).toContain("Generated 1 image");
|
|
395
406
|
} finally {
|
|
396
407
|
const { unlink } = await import("fs/promises");
|
|
397
|
-
if (await Bun.file(tmpPath).exists())
|
|
408
|
+
if (await Bun.file(tmpPath).exists()) {
|
|
409
|
+
await unlink(tmpPath);
|
|
410
|
+
}
|
|
398
411
|
}
|
|
399
412
|
});
|
|
400
413
|
|
|
@@ -85,7 +85,9 @@ mock.module("../permissions/checker.js", () => ({
|
|
|
85
85
|
isDynamicSkillLoadInvocation: () => false,
|
|
86
86
|
classifyRisk: async () => ({ level: riskOverride }),
|
|
87
87
|
check: async () => {
|
|
88
|
-
if (checkResultOverride)
|
|
88
|
+
if (checkResultOverride) {
|
|
89
|
+
return checkResultOverride;
|
|
90
|
+
}
|
|
89
91
|
return { decision: "allow", reason: "allowed" };
|
|
90
92
|
},
|
|
91
93
|
generateAllowlistOptions: () => [
|
|
@@ -106,7 +108,22 @@ mock.module("../telemetry/tool-usage-store.js", () => ({
|
|
|
106
108
|
|
|
107
109
|
mock.module("../tools/registry.js", () => ({
|
|
108
110
|
getTool: (name: string) => {
|
|
109
|
-
if (name === "unknown_tool")
|
|
111
|
+
if (name === "unknown_tool") {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
name,
|
|
116
|
+
description: "test tool",
|
|
117
|
+
category: "shell",
|
|
118
|
+
defaultRiskLevel: "medium",
|
|
119
|
+
input_schema: {},
|
|
120
|
+
execute: async () => fakeToolResult,
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
resolveTool: (name: string) => {
|
|
124
|
+
if (name === "unknown_tool") {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
110
127
|
return {
|
|
111
128
|
name,
|
|
112
129
|
description: "test tool",
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
registerSkillTools,
|
|
18
18
|
registerTool,
|
|
19
19
|
registerWorkspaceTools,
|
|
20
|
+
resolveTool,
|
|
20
21
|
unregisterSkillTools,
|
|
21
22
|
unregisterWorkspaceTool,
|
|
22
23
|
} from "../tools/registry.js";
|
|
@@ -109,6 +110,23 @@ describe("tool registry dynamic-tools tools", () => {
|
|
|
109
110
|
});
|
|
110
111
|
});
|
|
111
112
|
|
|
113
|
+
describe("resolveTool lazy initialization", () => {
|
|
114
|
+
test("await resolveTool initializes a cold registry, then sync getTool sees it", async () => {
|
|
115
|
+
// Empty the registry and clear the cached init promise.
|
|
116
|
+
__clearRegistryForTesting();
|
|
117
|
+
// A synchronous peek sees nothing yet — the registry is cold.
|
|
118
|
+
expect(getTool("file_read")).toBeUndefined();
|
|
119
|
+
|
|
120
|
+
// The async getter awaits initialization (like getHooksFor awaits its
|
|
121
|
+
// reconcile), so it resolves the built-in instead of a spurious undefined.
|
|
122
|
+
const tool = await resolveTool("file_read");
|
|
123
|
+
expect(tool?.name).toBe("file_read");
|
|
124
|
+
|
|
125
|
+
// After the ensure, the synchronous hot-path read sees it too.
|
|
126
|
+
expect(getTool("file_read")?.name).toBe("file_read");
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
112
130
|
describe("tool manifest", () => {
|
|
113
131
|
test("explicit tools list includes memory tools", () => {
|
|
114
132
|
const names = explicitTools.map((t) => t.name);
|
|
@@ -157,6 +157,21 @@ mock.module("../tools/registry.js", () => ({
|
|
|
157
157
|
execute: async () => fakeToolResult,
|
|
158
158
|
};
|
|
159
159
|
},
|
|
160
|
+
resolveTool: (name: string) => {
|
|
161
|
+
if (name === "unknown_tool") {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
const isGmailTool = name.startsWith("gmail_");
|
|
165
|
+
return {
|
|
166
|
+
name,
|
|
167
|
+
description: "test tool",
|
|
168
|
+
category: isGmailTool ? "gmail" : "credential-execution",
|
|
169
|
+
defaultRiskLevel: "high",
|
|
170
|
+
executionTarget: isGmailTool ? ("host" as const) : undefined,
|
|
171
|
+
input_schema: {},
|
|
172
|
+
execute: async () => fakeToolResult,
|
|
173
|
+
};
|
|
174
|
+
},
|
|
160
175
|
getAllTools: () => [],
|
|
161
176
|
}));
|
|
162
177
|
|
|
@@ -55,7 +55,9 @@ let _mockOverrides: Record<string, boolean> = {};
|
|
|
55
55
|
mock.module("../config/assistant-feature-flags.js", () => ({
|
|
56
56
|
isAssistantFeatureFlagEnabled: (key: string, _config: unknown): boolean => {
|
|
57
57
|
const explicit = _mockOverrides[key];
|
|
58
|
-
if (typeof explicit === "boolean")
|
|
58
|
+
if (typeof explicit === "boolean") {
|
|
59
|
+
return explicit;
|
|
60
|
+
}
|
|
59
61
|
return false; // undeclared flags default to disabled
|
|
60
62
|
},
|
|
61
63
|
clearFeatureFlagOverridesCache: () => {
|
|
@@ -84,15 +86,23 @@ mock.module("../skills/active-skill-tools.js", () => {
|
|
|
84
86
|
const entries: Array<{ id: string; version?: string }> = [];
|
|
85
87
|
for (const msg of messages) {
|
|
86
88
|
for (const block of msg.content) {
|
|
87
|
-
if (block.type !== "tool_result")
|
|
88
|
-
|
|
89
|
+
if (block.type !== "tool_result") {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (!skillLoadUseIds.has(block.tool_use_id)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
89
95
|
const text = block.content;
|
|
90
|
-
if (!text)
|
|
96
|
+
if (!text) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
91
99
|
for (const m of text.matchAll(re)) {
|
|
92
100
|
if (!seen.has(m[1])) {
|
|
93
101
|
seen.add(m[1]);
|
|
94
102
|
const entry: { id: string; version?: string } = { id: m[1] };
|
|
95
|
-
if (m[2])
|
|
103
|
+
if (m[2]) {
|
|
104
|
+
entry.version = m[2];
|
|
105
|
+
}
|
|
96
106
|
entries.push(entry);
|
|
97
107
|
}
|
|
98
108
|
}
|
|
@@ -111,7 +121,9 @@ mock.module("../skills/tool-manifest.js", () => ({
|
|
|
111
121
|
const parts = filePath.split("/");
|
|
112
122
|
const skillId = parts[parts.length - 2];
|
|
113
123
|
const manifest = mockManifests[skillId];
|
|
114
|
-
if (!manifest)
|
|
124
|
+
if (!manifest) {
|
|
125
|
+
throw new Error(`Mock: no manifest for skill "${skillId}"`);
|
|
126
|
+
}
|
|
115
127
|
return manifest;
|
|
116
128
|
},
|
|
117
129
|
}));
|
|
@@ -162,7 +174,20 @@ mock.module("../tools/registry.js", () => ({
|
|
|
162
174
|
let found: Tool | undefined;
|
|
163
175
|
for (const tools of mockRegisteredTools.values()) {
|
|
164
176
|
for (const tool of tools) {
|
|
165
|
-
if (tool.name === name)
|
|
177
|
+
if (tool.name === name) {
|
|
178
|
+
found = tool;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return found;
|
|
183
|
+
},
|
|
184
|
+
resolveTool: (name: string): Tool | undefined => {
|
|
185
|
+
let found: Tool | undefined;
|
|
186
|
+
for (const tools of mockRegisteredTools.values()) {
|
|
187
|
+
for (const tool of tools) {
|
|
188
|
+
if (tool.name === name) {
|
|
189
|
+
found = tool;
|
|
190
|
+
}
|
|
166
191
|
}
|
|
167
192
|
}
|
|
168
193
|
return found;
|
|
@@ -177,7 +202,9 @@ mock.module("../tools/registry.js", () => ({
|
|
|
177
202
|
let ownerSkillId: string | undefined;
|
|
178
203
|
for (const [skillId, tools] of mockRegisteredTools.entries()) {
|
|
179
204
|
for (const tool of tools) {
|
|
180
|
-
if (tool.name === name)
|
|
205
|
+
if (tool.name === name) {
|
|
206
|
+
ownerSkillId = skillId;
|
|
207
|
+
}
|
|
181
208
|
}
|
|
182
209
|
}
|
|
183
210
|
return ownerSkillId === undefined
|
|
@@ -175,15 +175,20 @@ mock.module("../tools/registry.js", () => ({
|
|
|
175
175
|
skillId: string,
|
|
176
176
|
tools: Array<{ name: string; [k: string]: unknown }>,
|
|
177
177
|
) => {
|
|
178
|
-
for (const t of tools)
|
|
178
|
+
for (const t of tools) {
|
|
179
|
+
benchmarkRegistry.set(t.name, { tool: t, skillId });
|
|
180
|
+
}
|
|
179
181
|
return tools;
|
|
180
182
|
},
|
|
181
183
|
unregisterSkillTools: (skillId: string) => {
|
|
182
184
|
for (const [name, entry] of benchmarkRegistry) {
|
|
183
|
-
if (entry.skillId === skillId)
|
|
185
|
+
if (entry.skillId === skillId) {
|
|
186
|
+
benchmarkRegistry.delete(name);
|
|
187
|
+
}
|
|
184
188
|
}
|
|
185
189
|
},
|
|
186
190
|
getTool: (name: string) => benchmarkRegistry.get(name)?.tool,
|
|
191
|
+
resolveTool: (name: string) => benchmarkRegistry.get(name)?.tool,
|
|
187
192
|
getToolOwner: (name: string) => {
|
|
188
193
|
const entry = benchmarkRegistry.get(name);
|
|
189
194
|
return entry ? { kind: "skill" as const, id: entry.skillId } : undefined;
|
|
@@ -32,6 +32,7 @@ const fakeTool = {
|
|
|
32
32
|
|
|
33
33
|
mock.module("../tools/registry.js", () => ({
|
|
34
34
|
getTool: (name: string) => (name === "bash" ? fakeTool : undefined),
|
|
35
|
+
resolveTool: (name: string) => (name === "bash" ? fakeTool : undefined),
|
|
35
36
|
getAllTools: () => [fakeTool],
|
|
36
37
|
}));
|
|
37
38
|
|
|
@@ -52,6 +52,7 @@ mock.module("../config/skills.js", () => ({
|
|
|
52
52
|
|
|
53
53
|
mock.module("../tools/registry.js", () => ({
|
|
54
54
|
getTool: (name: string) => localRegistry.get(name),
|
|
55
|
+
resolveTool: (name: string) => localRegistry.get(name),
|
|
55
56
|
getAllTools: () => Array.from(localRegistry.values()),
|
|
56
57
|
registerTool: (tool: import("../tools/types.js").Tool) => {
|
|
57
58
|
localRegistry.set(tool.name, tool);
|