@vellumai/assistant 0.9.0-dev.202606181513.fd39213 → 0.9.0-dev.202606181735.ae6e1d8

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 (41) hide show
  1. package/openapi.yaml +82 -0
  2. package/package.json +1 -1
  3. package/src/__tests__/plugin-tool-contribution.test.ts +6 -3
  4. package/src/api/events/workflow-completed.ts +53 -0
  5. package/src/api/events/workflow-leaf-finished.ts +38 -0
  6. package/src/api/events/workflow-leaf-started.ts +35 -0
  7. package/src/api/events/workflow-progress.ts +32 -0
  8. package/src/api/events/workflow-started.ts +31 -0
  9. package/src/api/index.ts +38 -0
  10. package/src/api/responses/workflow-journal.ts +53 -0
  11. package/src/daemon/message-types/workflows.ts +83 -1
  12. package/src/memory/db-init.ts +2 -0
  13. package/src/memory/migrations/293-workflow-journal-leaf-tokens.ts +32 -0
  14. package/src/memory/migrations/index.ts +1 -0
  15. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +43 -0
  16. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +134 -0
  17. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +147 -0
  18. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +138 -0
  19. package/src/plugins/defaults/advisor/__tests__/transcript.test.ts +147 -0
  20. package/src/plugins/defaults/advisor/advisor-state-store.ts +94 -0
  21. package/src/plugins/defaults/advisor/config.ts +26 -0
  22. package/src/plugins/defaults/advisor/consult.ts +76 -0
  23. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +34 -0
  24. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +24 -0
  25. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +19 -0
  26. package/src/plugins/defaults/advisor/package.json +14 -0
  27. package/src/plugins/defaults/advisor/steering.ts +51 -0
  28. package/src/plugins/defaults/advisor/tools/advisor.ts +52 -0
  29. package/src/plugins/defaults/advisor/transcript.ts +76 -0
  30. package/src/plugins/defaults/index.ts +35 -0
  31. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +3 -3
  32. package/src/runtime/routes/inbound-stages/background-dispatch.ts +1 -1
  33. package/src/runtime/routes/workflow-routes.test.ts +225 -1
  34. package/src/runtime/routes/workflow-routes.ts +131 -1
  35. package/src/tools/workflows/run-workflow.ts +1 -0
  36. package/src/workflows/engine.test.ts +175 -1
  37. package/src/workflows/engine.ts +82 -0
  38. package/src/workflows/journal-store.test.ts +70 -0
  39. package/src/workflows/journal-store.ts +18 -3
  40. package/src/workflows/run-manager.test.ts +171 -3
  41. package/src/workflows/run-manager.ts +63 -0
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Run one advisor consult.
3
+ *
4
+ * Routed entirely through the assistant's own inference: `getConfiguredProvider`
5
+ * resolves the general-purpose `inference` call site (with the advisor profile
6
+ * applied as an `overrideProfile`) to a provider/model/credentials from the
7
+ * configured profiles — managed-proxy or BYOK, no separate API key. The consult
8
+ * then runs a tool-less, capped one-shot completion through `provider.sendMessage`
9
+ * and returns the text.
10
+ */
11
+
12
+ import type { LLMCallSite } from "../../../config/schemas/llm.js";
13
+ import {
14
+ extractAllText,
15
+ getConfiguredProvider,
16
+ userMessage,
17
+ } from "../../../providers/provider-send-message.js";
18
+ import type { Message } from "../../../providers/types.js";
19
+ import { ADVISOR_CONFIG } from "./config.js";
20
+ import { advisorRequestText, buildAdvisorSystem } from "./steering.js";
21
+ import { toAdvisorMessages } from "./transcript.js";
22
+
23
+ // The general-purpose call site; the model is selected via `overrideProfile`.
24
+ const ADVISOR_CALL_SITE: LLMCallSite = "inference";
25
+
26
+ export interface ConsultParams {
27
+ systemPrompt: string | null;
28
+ messages: ReadonlyArray<Message>;
29
+ signal?: AbortSignal;
30
+ }
31
+
32
+ /** Combine the caller's signal with a consult timeout. */
33
+ function withTimeout(signal: AbortSignal | undefined, ms: number): AbortSignal {
34
+ const timeout = AbortSignal.timeout(ms);
35
+ return signal ? AbortSignal.any([signal, timeout]) : timeout;
36
+ }
37
+
38
+ /**
39
+ * Returns the advisor's guidance text, or a short benign notice when the
40
+ * advisor can't run. Callers should surface the string as a non-error tool
41
+ * result so the executor continues regardless.
42
+ */
43
+ export async function consultAdvisor(params: ConsultParams): Promise<string> {
44
+ const history = toAdvisorMessages(params.messages);
45
+ if (history.length === 0) {
46
+ return "(advisor: no conversation context is available yet)";
47
+ }
48
+
49
+ const provider = await getConfiguredProvider(ADVISOR_CALL_SITE, {
50
+ overrideProfile: ADVISOR_CONFIG.profile,
51
+ });
52
+ if (!provider) {
53
+ return "(advisor unavailable: no inference provider is configured)";
54
+ }
55
+
56
+ // Append the consult instruction as the final user turn, then run a
57
+ // tool-less, capped completion through the resolved provider.
58
+ const messages: Message[] = [
59
+ ...history,
60
+ userMessage(advisorRequestText(ADVISOR_CONFIG.wordLimit)),
61
+ ];
62
+
63
+ const response = await provider.sendMessage(messages, {
64
+ systemPrompt: buildAdvisorSystem(params.systemPrompt),
65
+ config: {
66
+ callSite: ADVISOR_CALL_SITE,
67
+ overrideProfile: ADVISOR_CONFIG.profile,
68
+ tool_choice: { type: "none" },
69
+ max_tokens: ADVISOR_CONFIG.maxTokens,
70
+ },
71
+ signal: withTimeout(params.signal, ADVISOR_CONFIG.timeoutMs),
72
+ });
73
+
74
+ const advice = extractAllText(response).trim();
75
+ return advice.length > 0 ? advice : "(advisor returned no guidance)";
76
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `post-model-call` hook: snapshot the transcript the executor just saw. Fires
3
+ * after the model returns its message (which may carry the `advisor` tool_use)
4
+ * but before tools run, so when `advisor.execute()` runs an instant later it
5
+ * reads exactly this snapshot.
6
+ *
7
+ * Pure observer: never mutates `content` or the continue/stop `decision`. Gated
8
+ * to the user-facing reply, so background/subagent/compaction calls — and the
9
+ * advisor's own `inference`-call-site sub-call — are ignored.
10
+ */
11
+
12
+ import type { PluginHookFn, PostModelCallContext } from "@vellumai/plugin-api";
13
+
14
+ import { recordMessages } from "../advisor-state-store.js";
15
+
16
+ const postModelCall: PluginHookFn<PostModelCallContext> = async (ctx) => {
17
+ if (ctx.callSite !== "mainAgent") return;
18
+ if (ctx.error) return;
19
+ // `ctx.messages` is the pre-reply history; the turn the model just produced —
20
+ // including any text/plan it wrote before the `advisor` tool_use — lives in
21
+ // `ctx.content` and is not yet in `messages`. Append it (cloned) so the
22
+ // advisor reviews the full current transcript, not just the prior history.
23
+ // `transcript.ts` strips the pending tool_use from this final assistant turn.
24
+ const messages =
25
+ ctx.content.length > 0
26
+ ? [
27
+ ...ctx.messages,
28
+ { role: "assistant" as const, content: [...ctx.content] },
29
+ ]
30
+ : ctx.messages;
31
+ recordMessages(ctx.conversationId, messages);
32
+ };
33
+
34
+ export default postModelCall;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `pre-model-call` hook: for the user-facing reply (`mainAgent`), capture the
3
+ * executor's system prompt (steering stripped) so the advisor can be given it
4
+ * as context, and inject the advisor steering so the model reaches for the
5
+ * tool. Idempotent via the steering marker.
6
+ */
7
+
8
+ import type { PluginHookFn, PreModelCallContext } from "@vellumai/plugin-api";
9
+
10
+ import { recordSystemPrompt } from "../advisor-state-store.js";
11
+ import { ADVISOR_CONFIG } from "../config.js";
12
+ import { appendSteering, stripSteering } from "../steering.js";
13
+
14
+ const preModelCall: PluginHookFn<PreModelCallContext> = async (ctx) => {
15
+ if (ctx.callSite !== "mainAgent") return;
16
+
17
+ recordSystemPrompt(ctx.conversationId, stripSteering(ctx.systemPrompt));
18
+
19
+ if (ADVISOR_CONFIG.steeringEnabled) {
20
+ ctx.systemPrompt = appendSteering(ctx.systemPrompt);
21
+ }
22
+ };
23
+
24
+ export default preModelCall;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `user-prompt-submit` hook: seed the per-conversation capture at the start of a
3
+ * user turn with the inbound history, so an advisor call on the very first model
4
+ * turn still has context even before `post-model-call` snapshots the running
5
+ * transcript.
6
+ */
7
+
8
+ import type {
9
+ PluginHookFn,
10
+ UserPromptSubmitContext,
11
+ } from "@vellumai/plugin-api";
12
+
13
+ import { seedCapture } from "../advisor-state-store.js";
14
+
15
+ const userPromptSubmit: PluginHookFn<UserPromptSubmitContext> = async (ctx) => {
16
+ seedCapture(ctx.conversationId, ctx.latestMessages);
17
+ };
18
+
19
+ export default userPromptSubmit;
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "default-advisor",
3
+ "version": "1.0.0",
4
+ "description": "First-party default plugin that adds an `advisor` tool: a no-argument tool the model calls mid-task to consult a stronger inference profile on the full conversation transcript for strategic guidance, routed through the assistant's own inference.",
5
+ "private": true,
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "engines": {
9
+ "node": ">=20.12.0"
10
+ },
11
+ "peerDependencies": {
12
+ "@vellumai/plugin-api": "^0.8.0"
13
+ }
14
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Prompt fragments for the advisor plugin:
3
+ * - `appendSteering` / `stripSteering` — the executor-facing steering block the
4
+ * `pre-model-call` hook injects so the model reaches for the advisor tool at
5
+ * the right times.
6
+ * - `buildAdvisorSystem` / `advisorRequestText` — the advisor-facing framing for
7
+ * the consult itself.
8
+ */
9
+
10
+ /** Idempotency marker; the steering block is appended at the end of the prompt. */
11
+ export const STEERING_MARKER = "<!-- advisor:steering -->";
12
+
13
+ const STEERING_BODY = `You have an \`advisor\` tool backed by a stronger reviewer model. It takes NO parameters — calling it forwards your entire conversation automatically (the task, every tool call, every result). Call advisor BEFORE substantive work — before writing, before committing to an interpretation, before building on an assumption. Orientation (reading files, fetching a source) is not substantive work; do that first, then call advisor. Also call it when stuck, when changing approach, and once before you declare the task done. Give its guidance serious weight; only override it when primary-source evidence contradicts a specific claim, and say so when you do.`;
14
+
15
+ const ADVISOR_STEERING = `${STEERING_MARKER}\n${STEERING_BODY}`;
16
+
17
+ /** Append the steering block to the executor's system prompt (idempotent). */
18
+ export function appendSteering(systemPrompt: string | null): string | null {
19
+ if (systemPrompt === null) return null;
20
+ if (systemPrompt.includes(STEERING_MARKER)) return systemPrompt;
21
+ return `${systemPrompt}\n\n${ADVISOR_STEERING}`;
22
+ }
23
+
24
+ /** Remove a previously-appended steering block, recovering the original prompt. */
25
+ export function stripSteering(systemPrompt: string | null): string | null {
26
+ if (systemPrompt === null) return null;
27
+ const idx = systemPrompt.indexOf(STEERING_MARKER);
28
+ if (idx === -1) return systemPrompt;
29
+ return systemPrompt.slice(0, idx).trimEnd();
30
+ }
31
+
32
+ /**
33
+ * System prompt for the advisor sub-call. Frames the advisor's role and, for
34
+ * context, quotes the executor's own system prompt (as the advisor tool does —
35
+ * the advisor sees the system prompt as context about the executor's task).
36
+ */
37
+ export function buildAdvisorSystem(
38
+ originalSystemPrompt: string | null,
39
+ ): string {
40
+ const base = `You are a senior technical advisor consulted by another AI agent partway through a task. You can see the agent's full conversation above: its task, every tool call it has made, and every result it has seen. Give concise, high-leverage strategic guidance — the right approach, the key risk or failure mode to avoid, and the single most important next step. Do not role-play as the agent or produce its final deliverable; advise it. If the agent is already on track, say so briefly and sharpen its plan.`;
41
+ if (!originalSystemPrompt) return base;
42
+ return `${base}\n\nFor context, the agent is operating under this system prompt:\n<agent_system_prompt>\n${originalSystemPrompt}\n</agent_system_prompt>`;
43
+ }
44
+
45
+ /**
46
+ * The final user turn appended to the transcript for the advisor sub-call. Asks
47
+ * for guidance and carries the soft word-limit nudge.
48
+ */
49
+ export function advisorRequestText(wordLimit: number): string {
50
+ return `Review the conversation above — the task, the tool calls, and their results — and give focused strategic guidance on how to proceed. (Advisor: keep your guidance under ~${wordLimit} words — a focused starting point, not a comprehensive plan.)`;
51
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The `advisor` tool — a no-argument tool the model calls to consult a stronger
3
+ * model for strategic guidance. The model supplies no input; the plugin reads
4
+ * the transcript captured by the lifecycle hooks and runs the consult, routed
5
+ * through the assistant's own inference.
6
+ *
7
+ * Default export = the tool definition. `defaults/index.ts` finalizes it and
8
+ * attaches it to the advisor plugin's `tools` array, which `bootstrapPlugins`
9
+ * registers into the model-visible tool catalog.
10
+ */
11
+
12
+ import type {
13
+ ToolContext,
14
+ ToolDefinition,
15
+ ToolExecutionResult,
16
+ } from "@vellumai/plugin-api";
17
+ import { RiskLevel } from "@vellumai/plugin-api";
18
+
19
+ import { getCapture } from "../advisor-state-store.js";
20
+ import { consultAdvisor } from "../consult.js";
21
+
22
+ const advisorTool: ToolDefinition = {
23
+ name: "advisor",
24
+ description:
25
+ "Consult a stronger advisor model for strategic guidance. Takes NO parameters — your " +
26
+ "full conversation (the task, every tool call, and every result) is forwarded " +
27
+ "automatically. Call it before substantive work, when you're stuck, when changing " +
28
+ "approach, and once before declaring a task complete.",
29
+ input_schema: { type: "object", properties: {}, additionalProperties: false },
30
+ // Read-only advice; low risk so the consult isn't gated behind a prompt.
31
+ defaultRiskLevel: RiskLevel.Low,
32
+ async execute(
33
+ _input: Record<string, unknown>,
34
+ ctx: ToolContext,
35
+ ): Promise<ToolExecutionResult> {
36
+ try {
37
+ const capture = getCapture(ctx.conversationId);
38
+ const advice = await consultAdvisor({
39
+ systemPrompt: capture?.systemPrompt ?? null,
40
+ messages: capture?.messages ?? [],
41
+ signal: ctx.signal,
42
+ });
43
+ return { content: advice, isError: false };
44
+ } catch (err) {
45
+ // Degrade like the advisor tool: never fail the turn over a consult.
46
+ const reason = err instanceof Error ? err.message : String(err);
47
+ return { content: `(advisor unavailable: ${reason})`, isError: false };
48
+ }
49
+ },
50
+ };
51
+
52
+ export default advisorTool;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Convert a captured executor transcript into the message list sent to the
3
+ * advisor sub-call.
4
+ *
5
+ * Strips blocks the advisor shouldn't (or can't) replay:
6
+ * - thinking / redacted-thinking (the advisor tool drops thinking),
7
+ * - files,
8
+ * - `server_tool_use` AND `web_search_tool_result` — provider-side tool calls
9
+ * (e.g. web search) and their results are dropped *together*. Dropping the
10
+ * result without its paired `server_tool_use` would leave an orphaned call
11
+ * block the provider rejects, so any consult after prior web-search history
12
+ * would fail; dropping both keeps the sequence valid.
13
+ *
14
+ * Images are preserved — top-level and nested inside `tool_result.contentBlocks`
15
+ * (which are recursively sanitized) — so the advisor sees what the executor saw.
16
+ * Visual tasks depend on it; the advisor profile is expected to be vision-capable.
17
+ *
18
+ * It also strips the *pending* client tool calls from the final assistant turn:
19
+ * at capture time (a `post-model-call` before tools run) the last assistant
20
+ * message carries the `advisor` tool_use with no matching `tool_result` yet, so
21
+ * sending it would be a dangling call. Earlier, completed `tool_use` /
22
+ * `tool_result` pairs are preserved intact.
23
+ */
24
+
25
+ import type { ContentBlock, Message } from "../../../providers/types.js";
26
+
27
+ /** Drop disallowed blocks; recursively sanitize tool_result content. `null` = drop. */
28
+ function sanitize(block: ContentBlock): ContentBlock | null {
29
+ switch (block.type) {
30
+ case "thinking":
31
+ case "redacted_thinking":
32
+ case "file":
33
+ case "server_tool_use":
34
+ case "web_search_tool_result":
35
+ return null;
36
+ case "tool_result": {
37
+ if (!block.contentBlocks) return block;
38
+ // Keep images (and other allowed blocks) nested in the tool result; drop
39
+ // the disallowed ones.
40
+ const contentBlocks = block.contentBlocks
41
+ .map(sanitize)
42
+ .filter((b): b is ContentBlock => b !== null);
43
+ return {
44
+ ...block,
45
+ contentBlocks: contentBlocks.length > 0 ? contentBlocks : undefined,
46
+ };
47
+ }
48
+ default:
49
+ // text, image, tool_use — kept.
50
+ return block;
51
+ }
52
+ }
53
+
54
+ export function toAdvisorMessages(messages: ReadonlyArray<Message>): Message[] {
55
+ const out: Message[] = [];
56
+ const lastIndex = messages.length - 1;
57
+
58
+ messages.forEach((message, index) => {
59
+ let content = message.content
60
+ .map(sanitize)
61
+ .filter((b): b is ContentBlock => b !== null);
62
+
63
+ // The final assistant turn's client tool calls have no results yet — drop
64
+ // them so we never send a dangling tool_use. (server_tool_use is already
65
+ // dropped above.)
66
+ if (index === lastIndex && message.role === "assistant") {
67
+ content = content.filter(
68
+ (b) => b.type !== "tool_use" && b.type !== "server_tool_use",
69
+ );
70
+ }
71
+
72
+ if (content.length > 0) out.push({ role: message.role, content });
73
+ });
74
+
75
+ return out;
76
+ }
@@ -24,8 +24,15 @@
24
24
  * {@link registerDefaultPlugins} at call time.
25
25
  */
26
26
 
27
+ import { finalizeTool } from "../../tools/tool-defaults.js";
27
28
  import { registerPlugin, resetPluginRegistryForTests } from "../registry.js";
28
29
  import { type Plugin, PluginExecutionError } from "../types.js";
30
+ import { resetAdvisorStateForTests } from "./advisor/advisor-state-store.js";
31
+ import advisorPostModelCall from "./advisor/hooks/post-model-call.js";
32
+ import advisorPreModelCall from "./advisor/hooks/pre-model-call.js";
33
+ import advisorUserPromptSubmit from "./advisor/hooks/user-prompt-submit.js";
34
+ import advisorPkg from "./advisor/package.json" with { type: "json" };
35
+ import advisorTool from "./advisor/tools/advisor.js";
29
36
  import compactionPkg from "./compaction/package.json" with { type: "json" };
30
37
  import emptyResponsePostModelCall from "./empty-response/hooks/post-model-call.js";
31
38
  import emptyResponseStop from "./empty-response/hooks/stop.js";
@@ -297,6 +304,30 @@ export const defaultToolResultTruncatePlugin: Plugin = {
297
304
  },
298
305
  };
