@vellumai/assistant 0.12.0-staging.1 → 0.12.0
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/Dockerfile +5 -0
- package/docs/architecture/turn-actor.md +9 -0
- package/knip.json +1 -0
- package/node_modules/@vellumai/app-icons/package.json +18 -0
- package/node_modules/@vellumai/app-icons/src/index.test.ts +85 -0
- package/node_modules/@vellumai/app-icons/src/index.ts +387 -0
- package/node_modules/@vellumai/app-icons/tsconfig.json +20 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +31 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +28 -3
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +31 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +28 -3
- package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +10 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +15 -2
- package/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +31 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +28 -3
- package/openapi.yaml +628 -0
- package/package.json +3 -1
- package/scripts/smoke-container-workspace-dependencies.ts +19 -0
- package/src/__tests__/app-builder-icon-names.test.ts +29 -0
- package/src/__tests__/assistant-attachment-directive.test.ts +4 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop.test.ts +2 -0
- package/src/__tests__/conversation-attachments.test.ts +142 -0
- package/src/__tests__/conversation-delete-activation-progress.test.ts +145 -0
- package/src/__tests__/conversation-event-sink.test.ts +50 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +10 -2
- package/src/__tests__/conversation-queue.test.ts +297 -0
- package/src/__tests__/credential-routes.test.ts +185 -6
- package/src/__tests__/drain-kick-guard.test.ts +2 -0
- package/src/__tests__/drain-requeue-on-contention.test.ts +6 -0
- package/src/__tests__/messaging-send-tool.test.ts +42 -0
- package/src/__tests__/oauth-commands-routes.test.ts +47 -0
- package/src/__tests__/subagent-manager-notify.test.ts +25 -4
- package/src/activation/progress-store.test.ts +1564 -0
- package/src/activation/progress-store.ts +1296 -0
- package/src/activation/turn-hooks.test.ts +246 -0
- package/src/activation/turn-hooks.ts +126 -0
- package/src/api/events/subagent-status-changed.ts +7 -5
- package/src/api/responses/activation.ts +136 -0
- package/src/apps/app-store.ts +8 -0
- package/src/cli/__tests__/catalog-search-help.test.ts +7 -5
- package/src/cli/commands/__tests__/cli-test-harness.ts +12 -3
- package/src/cli/commands/channels/__tests__/channels.test.ts +2 -0
- package/src/cli/commands/channels/__tests__/request.test.ts +191 -0
- package/src/cli/commands/channels/index.help.ts +70 -18
- package/src/cli/commands/channels/index.ts +17 -6
- package/src/cli/commands/channels/request.ts +66 -0
- package/src/cli/commands/oauth/request.test.ts +2 -0
- package/src/cli/commands/oauth/request.ts +227 -186
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -1
- package/src/config/bundled-skills/app-builder/TOOLS.json +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +8 -4
- package/src/config/feature-flag-registry.json +35 -5
- package/src/daemon/assistant-attachments.ts +11 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +5 -0
- package/src/daemon/conversation-agent-loop.ts +71 -3
- package/src/daemon/conversation-attachments.ts +42 -1
- package/src/daemon/conversation-event-sink.ts +25 -0
- package/src/daemon/conversation-process.ts +69 -21
- package/src/daemon/conversation-store.ts +4 -3
- package/src/daemon/conversation-surfaces.ts +19 -4
- package/src/daemon/message-types/sync.ts +2 -0
- package/src/ipc/assistant-server.ts +2 -0
- package/src/ipc/routes/__tests__/activation-sync-ipc-routes.test.ts +51 -0
- package/src/ipc/routes/activation-sync-ipc-routes.ts +42 -0
- package/src/notifications/AGENTS.md +1 -1
- package/src/notifications/__tests__/proactive-home-thread.test.ts +111 -0
- package/src/notifications/conversation-pairing.ts +47 -5
- package/src/notifications/delivered-post-record.ts +3 -0
- package/src/persistence/__tests__/slack-thread-root-evidence.test.ts +102 -0
- package/src/persistence/conversation-crud.ts +39 -0
- package/src/persistence/delivery-crud.ts +13 -0
- package/src/plugins/AGENTS.md +1 -0
- package/src/runtime/auth/__tests__/route-policy.test.ts +37 -0
- package/src/runtime/routes/__tests__/user-routes-notices.test.ts +248 -0
- package/src/runtime/routes/activation-routes.test.ts +415 -0
- package/src/runtime/routes/activation-routes.ts +172 -0
- package/src/runtime/routes/credential-routes.ts +46 -3
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/oauth-commands-routes.ts +21 -8
- package/src/runtime/routes/platform-managed-credentials.ts +48 -0
- package/src/runtime/routes/secret-routes.ts +3 -12
- package/src/runtime/routes/user-route-resolution.ts +21 -0
- package/src/runtime/routes/user-routes.ts +112 -9
- package/src/runtime/sync/activation-sidecar-publish.test.ts +78 -0
- package/src/runtime/sync/documents-sidecar-publish.test.ts +3 -0
- package/src/runtime/sync/resource-sync-events.ts +23 -0
- package/src/runtime/sync/worker-daemon-notify.test.ts +36 -0
- package/src/runtime/sync/worker-daemon-notify.ts +39 -1
- package/src/tools/apps/executors.ts +8 -8
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A scheduled or background turn working a checklist task runs in a sidecar
|
|
3
|
+
* worker, whose local hub has no SSE subscriber. The activation-progress
|
|
4
|
+
* invalidation has to be handed to the daemon there, or the checklist row
|
|
5
|
+
* keeps showing Working on every client until something else refetches it.
|
|
6
|
+
*
|
|
7
|
+
* The two branches are told apart by what reaches the local hub: the daemon
|
|
8
|
+
* publishes there, the worker publishes nothing and calls the daemon over
|
|
9
|
+
* IPC instead (that hand-off is covered in `worker-daemon-notify.test.ts`).
|
|
10
|
+
* No module is mocked here, so nothing this file does can reach a sibling
|
|
11
|
+
* file bun runs in the same process.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
15
|
+
|
|
16
|
+
import type { AssistantEventEnvelope } from "../../api/index.js";
|
|
17
|
+
import { assistantEventHub } from "../assistant-event-hub.js";
|
|
18
|
+
import {
|
|
19
|
+
_resetStreamStateForTesting,
|
|
20
|
+
disableStreamSeqStamping,
|
|
21
|
+
} from "../assistant-stream-state.js";
|
|
22
|
+
import { publishActivationProgressChanged } from "./resource-sync-events.js";
|
|
23
|
+
|
|
24
|
+
async function settle(): Promise<void> {
|
|
25
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Run `publish`, then return the `sync_changed` events it put on the hub. */
|
|
29
|
+
async function captureLocalPublishes(
|
|
30
|
+
publish: () => void,
|
|
31
|
+
): Promise<AssistantEventEnvelope[]> {
|
|
32
|
+
await settle();
|
|
33
|
+
const received: AssistantEventEnvelope[] = [];
|
|
34
|
+
const subscription = assistantEventHub.subscribe({
|
|
35
|
+
type: "process",
|
|
36
|
+
callback: (event) => {
|
|
37
|
+
if (event.message.type === "sync_changed") {
|
|
38
|
+
received.push(event);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
try {
|
|
43
|
+
publish();
|
|
44
|
+
await settle();
|
|
45
|
+
return received;
|
|
46
|
+
} finally {
|
|
47
|
+
subscription.dispose();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe("publishActivationProgressChanged", () => {
|
|
52
|
+
beforeEach(async () => {
|
|
53
|
+
await settle();
|
|
54
|
+
_resetStreamStateForTesting();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("publishes on the local hub in the daemon, where subscribers live", async () => {
|
|
58
|
+
const events = await captureLocalPublishes(() => {
|
|
59
|
+
publishActivationProgressChanged("client-a");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(events).toHaveLength(1);
|
|
63
|
+
expect(events[0].message).toMatchObject({
|
|
64
|
+
type: "sync_changed",
|
|
65
|
+
tags: ["activation:progress"],
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("publishes nothing locally in a sidecar worker", async () => {
|
|
70
|
+
disableStreamSeqStamping();
|
|
71
|
+
|
|
72
|
+
const events = await captureLocalPublishes(() => {
|
|
73
|
+
publishActivationProgressChanged();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(events).toEqual([]);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -13,6 +13,9 @@ mock.module("./worker-daemon-notify.js", () => ({
|
|
|
13
13
|
NOTIFY_CONVERSATION_PERSISTED_IPC_METHOD:
|
|
14
14
|
"notify_conversation_persisted_externally",
|
|
15
15
|
NOTIFY_DOCUMENTS_CHANGED_IPC_METHOD: "notify_documents_changed_externally",
|
|
16
|
+
NOTIFY_ACTIVATION_PROGRESS_CHANGED_IPC_METHOD:
|
|
17
|
+
"notify_activation_progress_changed_externally",
|
|
18
|
+
notifyDaemonActivationProgressChanged: async () => {},
|
|
16
19
|
notifyDaemonConversationPersisted: async () => {},
|
|
17
20
|
notifyDaemonDocumentsChanged: async () => {
|
|
18
21
|
notifyCount++;
|
|
@@ -10,6 +10,7 @@ import { broadcastMessage } from "../assistant-event-hub.js";
|
|
|
10
10
|
import { isStreamSeqStampingDisabled } from "../assistant-stream-state.js";
|
|
11
11
|
import { publishSyncInvalidation } from "./sync-publisher.js";
|
|
12
12
|
import {
|
|
13
|
+
notifyDaemonActivationProgressChanged,
|
|
13
14
|
notifyDaemonConversationPersisted,
|
|
14
15
|
notifyDaemonDocumentsChanged,
|
|
15
16
|
} from "./worker-daemon-notify.js";
|
|
@@ -150,6 +151,28 @@ export function publishPluginsChanged(originClientId?: string): void {
|
|
|
150
151
|
void publishSyncInvalidation([SYNC_TAGS.pluginsList], originClientId);
|
|
151
152
|
}
|
|
152
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Invalidate the activation-checklist progress resource on every client.
|
|
156
|
+
*
|
|
157
|
+
* Reached from the routes the client writes through and from the turn hooks
|
|
158
|
+
* that count a launched task's steps. Those hooks run inside whichever
|
|
159
|
+
* process is driving the turn, and a scheduled or background turn runs in a
|
|
160
|
+
* sidecar worker (seq stamping disabled) whose local hub has no SSE
|
|
161
|
+
* subscribers, so a local publish would reach nobody and the checklist row
|
|
162
|
+
* would sit on Working until the client refetched for another reason. Hand
|
|
163
|
+
* off to the daemon there, as the documents list does. A worker turn has no
|
|
164
|
+
* originating client, so no `originClientId` is forwarded.
|
|
165
|
+
*/
|
|
166
|
+
export function publishActivationProgressChanged(
|
|
167
|
+
originClientId?: string,
|
|
168
|
+
): void {
|
|
169
|
+
if (isStreamSeqStampingDisabled()) {
|
|
170
|
+
void notifyDaemonActivationProgressChanged();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
void publishSyncInvalidation([SYNC_TAGS.activationProgress], originClientId);
|
|
174
|
+
}
|
|
175
|
+
|
|
153
176
|
/**
|
|
154
177
|
* Reasons that change the *shape* of the conversation list — a row is
|
|
155
178
|
* added, removed, or its position changes. These require web clients to
|
|
@@ -32,8 +32,10 @@ mock.module("../../ipc/cli-client.js", () => ({
|
|
|
32
32
|
}));
|
|
33
33
|
|
|
34
34
|
import {
|
|
35
|
+
NOTIFY_ACTIVATION_PROGRESS_CHANGED_IPC_METHOD,
|
|
35
36
|
NOTIFY_CONVERSATION_PERSISTED_IPC_METHOD,
|
|
36
37
|
NOTIFY_DOCUMENTS_CHANGED_IPC_METHOD,
|
|
38
|
+
notifyDaemonActivationProgressChanged,
|
|
37
39
|
notifyDaemonConversationPersisted,
|
|
38
40
|
notifyDaemonDocumentsChanged,
|
|
39
41
|
} from "./worker-daemon-notify.js";
|
|
@@ -101,3 +103,37 @@ describe("notifyDaemonDocumentsChanged", () => {
|
|
|
101
103
|
expect(calls).toHaveLength(1);
|
|
102
104
|
});
|
|
103
105
|
});
|
|
106
|
+
|
|
107
|
+
describe("notifyDaemonActivationProgressChanged", () => {
|
|
108
|
+
beforeEach(() => {
|
|
109
|
+
calls.length = 0;
|
|
110
|
+
nextResult = { ok: true };
|
|
111
|
+
shouldThrow = false;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("asks the daemon to republish over the shared IPC method", async () => {
|
|
115
|
+
await notifyDaemonActivationProgressChanged();
|
|
116
|
+
|
|
117
|
+
expect(calls).toHaveLength(1);
|
|
118
|
+
expect(calls[0]!.method).toBe(
|
|
119
|
+
NOTIFY_ACTIVATION_PROGRESS_CHANGED_IPC_METHOD,
|
|
120
|
+
);
|
|
121
|
+
expect(calls[0]!.params).toEqual({ body: {} });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("swallows a failed IPC result (best-effort, no throw)", async () => {
|
|
125
|
+
nextResult = { ok: false, error: "daemon unreachable" };
|
|
126
|
+
|
|
127
|
+
const result = await notifyDaemonActivationProgressChanged();
|
|
128
|
+
expect(result).toBeUndefined();
|
|
129
|
+
expect(calls).toHaveLength(1);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("swallows a thrown IPC error (best-effort, no throw)", async () => {
|
|
133
|
+
shouldThrow = true;
|
|
134
|
+
|
|
135
|
+
const result = await notifyDaemonActivationProgressChanged();
|
|
136
|
+
expect(result).toBeUndefined();
|
|
137
|
+
expect(calls).toHaveLength(1);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* live in the daemon). A worker hands the notification here instead and the
|
|
8
8
|
* daemon republishes it to real subscribers (see
|
|
9
9
|
* `ipc/routes/conversation-sync-ipc-routes.ts`,
|
|
10
|
-
* `ipc/routes/documents-sync-ipc-routes.ts
|
|
10
|
+
* `ipc/routes/documents-sync-ipc-routes.ts`,
|
|
11
|
+
* `ipc/routes/activation-sync-ipc-routes.ts`).
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import { cliIpcCall } from "../../ipc/cli-client.js";
|
|
@@ -30,6 +31,14 @@ export const NOTIFY_CONVERSATION_PERSISTED_IPC_METHOD =
|
|
|
30
31
|
export const NOTIFY_DOCUMENTS_CHANGED_IPC_METHOD =
|
|
31
32
|
"notify_documents_changed_externally";
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* IPC method the daemon exposes for the activation-progress hand-off. Shared
|
|
36
|
+
* by the worker caller and the daemon route registration so the wire name
|
|
37
|
+
* cannot drift.
|
|
38
|
+
*/
|
|
39
|
+
export const NOTIFY_ACTIVATION_PROGRESS_CHANGED_IPC_METHOD =
|
|
40
|
+
"notify_activation_progress_changed_externally";
|
|
41
|
+
|
|
33
42
|
/**
|
|
34
43
|
* Short timeout: this is a fire-and-forget invalidation, not a request whose
|
|
35
44
|
* result the worker consumes. A busy or unreachable daemon simply leaves the
|
|
@@ -99,3 +108,32 @@ export async function notifyDaemonDocumentsChanged(): Promise<void> {
|
|
|
99
108
|
log.debug({ err }, "daemon documents-changed notify failed");
|
|
100
109
|
}
|
|
101
110
|
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Ask the daemon to republish the activation-progress invalidation.
|
|
114
|
+
*
|
|
115
|
+
* A scheduled or background turn working a checklist task moves that task's
|
|
116
|
+
* step count and finishes it, but the worker's own broadcast reaches nobody.
|
|
117
|
+
* This is the path that lets the checklist row stop showing Working.
|
|
118
|
+
*
|
|
119
|
+
* Best-effort by design, like the other hand-offs: an unreachable daemon is
|
|
120
|
+
* logged at debug and swallowed, and the client picks the change up on its
|
|
121
|
+
* next progress fetch.
|
|
122
|
+
*/
|
|
123
|
+
export async function notifyDaemonActivationProgressChanged(): Promise<void> {
|
|
124
|
+
try {
|
|
125
|
+
const result = await cliIpcCall(
|
|
126
|
+
NOTIFY_ACTIVATION_PROGRESS_CHANGED_IPC_METHOD,
|
|
127
|
+
{ body: {} },
|
|
128
|
+
{ timeoutMs: NOTIFY_TIMEOUT_MS },
|
|
129
|
+
);
|
|
130
|
+
if (!result.ok) {
|
|
131
|
+
log.debug(
|
|
132
|
+
{ error: result.error },
|
|
133
|
+
"daemon activation-progress notify was not acknowledged",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
} catch (err) {
|
|
137
|
+
log.debug({ err }, "daemon activation-progress notify failed");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* ToolDefinition or ToolContext types.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { normalizeAppIcon } from "@vellumai/app-icons";
|
|
12
|
+
|
|
11
13
|
import type { AppDefinition } from "../../apps/app-store.js";
|
|
12
14
|
import { getAppDirPath } from "../../apps/app-store.js";
|
|
13
15
|
import { compileApp } from "../../bundler/app-compiler.js";
|
|
@@ -208,14 +210,12 @@ export async function executeAppCreate(
|
|
|
208
210
|
return sourceFilesError;
|
|
209
211
|
}
|
|
210
212
|
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
| undefined;
|
|
218
|
-
const icon = rawIcon && !rawIcon.startsWith("http") ? rawIcon : undefined;
|
|
213
|
+
// The app's icon comes from preview.icon: a name from the app icon
|
|
214
|
+
// registry (`@vellumai/app-icons`), or an emoji the registry maps. URLs are
|
|
215
|
+
// dropped there; they would render as raw strings in UI and bundle
|
|
216
|
+
// manifests. Lenient alias: a top-level `icon` is folded in when
|
|
217
|
+
// preview.icon is absent.
|
|
218
|
+
const icon = normalizeAppIcon(preview?.icon ?? input.icon);
|
|
219
219
|
|
|
220
220
|
const app = store.createApp({
|
|
221
221
|
name,
|