@vellumai/assistant 0.10.9-staging.1 → 0.10.9
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/AGENTS.md +4 -0
- package/knip.json +1 -4
- package/openapi.yaml +27 -17
- package/package.json +1 -1
- package/src/__tests__/always-loaded-tools-guard.test.ts +4 -6
- package/src/__tests__/app-routes-csp.test.ts +13 -4
- package/src/__tests__/conversation-tool-setup-tools-disabled.test.ts +4 -8
- package/src/__tests__/disk-pressure-tools.test.ts +4 -4
- package/src/__tests__/file-list-tool.test.ts +22 -12
- package/src/__tests__/list-all-apps.test.ts +204 -0
- package/src/__tests__/messaging-send-tool.test.ts +67 -0
- package/src/__tests__/mtime-cache.test.ts +42 -0
- package/src/__tests__/oauth-provider-profiles.test.ts +3 -2
- package/src/__tests__/outlook-messaging-provider.test.ts +71 -0
- package/src/__tests__/plugin-app-serve-routes.test.ts +163 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +47 -0
- package/src/__tests__/subagent-notify-parent.test.ts +4 -3
- package/src/__tests__/subagent-tool-filtering.test.ts +21 -21
- package/src/__tests__/subagent-tool-gate-mode.test.ts +6 -6
- package/src/apps/app-store.ts +302 -2
- package/src/cli/AGENTS.md +17 -0
- package/src/cli/commands/__tests__/gateway.test.ts +55 -0
- package/src/cli/commands/apps.help.ts +17 -11
- package/src/cli/commands/apps.ts +5 -15
- package/src/cli/commands/gateway.help.ts +30 -0
- package/src/cli/commands/gateway.ts +40 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +0 -90
- package/src/cli/commands/platform/index.help.ts +3 -2
- package/src/cli/commands/platform/index.ts +0 -9
- package/src/cli/commands/routes.help.ts +13 -7
- package/src/cli/commands/routes.ts +5 -3
- package/src/cli/commands/status.help.ts +24 -0
- package/src/cli/commands/status.ts +29 -0
- package/src/cli/lib/__tests__/global-json-option.test.ts +97 -0
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +1 -0
- package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +31 -0
- package/src/cli/lib/bundled-marketplace.json +187 -173
- package/src/cli/lib/global-json-option.ts +41 -0
- package/src/cli/lib/plugin-fingerprint.ts +18 -7
- package/src/cli/program.ts +5 -0
- package/src/config/bundled-skills/messaging/TOOLS.json +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +27 -19
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
- package/src/config/feature-flag-registry.json +1 -9
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +3 -6
- package/src/daemon/__tests__/conversation-tool-setup-plugin-scope.test.ts +3 -6
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +8 -5
- package/src/daemon/conversation-tool-setup.ts +5 -90
- package/src/daemon/conversation.ts +2 -2
- package/src/messaging/provider-types.ts +9 -0
- package/src/messaging/providers/outlook/adapter.ts +17 -2
- package/src/messaging/providers/outlook/client.ts +11 -2
- package/src/messaging/providers/outlook/types.ts +9 -0
- package/src/monitoring/__tests__/plugin-source-watch.test.ts +37 -0
- package/src/monitoring/plugin-source-watch.ts +53 -0
- package/src/oauth/seed-providers.ts +6 -0
- package/src/persistence/embeddings/__tests__/messages-lexical-index.test.ts +3 -0
- package/src/persistence/embeddings/messages-lexical-index.ts +11 -0
- package/src/plugin-api/conversation-turn.ts +234 -0
- package/src/plugin-api/index.ts +12 -0
- package/src/plugins/__tests__/source-fingerprint.test.ts +41 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +48 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-skill-card.test.ts +46 -5
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +2 -2
- package/src/plugins/defaults/memory/graph-topology/__tests__/build-memory-graph.test.ts +81 -2
- package/src/plugins/defaults/memory/graph-topology/build-memory-graph.ts +80 -33
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +40 -1
- package/src/plugins/defaults/memory/memory-retrospective-skill-card.ts +28 -2
- package/src/plugins/mtime-cache.ts +26 -13
- package/src/plugins/plugin-tree-walk.ts +30 -0
- package/src/plugins/source-fingerprint.ts +9 -2
- package/src/plugins/surface-import.ts +28 -0
- package/src/providers/speech-to-text/__tests__/vellum-managed-realtime.test.ts +1 -1
- package/src/providers/speech-to-text/__tests__/vellum-speech-relay-connection.test.ts +4 -1
- package/src/providers/speech-to-text/vellum-speech-relay-connection.ts +5 -0
- package/src/runtime/routes/__tests__/gateway-status-routes.test.ts +84 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +47 -3
- package/src/runtime/routes/__tests__/user-routes-cli.test.ts +154 -0
- package/src/runtime/routes/app-management-routes.ts +96 -32
- package/src/runtime/routes/app-routes.ts +33 -17
- package/src/runtime/routes/gateway-status-routes.ts +69 -0
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/platform-routes.ts +4 -14
- package/src/runtime/routes/plugins-routes.ts +58 -1
- package/src/runtime/routes/settings-routes.ts +8 -6
- package/src/runtime/routes/user-route-dispatcher.ts +6 -86
- package/src/runtime/routes/user-route-resolution.ts +141 -0
- package/src/runtime/routes/user-routes-cli.ts +56 -35
- package/src/telemetry/AGENTS.md +15 -0
- package/src/telemetry/__tests__/telemetry-event-fixtures.ts +258 -0
- package/src/telemetry/telemetry-wire-source.json +1 -1
- package/src/telemetry/telemetry-wire-validation.test.ts +157 -0
- package/src/telemetry/telemetry-wire-validation.ts +125 -0
- package/src/telemetry/telemetry-wire.generated.ts +60 -9
- package/src/telemetry/types.test.ts +38 -0
- package/src/telemetry/types.ts +183 -91
- package/src/telemetry/usage-telemetry-reporter.ts +5 -0
- package/src/telemetry/watchdog-direct-emit.ts +6 -0
- package/src/tools/skills/scaffold-managed.ts +43 -11
|
@@ -18,7 +18,10 @@
|
|
|
18
18
|
import { createHash } from "node:crypto";
|
|
19
19
|
import { readFileSync } from "node:fs";
|
|
20
20
|
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
isGeneratedAppBuildDir,
|
|
23
|
+
walkPluginTree,
|
|
24
|
+
} from "../../plugins/plugin-tree-walk.js";
|
|
22
25
|
|
|
23
26
|
/** Digest algorithm recorded alongside the file map, for forward compatibility. */
|
|
24
27
|
export type FingerprintAlgorithm = "sha256";
|
|
@@ -64,9 +67,13 @@ export function computeFingerprint(
|
|
|
64
67
|
exclude: readonly string[] = [],
|
|
65
68
|
): Fingerprint {
|
|
66
69
|
const files: Record<string, string> = {};
|
|
67
|
-
walkPluginTree(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
walkPluginTree(
|
|
71
|
+
root,
|
|
72
|
+
{ excludeRootEntries: exclude, excludeDir: isGeneratedAppBuildDir },
|
|
73
|
+
(rel, abs) => {
|
|
74
|
+
files[rel] = hashFile(abs);
|
|
75
|
+
},
|
|
76
|
+
);
|
|
70
77
|
return { algorithm: "sha256", files };
|
|
71
78
|
}
|
|
72
79
|
|
|
@@ -178,9 +185,13 @@ export function computeContentHash(
|
|
|
178
185
|
exclude: readonly string[] = [],
|
|
179
186
|
): string {
|
|
180
187
|
const entries: Array<{ rel: string; abs: string }> = [];
|
|
181
|
-
walkPluginTree(
|
|
182
|
-
|
|
183
|
-
|
|
188
|
+
walkPluginTree(
|
|
189
|
+
root,
|
|
190
|
+
{ excludeRootEntries: exclude, excludeDir: isGeneratedAppBuildDir },
|
|
191
|
+
(rel, abs) => {
|
|
192
|
+
entries.push({ rel, abs });
|
|
193
|
+
},
|
|
194
|
+
);
|
|
184
195
|
entries.sort((a, b) => a.rel.localeCompare(b.rel));
|
|
185
196
|
|
|
186
197
|
const hash = createHash("sha256");
|
package/src/cli/program.ts
CHANGED
|
@@ -55,6 +55,7 @@ import { registerUsageCommand } from "./commands/usage.js";
|
|
|
55
55
|
import { registerWatchersCommand } from "./commands/watchers.js";
|
|
56
56
|
import { registerWebhooksCommand } from "./commands/webhooks.js";
|
|
57
57
|
import { red } from "./lib/cli-colors.js";
|
|
58
|
+
import { registerGlobalJsonOption } from "./lib/global-json-option.js";
|
|
58
59
|
import { log } from "./logger.js";
|
|
59
60
|
|
|
60
61
|
/**
|
|
@@ -151,6 +152,10 @@ Examples:
|
|
|
151
152
|
registerWatchersCommand(program);
|
|
152
153
|
registerWebhooksCommand(program);
|
|
153
154
|
|
|
155
|
+
// Every command accepts `--json` — see registerGlobalJsonOption. Must run
|
|
156
|
+
// after all commands are registered so the whole tree is covered.
|
|
157
|
+
registerGlobalJsonOption(program);
|
|
158
|
+
|
|
154
159
|
// Fail fast when no assistant workspace exists on disk. The workspace is
|
|
155
160
|
// created by `vellum hatch` and must be present for any command to work.
|
|
156
161
|
// Commander handles --help and --version before preAction fires, so those
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
},
|
|
126
126
|
{
|
|
127
127
|
"name": "messaging_send",
|
|
128
|
-
"description": "Send a message, reply to a thread, or create a draft. On Gmail, always creates a draft for review. Supports replies (via thread_id), attachments (via attachment_paths, Gmail
|
|
128
|
+
"description": "Send a message, reply to a thread, or create a draft. On Gmail, always creates a draft for review. Supports replies (via thread_id), attachments (via attachment_paths, Gmail and Outlook), and all messaging platforms.",
|
|
129
129
|
"category": "messaging",
|
|
130
130
|
"risk": "high",
|
|
131
131
|
"input_schema": {
|
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
"items": {
|
|
161
161
|
"type": "string"
|
|
162
162
|
},
|
|
163
|
-
"description": "Absolute file paths of attachments to include (Gmail
|
|
163
|
+
"description": "Absolute file paths of attachments to include (Gmail and Outlook)"
|
|
164
164
|
},
|
|
165
165
|
"thread_id": {
|
|
166
166
|
"type": "string",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
|
|
4
|
+
import type { OutboundAttachment } from "../../../../messaging/provider-types.js";
|
|
4
5
|
import {
|
|
5
6
|
createDraft,
|
|
6
7
|
createDraftRaw,
|
|
@@ -32,6 +33,20 @@ import {
|
|
|
32
33
|
|
|
33
34
|
const log = getLogger("messaging-send");
|
|
34
35
|
|
|
36
|
+
/** Read attachment files from disk into in-memory parts for outbound sending. */
|
|
37
|
+
async function readAttachments(paths: string[]): Promise<OutboundAttachment[]> {
|
|
38
|
+
return Promise.all(
|
|
39
|
+
paths.map(async (filePath) => ({
|
|
40
|
+
filename: basename(filePath),
|
|
41
|
+
mimeType: guessMimeType(filePath),
|
|
42
|
+
data: await readFile(filePath),
|
|
43
|
+
})),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Email providers that accept file attachments on outbound sends. */
|
|
48
|
+
const ATTACHMENT_CAPABLE_PLATFORMS = new Set(["gmail", "outlook"]);
|
|
49
|
+
|
|
35
50
|
export async function run(
|
|
36
51
|
input: Record<string, unknown>,
|
|
37
52
|
context: ToolContext,
|
|
@@ -54,9 +69,12 @@ export async function run(
|
|
|
54
69
|
try {
|
|
55
70
|
const provider = await resolveProvider(platform);
|
|
56
71
|
|
|
57
|
-
//
|
|
58
|
-
if (
|
|
59
|
-
|
|
72
|
+
// Reject attachments on platforms that can't carry them (e.g. Telegram, WhatsApp).
|
|
73
|
+
if (
|
|
74
|
+
attachmentPaths?.length &&
|
|
75
|
+
!ATTACHMENT_CAPABLE_PLATFORMS.has(provider.id)
|
|
76
|
+
) {
|
|
77
|
+
return err("Attachments are only supported on Gmail and Outlook.");
|
|
60
78
|
}
|
|
61
79
|
|
|
62
80
|
const account = input.account as string | undefined;
|
|
@@ -125,14 +143,7 @@ export async function run(
|
|
|
125
143
|
|
|
126
144
|
// With attachments: build multipart MIME for threaded reply
|
|
127
145
|
if (attachmentPaths?.length) {
|
|
128
|
-
const attachments = await
|
|
129
|
-
attachmentPaths.map(async (filePath) => {
|
|
130
|
-
const data = await readFile(filePath);
|
|
131
|
-
const filename = basename(filePath);
|
|
132
|
-
const mimeType = guessMimeType(filePath);
|
|
133
|
-
return { filename, mimeType, data };
|
|
134
|
-
}),
|
|
135
|
-
);
|
|
146
|
+
const attachments = await readAttachments(attachmentPaths);
|
|
136
147
|
|
|
137
148
|
const raw = buildMultipartMime({
|
|
138
149
|
to: toList.join(", "),
|
|
@@ -176,14 +187,7 @@ export async function run(
|
|
|
176
187
|
|
|
177
188
|
// With attachments: build multipart MIME and use createDraftRaw
|
|
178
189
|
if (attachmentPaths?.length) {
|
|
179
|
-
const attachments = await
|
|
180
|
-
attachmentPaths.map(async (filePath) => {
|
|
181
|
-
const data = await readFile(filePath);
|
|
182
|
-
const filename = basename(filePath);
|
|
183
|
-
const mimeType = guessMimeType(filePath);
|
|
184
|
-
return { filename, mimeType, data };
|
|
185
|
-
}),
|
|
186
|
-
);
|
|
190
|
+
const attachments = await readAttachments(attachmentPaths);
|
|
187
191
|
|
|
188
192
|
const raw = buildMultipartMime({
|
|
189
193
|
to: conversationId,
|
|
@@ -217,10 +221,14 @@ export async function run(
|
|
|
217
221
|
}
|
|
218
222
|
|
|
219
223
|
// Non-Gmail platforms
|
|
224
|
+
const attachments = attachmentPaths?.length
|
|
225
|
+
? await readAttachments(attachmentPaths)
|
|
226
|
+
: undefined;
|
|
220
227
|
const result = await provider.sendMessage(conn, conversationId, text, {
|
|
221
228
|
subject,
|
|
222
229
|
inReplyTo,
|
|
223
230
|
threadId,
|
|
231
|
+
attachments,
|
|
224
232
|
assistantId: context.assistantId,
|
|
225
233
|
});
|
|
226
234
|
|
|
@@ -16,18 +16,18 @@ First check whether this is a managed/platform assistant:
|
|
|
16
16
|
assistant platform status --json
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
If it reports an available platform assistant, do not install or start ngrok for Twilio. The gateway should use Velay
|
|
19
|
+
If it reports an available platform assistant, do not install or start ngrok for Twilio. The gateway should use Velay; run `assistant gateway status --json` and confirm it reports a `tunnel` URL (a `{ "tunnel": "..." }` object, not `{}`) after registration. If this is a local/self-hosted assistant without Velay, run the **public-ingress** skill to set up ngrok or another custom tunnel and configure `ingress.publicBaseUrl`.
|
|
20
20
|
|
|
21
21
|
## Call fails immediately after initiating
|
|
22
22
|
|
|
23
23
|
- Check that the phone number is in E.164 format
|
|
24
24
|
- Verify Twilio credentials are correct (wrong auth token causes API errors)
|
|
25
25
|
- On trial accounts, ensure the destination number is verified
|
|
26
|
-
- Check that the configured tunnel is still running. For ngrok, use `curl -s http://127.0.0.1:4040/api/tunnels`. For Velay, run `assistant
|
|
26
|
+
- Check that the configured tunnel is still running. For ngrok, use `curl -s http://127.0.0.1:4040/api/tunnels`. For Velay, run `assistant gateway status --json` and confirm it reports a `tunnel` URL (not `{}`), or check gateway logs for `Velay tunnel registered`.
|
|
27
27
|
|
|
28
28
|
## Call connects but no audio / one-way audio
|
|
29
29
|
|
|
30
|
-
- The media-stream WebSocket may not be connecting. If you are using ngrok or a custom tunnel, check that `ingress.publicBaseUrl` is correct and the tunnel is active. If you are using Velay, check `assistant
|
|
30
|
+
- The media-stream WebSocket may not be connecting. If you are using ngrok or a custom tunnel, check that `ingress.publicBaseUrl` is correct and the tunnel is active. If you are using Velay, check `assistant gateway status --json`; a 503 from `https://velay.../<assistant-id>/...` usually means the assistant tunnel is not connected or the gateway did not complete the WebSocket open, not that ngrok is required.
|
|
31
31
|
- Verify the assistant is running
|
|
32
32
|
|
|
33
33
|
## "Number not eligible for caller identity"
|
|
@@ -178,14 +178,6 @@
|
|
|
178
178
|
"description": "Show a live HUD in the top-right of the conversation with scroll geometry, velocity, update rate, and anchor-preserver activity. Developer diagnostic for investigating scroll jank.",
|
|
179
179
|
"defaultEnabled": false
|
|
180
180
|
},
|
|
181
|
-
{
|
|
182
|
-
"id": "coding-agents-panel",
|
|
183
|
-
"scope": "client",
|
|
184
|
-
"key": "coding-agents-panel",
|
|
185
|
-
"label": "Coding Agents Panel",
|
|
186
|
-
"description": "Gate native macOS Coding Agents panel entry points and inline ACP session deep links.",
|
|
187
|
-
"defaultEnabled": false
|
|
188
|
-
},
|
|
189
181
|
{
|
|
190
182
|
"id": "compaction-playground",
|
|
191
183
|
"scope": "assistant",
|
|
@@ -311,7 +303,7 @@
|
|
|
311
303
|
"scope": "assistant",
|
|
312
304
|
"key": "memory-concept-graph",
|
|
313
305
|
"label": "Memory concept graph",
|
|
314
|
-
"description": "Gates the Obsidian-style memory concept graph on
|
|
306
|
+
"description": "Gates the Obsidian-style memory concept graph on its own Memory tab (/assistant/memory) and the backend-agnostic /memory-graph + /memory-graph-node routes that feed it. When off, the Memory tab is hidden; the identity page keeps the skills constellation. Only produces a graph on memory-v3 backends. Default off.",
|
|
315
307
|
"defaultEnabled": false
|
|
316
308
|
},
|
|
317
309
|
{
|
|
@@ -16,10 +16,9 @@ import {
|
|
|
16
16
|
registerPluginTools,
|
|
17
17
|
} from "../../tools/registry.js";
|
|
18
18
|
import type { Tool } from "../../tools/types.js";
|
|
19
|
+
import type { Conversation } from "../conversation.js";
|
|
19
20
|
import { createResolveToolsCallback } from "../conversation-tool-setup.js";
|
|
20
21
|
|
|
21
|
-
type SkillProjectionContext =
|
|
22
|
-
import("../conversation-tool-setup.js").SkillProjectionContext;
|
|
23
22
|
type SkillProjectionCache =
|
|
24
23
|
import("../conversation-skill-tools.js").SkillProjectionCache;
|
|
25
24
|
|
|
@@ -43,15 +42,13 @@ function pluginTool(name: string): Tool {
|
|
|
43
42
|
} as unknown as Tool;
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
function makeCtx(
|
|
47
|
-
overrides: Partial<SkillProjectionContext> = {},
|
|
48
|
-
): SkillProjectionContext {
|
|
45
|
+
function makeCtx(overrides: Partial<Conversation> = {}): Conversation {
|
|
49
46
|
return {
|
|
50
47
|
skillProjectionState: new Map(),
|
|
51
48
|
skillProjectionCache: { fingerprints: new Map() } as SkillProjectionCache,
|
|
52
49
|
toolsDisabledDepth: 0,
|
|
53
50
|
...overrides,
|
|
54
|
-
};
|
|
51
|
+
} as unknown as Conversation;
|
|
55
52
|
}
|
|
56
53
|
|
|
57
54
|
function withExclude(exclude: string[]) {
|
|
@@ -16,10 +16,9 @@ import {
|
|
|
16
16
|
registerPluginTools,
|
|
17
17
|
} from "../../tools/registry.js";
|
|
18
18
|
import type { Tool } from "../../tools/types.js";
|
|
19
|
+
import type { Conversation } from "../conversation.js";
|
|
19
20
|
import { createResolveToolsCallback } from "../conversation-tool-setup.js";
|
|
20
21
|
|
|
21
|
-
type SkillProjectionContext =
|
|
22
|
-
import("../conversation-tool-setup.js").SkillProjectionContext;
|
|
23
22
|
type SkillProjectionCache =
|
|
24
23
|
import("../conversation-skill-tools.js").SkillProjectionCache;
|
|
25
24
|
|
|
@@ -35,15 +34,13 @@ function pluginTool(name: string): Tool {
|
|
|
35
34
|
} as unknown as Tool;
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
function makeCtx(
|
|
39
|
-
overrides: Partial<SkillProjectionContext> = {},
|
|
40
|
-
): SkillProjectionContext {
|
|
37
|
+
function makeCtx(overrides: Partial<Conversation> = {}): Conversation {
|
|
41
38
|
return {
|
|
42
39
|
skillProjectionState: new Map(),
|
|
43
40
|
skillProjectionCache: { fingerprints: new Map() } as SkillProjectionCache,
|
|
44
41
|
toolsDisabledDepth: 0,
|
|
45
42
|
...overrides,
|
|
46
|
-
};
|
|
43
|
+
} as unknown as Conversation;
|
|
47
44
|
}
|
|
48
45
|
|
|
49
46
|
let getConfigSpy: ReturnType<typeof spyOn> | undefined;
|
|
@@ -29,6 +29,9 @@
|
|
|
29
29
|
|
|
30
30
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
31
31
|
|
|
32
|
+
import type { Conversation } from "../conversation.js";
|
|
33
|
+
import type { ChannelCapabilities } from "../conversation-runtime-assembly.js";
|
|
34
|
+
|
|
32
35
|
// ── Module-level mocks ─────────────────────────────────────────────
|
|
33
36
|
|
|
34
37
|
// Control how many capable clients the hub reports per capability.
|
|
@@ -51,20 +54,20 @@ mock.module("../../runtime/assistant-event-hub.js", () => ({
|
|
|
51
54
|
// before the modules under test are loaded.
|
|
52
55
|
const { HOST_TOOL_NAMES, HOST_TOOL_TO_CAPABILITY, isToolActiveForContext } =
|
|
53
56
|
await import("../conversation-tool-setup.js");
|
|
54
|
-
type SkillProjectionContext =
|
|
55
|
-
import("../conversation-tool-setup.js").SkillProjectionContext;
|
|
56
57
|
type SkillProjectionCache =
|
|
57
58
|
import("../conversation-skill-tools.js").SkillProjectionCache;
|
|
58
59
|
|
|
59
60
|
function makeCtx(
|
|
60
|
-
overrides: Partial<
|
|
61
|
-
|
|
61
|
+
overrides: Partial<Omit<Conversation, "channelCapabilities">> & {
|
|
62
|
+
channelCapabilities?: Partial<ChannelCapabilities>;
|
|
63
|
+
} = {},
|
|
64
|
+
): Conversation {
|
|
62
65
|
return {
|
|
63
66
|
skillProjectionState: new Map(),
|
|
64
67
|
skillProjectionCache: {} as SkillProjectionCache,
|
|
65
68
|
toolsDisabledDepth: 0,
|
|
66
69
|
...overrides,
|
|
67
|
-
};
|
|
70
|
+
} as unknown as Conversation;
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
beforeEach(() => {
|
|
@@ -8,12 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
type HostProxyCapability,
|
|
11
|
-
type InterfaceId,
|
|
12
11
|
supportsHostProxy,
|
|
13
12
|
} from "../channels/types.js";
|
|
14
13
|
import { getIsPlatform } from "../config/env-registry.js";
|
|
15
14
|
import { getConfig } from "../config/loader.js";
|
|
16
|
-
import type { LLMCallSite } from "../config/schemas/llm.js";
|
|
17
15
|
import type { PermissionPrompter } from "../permissions/prompter.js";
|
|
18
16
|
import type { SecretPrompter } from "../permissions/secret-prompter.js";
|
|
19
17
|
import { getBindingByConversation } from "../persistence/external-conversation-store.js";
|
|
@@ -58,10 +56,8 @@ import {
|
|
|
58
56
|
type UsageAttributionSnapshot,
|
|
59
57
|
} from "../usage/attribution.js";
|
|
60
58
|
import { getLogger } from "../util/logger.js";
|
|
61
|
-
import {
|
|
62
|
-
|
|
63
|
-
type SkillProjectionCache,
|
|
64
|
-
} from "./conversation-skill-tools.js";
|
|
59
|
+
import type { Conversation } from "./conversation.js";
|
|
60
|
+
import { projectSkillTools } from "./conversation-skill-tools.js";
|
|
65
61
|
import { surfaceProxyResolver } from "./conversation-surfaces.js";
|
|
66
62
|
import {
|
|
67
63
|
isDoordashCommand,
|
|
@@ -73,11 +69,7 @@ import { FALLBACK_TURN_TRUST, resolveTrustClass } from "./trust-context.js";
|
|
|
73
69
|
|
|
74
70
|
const log = getLogger("conversation-tool-setup");
|
|
75
71
|
|
|
76
|
-
import type {
|
|
77
|
-
SubagentToolGateMode,
|
|
78
|
-
ToolSetupContext,
|
|
79
|
-
WakeToolContextPin,
|
|
80
|
-
} from "./tool-setup-types.js";
|
|
72
|
+
import type { ToolSetupContext } from "./tool-setup-types.js";
|
|
81
73
|
export type {
|
|
82
74
|
SubagentToolGateMode,
|
|
83
75
|
ToolSetupContext,
|
|
@@ -480,83 +472,6 @@ export function createProxyApprovalCallback(
|
|
|
480
472
|
*/
|
|
481
473
|
export const DEFAULT_PREACTIVATED_SKILL_IDS = ["notifications", "subagent"];
|
|
482
474
|
|
|
483
|
-
/**
|
|
484
|
-
* Subset of Conversation state that the resolveTools callback reads at each
|
|
485
|
-
* agent turn. Properties are read lazily from this reference.
|
|
486
|
-
*/
|
|
487
|
-
export interface SkillProjectionContext {
|
|
488
|
-
preactivatedSkillIds?: string[];
|
|
489
|
-
readonly skillProjectionState: Map<string, string>;
|
|
490
|
-
readonly skillProjectionCache: SkillProjectionCache;
|
|
491
|
-
allowedToolNames?: Set<string>;
|
|
492
|
-
/**
|
|
493
|
-
* Durable copy of the full tool definitions resolved on the most recent
|
|
494
|
-
* turn, used by read-only inventory queries. Set alongside
|
|
495
|
-
* {@link allowedToolNames} but, unlike that per-turn execution gate, never
|
|
496
|
-
* cleared at turn teardown.
|
|
497
|
-
*/
|
|
498
|
-
registeredToolDefinitions?: ToolDefinition[];
|
|
499
|
-
/** When > 0, the resolveTools callback returns no tools at all. */
|
|
500
|
-
toolsDisabledDepth: number;
|
|
501
|
-
/** Channel capabilities — read lazily per turn for conditional tool filtering. */
|
|
502
|
-
readonly channelCapabilities?: {
|
|
503
|
-
channel: string;
|
|
504
|
-
supportsDynamicUi: boolean;
|
|
505
|
-
clientOS?: string;
|
|
506
|
-
};
|
|
507
|
-
/** True when no client is connected (HTTP-only). */
|
|
508
|
-
readonly hasNoClient?: boolean;
|
|
509
|
-
/** When set, only tools in this set are included in the resolved tool list (subagent delegation). */
|
|
510
|
-
subagentAllowedTools?: Set<string>;
|
|
511
|
-
/**
|
|
512
|
-
* How {@link subagentAllowedTools} is enforced — see
|
|
513
|
-
* {@link SubagentToolGateMode}. Absent means `"wire"`.
|
|
514
|
-
*/
|
|
515
|
-
subagentToolGateMode?: SubagentToolGateMode;
|
|
516
|
-
/**
|
|
517
|
-
* When set (execution-gate-mode wakes), tool-definition resolution reads
|
|
518
|
-
* `hasNoClient` / `transportInterface` / `channelCapabilities` exclusively
|
|
519
|
-
* from this pin instead of the live conversation — see
|
|
520
|
-
* {@link WakeToolContextPin}.
|
|
521
|
-
*/
|
|
522
|
-
readonly toolContextPin?: WakeToolContextPin;
|
|
523
|
-
/** True when the current turn is restricted to disk-pressure cleanup-safe tools. */
|
|
524
|
-
diskPressureCleanupModeActive?: boolean;
|
|
525
|
-
/** True when this conversation belongs to a subagent spawned by SubagentManager. */
|
|
526
|
-
readonly isSubagent?: boolean;
|
|
527
|
-
/**
|
|
528
|
-
* The interface id of the connected client driving the current turn (e.g.
|
|
529
|
-
* "macos", "chrome-extension"). Used to gate host tools by per-capability
|
|
530
|
-
* `supportsHostProxy(transport, capability)` so that interfaces which only
|
|
531
|
-
* support a subset of the host proxy set (e.g. chrome-extension supports
|
|
532
|
-
* `host_browser` but not `host_bash`/`host_file`) do not leak unsupported
|
|
533
|
-
* host tools into the LLM tool definitions.
|
|
534
|
-
*/
|
|
535
|
-
readonly transportInterface?: InterfaceId;
|
|
536
|
-
/** Per-turn override profile. */
|
|
537
|
-
currentTurnOverrideProfile?: string;
|
|
538
|
-
/**
|
|
539
|
-
* The conversation's per-chat plugin scope (mirrors
|
|
540
|
-
* {@link Conversation.enabledPlugins}). `null`/absent means no per-chat
|
|
541
|
-
* restriction; otherwise plugin-owned tools and skills are intersected with
|
|
542
|
-
* the effective set (the scope unioned with the always-on first-party
|
|
543
|
-
* defaults) via {@link getEffectiveEnabledPluginSet}. Read per turn so a
|
|
544
|
-
* mid-conversation scope change is picked up.
|
|
545
|
-
*/
|
|
546
|
-
readonly enabledPlugins?: string[] | null;
|
|
547
|
-
/**
|
|
548
|
-
* Conversation id for `skill_loaded` telemetry. Absent (e.g. minimal test
|
|
549
|
-
* contexts) disables telemetry recording in the skill projection.
|
|
550
|
-
*/
|
|
551
|
-
readonly conversationId?: string;
|
|
552
|
-
/**
|
|
553
|
-
* The LLM call site driving the current turn (see
|
|
554
|
-
* {@link ToolSetupContext.currentCallSite}) — read per turn so skill_loaded
|
|
555
|
-
* telemetry attributes the provider/model/profile the turn actually ran on.
|
|
556
|
-
*/
|
|
557
|
-
currentCallSite?: LLMCallSite;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
475
|
// ── Conditional tool sets ────────────────────────────────────────────
|
|
561
476
|
|
|
562
477
|
const UI_SURFACE_TOOL_NAMES = new Set(["ui_show", "ui_update", "ui_dismiss"]);
|
|
@@ -646,7 +561,7 @@ export const SUBAGENT_ONLY_TOOL_NAMES = new Set<string>([
|
|
|
646
561
|
*/
|
|
647
562
|
export function isToolActiveForContext(
|
|
648
563
|
name: string,
|
|
649
|
-
ctx:
|
|
564
|
+
ctx: Conversation,
|
|
650
565
|
): boolean {
|
|
651
566
|
// Execution-gate-mode wakes pin the client-context inputs so the wire tool
|
|
652
567
|
// surface matches the SOURCE conversation's live turns rather than the
|
|
@@ -780,7 +695,7 @@ export function isToolActiveForContext(
|
|
|
780
695
|
*/
|
|
781
696
|
export function createResolveToolsCallback(
|
|
782
697
|
toolDefs: ToolDefinition[],
|
|
783
|
-
ctx:
|
|
698
|
+
ctx: Conversation,
|
|
784
699
|
): ((history: Message[]) => ToolDefinition[]) | undefined {
|
|
785
700
|
if (toolDefs.length === 0) {
|
|
786
701
|
return undefined;
|
|
@@ -2286,8 +2286,8 @@ export class Conversation {
|
|
|
2286
2286
|
}
|
|
2287
2287
|
|
|
2288
2288
|
/**
|
|
2289
|
-
*
|
|
2290
|
-
*
|
|
2289
|
+
* The `transportInterface` the tool resolver reads so
|
|
2290
|
+
* `isToolActiveForContext` can gate host tools by per-capability
|
|
2291
2291
|
* `supportsHostProxy(transport, capability)`. Derived from the live turn
|
|
2292
2292
|
* interface context so it tracks the connected client across turns.
|
|
2293
2293
|
*/
|
|
@@ -81,12 +81,21 @@ export interface SearchOptions {
|
|
|
81
81
|
cursor?: string;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/** A file to attach to an outbound message, with its content already read into memory. */
|
|
85
|
+
export interface OutboundAttachment {
|
|
86
|
+
filename: string;
|
|
87
|
+
mimeType: string;
|
|
88
|
+
data: Buffer;
|
|
89
|
+
}
|
|
90
|
+
|
|
84
91
|
export interface SendOptions {
|
|
85
92
|
threadId?: string;
|
|
86
93
|
/** For email: subject line */
|
|
87
94
|
subject?: string;
|
|
88
95
|
/** For email: in-reply-to message ID */
|
|
89
96
|
inReplyTo?: string;
|
|
97
|
+
/** For email: files to attach. Providers that don't support attachments ignore this. */
|
|
98
|
+
attachments?: OutboundAttachment[];
|
|
90
99
|
/** Optional assistant scope for multi-assistant channels. */
|
|
91
100
|
assistantId?: string;
|
|
92
101
|
}
|
|
@@ -19,7 +19,18 @@ import type {
|
|
|
19
19
|
SendResult,
|
|
20
20
|
} from "../../provider-types.js";
|
|
21
21
|
import * as outlook from "./client.js";
|
|
22
|
-
import type { OutlookMessage } from "./types.js";
|
|
22
|
+
import type { OutlookMessage, OutlookSendFileAttachment } from "./types.js";
|
|
23
|
+
|
|
24
|
+
function toGraphAttachments(
|
|
25
|
+
attachments: NonNullable<SendOptions["attachments"]>,
|
|
26
|
+
): OutlookSendFileAttachment[] {
|
|
27
|
+
return attachments.map((att) => ({
|
|
28
|
+
"@odata.type": "#microsoft.graph.fileAttachment",
|
|
29
|
+
name: att.filename,
|
|
30
|
+
contentType: att.mimeType,
|
|
31
|
+
contentBytes: att.data.toString("base64"),
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
23
34
|
|
|
24
35
|
function requireConnection(
|
|
25
36
|
connection: OAuthConnection | undefined,
|
|
@@ -143,9 +154,12 @@ export const outlookMessagingProvider: MessagingProvider = {
|
|
|
143
154
|
options?: SendOptions,
|
|
144
155
|
): Promise<SendResult> {
|
|
145
156
|
const conn = requireConnection(connection);
|
|
157
|
+
const attachments = options?.attachments?.length
|
|
158
|
+
? toGraphAttachments(options.attachments)
|
|
159
|
+
: undefined;
|
|
146
160
|
|
|
147
161
|
if (options?.inReplyTo) {
|
|
148
|
-
await outlook.replyToMessage(conn, options.inReplyTo, text);
|
|
162
|
+
await outlook.replyToMessage(conn, options.inReplyTo, text, attachments);
|
|
149
163
|
return {
|
|
150
164
|
id: "",
|
|
151
165
|
timestamp: Date.now(),
|
|
@@ -159,6 +173,7 @@ export const outlookMessagingProvider: MessagingProvider = {
|
|
|
159
173
|
subject: options?.subject ?? "",
|
|
160
174
|
body: { contentType: "text", content: text },
|
|
161
175
|
toRecipients: [{ emailAddress: { address: conversationId } }],
|
|
176
|
+
...(attachments ? { attachments } : {}),
|
|
162
177
|
},
|
|
163
178
|
});
|
|
164
179
|
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
OutlookMessageRule,
|
|
18
18
|
OutlookMessageRuleListResponse,
|
|
19
19
|
OutlookRecipient,
|
|
20
|
+
OutlookSendFileAttachment,
|
|
20
21
|
OutlookSendMessagePayload,
|
|
21
22
|
OutlookUserProfile,
|
|
22
23
|
} from "./types.js";
|
|
@@ -201,18 +202,26 @@ export async function sendMessage(
|
|
|
201
202
|
});
|
|
202
203
|
}
|
|
203
204
|
|
|
204
|
-
/** Reply to an existing message. */
|
|
205
|
+
/** Reply to an existing message, optionally with file attachments. */
|
|
205
206
|
export async function replyToMessage(
|
|
206
207
|
connection: OAuthConnection,
|
|
207
208
|
messageId: string,
|
|
208
209
|
comment: string,
|
|
210
|
+
attachments?: OutlookSendFileAttachment[],
|
|
209
211
|
): Promise<void> {
|
|
212
|
+
const body: {
|
|
213
|
+
comment: string;
|
|
214
|
+
message?: { attachments: OutlookSendFileAttachment[] };
|
|
215
|
+
} = { comment };
|
|
216
|
+
if (attachments?.length) {
|
|
217
|
+
body.message = { attachments };
|
|
218
|
+
}
|
|
210
219
|
await request<void>(
|
|
211
220
|
connection,
|
|
212
221
|
`/v1.0/me/messages/${encodeURIComponent(messageId)}/reply`,
|
|
213
222
|
{
|
|
214
223
|
method: "POST",
|
|
215
|
-
body: JSON.stringify(
|
|
224
|
+
body: JSON.stringify(body),
|
|
216
225
|
},
|
|
217
226
|
);
|
|
218
227
|
}
|
|
@@ -71,6 +71,14 @@ export interface OutlookMailFolderListResponse {
|
|
|
71
71
|
"@odata.nextLink"?: string;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/** Outbound file attachment for sendMail/reply (base64-encoded content). */
|
|
75
|
+
export interface OutlookSendFileAttachment {
|
|
76
|
+
"@odata.type": "#microsoft.graph.fileAttachment";
|
|
77
|
+
name: string;
|
|
78
|
+
contentType: string;
|
|
79
|
+
contentBytes: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
74
82
|
/** Payload for sending a message via Microsoft Graph */
|
|
75
83
|
export interface OutlookSendMessagePayload {
|
|
76
84
|
message: {
|
|
@@ -78,6 +86,7 @@ export interface OutlookSendMessagePayload {
|
|
|
78
86
|
body: OutlookItemBody;
|
|
79
87
|
toRecipients: OutlookRecipient[];
|
|
80
88
|
ccRecipients?: OutlookRecipient[];
|
|
89
|
+
attachments?: OutlookSendFileAttachment[];
|
|
81
90
|
};
|
|
82
91
|
saveToSentItems?: boolean;
|
|
83
92
|
}
|
|
@@ -212,3 +212,40 @@ describe("plugin source watch", () => {
|
|
|
212
212
|
expect(versions[join(PLUGINS_DIR, "not-a-plugin")]).toBeUndefined();
|
|
213
213
|
});
|
|
214
214
|
});
|
|
215
|
+
|
|
216
|
+
describe("compilePluginApps", () => {
|
|
217
|
+
test("builds only apps that have a src/ entry point", async () => {
|
|
218
|
+
const built: string[] = [];
|
|
219
|
+
const { mock } = await import("bun:test");
|
|
220
|
+
mock.module("../../bundler/app-compiler.js", () => ({
|
|
221
|
+
compileApp: (appDir: string) => {
|
|
222
|
+
built.push(appDir);
|
|
223
|
+
return Promise.resolve({ ok: true });
|
|
224
|
+
},
|
|
225
|
+
}));
|
|
226
|
+
|
|
227
|
+
const { compilePluginApps } = await import("../plugin-source-watch.js");
|
|
228
|
+
|
|
229
|
+
const pluginDir = join(PLUGINS_DIR, "with-apps");
|
|
230
|
+
// Multi-file app (has src/) → built.
|
|
231
|
+
mkdirSync(join(pluginDir, "apps", "multi", "src"), { recursive: true });
|
|
232
|
+
writeFileSync(
|
|
233
|
+
join(pluginDir, "apps", "multi", "src", "main.tsx"),
|
|
234
|
+
"export default 1;",
|
|
235
|
+
);
|
|
236
|
+
// Single-file app (index.html, no src/) → skipped.
|
|
237
|
+
mkdirSync(join(pluginDir, "apps", "single"), { recursive: true });
|
|
238
|
+
writeFileSync(join(pluginDir, "apps", "single", "index.html"), "<div/>");
|
|
239
|
+
|
|
240
|
+
await compilePluginApps(pluginDir);
|
|
241
|
+
|
|
242
|
+
expect(built).toEqual([join(pluginDir, "apps", "multi")]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("no-ops when the plugin bundles no apps", async () => {
|
|
246
|
+
const { compilePluginApps } = await import("../plugin-source-watch.js");
|
|
247
|
+
const pluginDir = join(PLUGINS_DIR, "no-apps");
|
|
248
|
+
mkdirSync(pluginDir, { recursive: true });
|
|
249
|
+
await expect(compilePluginApps(pluginDir)).resolves.toBeUndefined();
|
|
250
|
+
});
|
|
251
|
+
});
|