@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.
Files changed (92) hide show
  1. package/Dockerfile +5 -0
  2. package/docs/architecture/turn-actor.md +9 -0
  3. package/knip.json +1 -0
  4. package/node_modules/@vellumai/app-icons/package.json +18 -0
  5. package/node_modules/@vellumai/app-icons/src/index.test.ts +85 -0
  6. package/node_modules/@vellumai/app-icons/src/index.ts +387 -0
  7. package/node_modules/@vellumai/app-icons/tsconfig.json +20 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +31 -0
  9. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +28 -3
  10. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +31 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +28 -3
  12. package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +10 -0
  13. package/node_modules/@vellumai/gateway-client/src/index.ts +1 -0
  14. package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +15 -2
  15. package/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +31 -0
  16. package/node_modules/@vellumai/service-contracts/src/channels.ts +28 -3
  17. package/openapi.yaml +628 -0
  18. package/package.json +3 -1
  19. package/scripts/smoke-container-workspace-dependencies.ts +19 -0
  20. package/src/__tests__/app-builder-icon-names.test.ts +29 -0
  21. package/src/__tests__/assistant-attachment-directive.test.ts +4 -0
  22. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  23. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  24. package/src/__tests__/conversation-agent-loop.test.ts +2 -0
  25. package/src/__tests__/conversation-attachments.test.ts +142 -0
  26. package/src/__tests__/conversation-delete-activation-progress.test.ts +145 -0
  27. package/src/__tests__/conversation-event-sink.test.ts +50 -0
  28. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +10 -2
  29. package/src/__tests__/conversation-queue.test.ts +297 -0
  30. package/src/__tests__/credential-routes.test.ts +185 -6
  31. package/src/__tests__/drain-kick-guard.test.ts +2 -0
  32. package/src/__tests__/drain-requeue-on-contention.test.ts +6 -0
  33. package/src/__tests__/messaging-send-tool.test.ts +42 -0
  34. package/src/__tests__/oauth-commands-routes.test.ts +47 -0
  35. package/src/__tests__/subagent-manager-notify.test.ts +25 -4
  36. package/src/activation/progress-store.test.ts +1564 -0
  37. package/src/activation/progress-store.ts +1296 -0
  38. package/src/activation/turn-hooks.test.ts +246 -0
  39. package/src/activation/turn-hooks.ts +126 -0
  40. package/src/api/events/subagent-status-changed.ts +7 -5
  41. package/src/api/responses/activation.ts +136 -0
  42. package/src/apps/app-store.ts +8 -0
  43. package/src/cli/__tests__/catalog-search-help.test.ts +7 -5
  44. package/src/cli/commands/__tests__/cli-test-harness.ts +12 -3
  45. package/src/cli/commands/channels/__tests__/channels.test.ts +2 -0
  46. package/src/cli/commands/channels/__tests__/request.test.ts +191 -0
  47. package/src/cli/commands/channels/index.help.ts +70 -18
  48. package/src/cli/commands/channels/index.ts +17 -6
  49. package/src/cli/commands/channels/request.ts +66 -0
  50. package/src/cli/commands/oauth/request.test.ts +2 -0
  51. package/src/cli/commands/oauth/request.ts +227 -186
  52. package/src/config/bundled-skills/app-builder/SKILL.md +3 -1
  53. package/src/config/bundled-skills/app-builder/TOOLS.json +2 -2
  54. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +8 -4
  55. package/src/config/feature-flag-registry.json +35 -5
  56. package/src/daemon/assistant-attachments.ts +11 -0
  57. package/src/daemon/conversation-agent-loop-handlers.ts +5 -0
  58. package/src/daemon/conversation-agent-loop.ts +71 -3
  59. package/src/daemon/conversation-attachments.ts +42 -1
  60. package/src/daemon/conversation-event-sink.ts +25 -0
  61. package/src/daemon/conversation-process.ts +69 -21
  62. package/src/daemon/conversation-store.ts +4 -3
  63. package/src/daemon/conversation-surfaces.ts +19 -4
  64. package/src/daemon/message-types/sync.ts +2 -0
  65. package/src/ipc/assistant-server.ts +2 -0
  66. package/src/ipc/routes/__tests__/activation-sync-ipc-routes.test.ts +51 -0
  67. package/src/ipc/routes/activation-sync-ipc-routes.ts +42 -0
  68. package/src/notifications/AGENTS.md +1 -1
  69. package/src/notifications/__tests__/proactive-home-thread.test.ts +111 -0
  70. package/src/notifications/conversation-pairing.ts +47 -5
  71. package/src/notifications/delivered-post-record.ts +3 -0
  72. package/src/persistence/__tests__/slack-thread-root-evidence.test.ts +102 -0
  73. package/src/persistence/conversation-crud.ts +39 -0
  74. package/src/persistence/delivery-crud.ts +13 -0
  75. package/src/plugins/AGENTS.md +1 -0
  76. package/src/runtime/auth/__tests__/route-policy.test.ts +37 -0
  77. package/src/runtime/routes/__tests__/user-routes-notices.test.ts +248 -0
  78. package/src/runtime/routes/activation-routes.test.ts +415 -0
  79. package/src/runtime/routes/activation-routes.ts +172 -0
  80. package/src/runtime/routes/credential-routes.ts +46 -3
  81. package/src/runtime/routes/index.ts +2 -0
  82. package/src/runtime/routes/oauth-commands-routes.ts +21 -8
  83. package/src/runtime/routes/platform-managed-credentials.ts +48 -0
  84. package/src/runtime/routes/secret-routes.ts +3 -12
  85. package/src/runtime/routes/user-route-resolution.ts +21 -0
  86. package/src/runtime/routes/user-routes.ts +112 -9
  87. package/src/runtime/sync/activation-sidecar-publish.test.ts +78 -0
  88. package/src/runtime/sync/documents-sidecar-publish.test.ts +3 -0
  89. package/src/runtime/sync/resource-sync-events.ts +23 -0
  90. package/src/runtime/sync/worker-daemon-notify.test.ts +36 -0
  91. package/src/runtime/sync/worker-daemon-notify.ts +39 -1
  92. 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
- // Extract icon from preview if provided - only persist emoji-like values,
212
- // not URLs which would render as raw strings in UI and bundle manifests.
213
- // Lenient alias: a top-level `icon` is folded in when preview.icon is absent.
214
- const rawIcon = (preview?.icon ??
215
- (typeof input.icon === "string" ? input.icon : undefined)) as
216
- | string
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,