299
306
 
307
+ /**
308
+ * `advisor` — adds the model-visible `advisor` tool: a no-argument tool the
309
+ * model calls to consult a stronger inference profile (the `inference` call
310
+ * site with a `quality-optimized` override) on the full transcript, routed
311
+ * through the assistant's own inference. Three hooks feed it: `user-prompt-submit`
312
+ * seeds the capture, `pre-model-call` records the executor's system prompt and
313
+ * injects the steering that nudges the model to consult, and `post-model-call`
314
+ * snapshots the transcript the tool reads. `finalizeTool` fills the tool's
315
+ * defaults so it satisfies `Tool`, and `bootstrapPlugins` registers it into the
316
+ * catalog.
317
+ */
318
+ export const defaultAdvisorPlugin: Plugin = {
319
+ manifest: {
320
+ name: advisorPkg.name,
321
+ version: advisorPkg.version,
322
+ },
323
+ hooks: {
324
+ "user-prompt-submit": advisorUserPromptSubmit,
325
+ "pre-model-call": advisorPreModelCall,
326
+ "post-model-call": advisorPostModelCall,
327
+ },
328
+ tools: [finalizeTool(advisorTool, "advisor")],
329
+ };
330
+
300
331
  /**
301
332
  * Full set of first-party default plugins. Used by
302
333
  * {@link registerDefaultPlugins} to drive the registration loop; the array
@@ -318,6 +349,9 @@ function getAllDefaultPlugins(): readonly Plugin[] {
318
349
  defaultCompactionPlugin,
319
350
  defaultTitleGeneratePlugin,
320
351
  memoryV3ShadowPlugin,
352
+ // Registered last so its capture hooks observe the fully-processed turn
353
+ // (memory injections, history repair) that the executor actually sees.
354
+ defaultAdvisorPlugin,
321
355
  ];
322
356
  }
323
357
 
@@ -364,5 +398,6 @@ export function resetPluginRegistryAndRegisterDefaults(): void {
364
398
  resetExplorationDriftStateForTests();
365
399
  resetTaskProgressNudgeStateForTests();
366
400
  resetSurfaceCompletionNudgeStoreForTests();
401
+ resetAdvisorStateForTests();
367
402
  registerDefaultPlugins();
368
403
  }
@@ -845,7 +845,7 @@ describe("Slack thinking status timing", () => {
845
845
  channel: channelId,
846
846
  threadTs,
847
847
  status: expect.any(String),
848
- loadingMessages: ["Working on it..."],
848
+ loadingMessages: ["Thinking\u2026"],
849
849
  },
850
850
  );
851
851
  const threadStatus = deliveredChannelReplies[0]!.payload
@@ -948,13 +948,13 @@ describe("Slack thinking status timing", () => {
948
948
  channel: channelId,
949
949
  threadTs,
950
950
  status: expect.any(String),
951
- loadingMessages: ["Working on it..."],
951
+ loadingMessages: ["Thinking\u2026"],
952
952
  },
953
953
  {
954
954
  channel: channelId,
955
955
  threadTs,
956
956
  status: expect.any(String),
957
- loadingMessages: ["Working on it..."],
957
+ loadingMessages: ["Thinking\u2026"],
958
958
  },
959
959
  {
960
960
  channel: channelId,
@@ -577,7 +577,7 @@ function createSlackThinkingStatusController(params: {
577
577
  }
578
578
 
579
579
  const SLACK_THINKING_MAX_DURATION_MS = 120_000;
580
- const SLACK_GENERIC_LOADING_MESSAGES = ["Working on it..."] as const;
580
+ const SLACK_GENERIC_LOADING_MESSAGES = ["Thinking…"] as const;
581
581
  const SLACK_THINKING_STATUSES = ["is on it", "is working hard"] as const;
582
582
 
583
583
  function getRandomSlackThinkingStatus(): string {