@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
package/openapi.yaml CHANGED
@@ -27773,6 +27773,88 @@ paths:
27773
27773
  additionalProperties: false
27774
27774
  "404":
27775
27775
  description: Run not found
27776
+ /v1/workflows/runs/{id}/journal:
27777
+ get:
27778
+ operationId: workflows_runs_by_id_journal_get
27779
+ summary: Get workflow run journal
27780
+ description: Return a workflow run's leaf journal as bounded per-leaf summaries (one entry per finished leaf).
27781
+ tags:
27782
+ - workflows
27783
+ parameters:
27784
+ - name: id
27785
+ in: path
27786
+ required: true
27787
+ schema:
27788
+ type: string
27789
+ responses:
27790
+ "200":
27791
+ description: Successful response
27792
+ content:
27793
+ application/json:
27794
+ schema:
27795
+ type: object
27796
+ properties:
27797
+ runId:
27798
+ type: string
27799
+ status:
27800
+ type: string
27801
+ enum:
27802
+ - running
27803
+ - completed
27804
+ - failed
27805
+ - aborted
27806
+ - cap_exceeded
27807
+ - interrupted
27808
+ agentsSpawned:
27809
+ type: number
27810
+ inputTokens:
27811
+ type: number
27812
+ outputTokens:
27813
+ type: number
27814
+ phase:
27815
+ type: string
27816
+ leaves:
27817
+ type: array
27818
+ items:
27819
+ type: object
27820
+ properties:
27821
+ seq:
27822
+ type: number
27823
+ kind:
27824
+ type: string
27825
+ enum:
27826
+ - agent
27827
+ - workflow
27828
+ label:
27829
+ type: string
27830
+ phase:
27831
+ type: string
27832
+ promptSummary:
27833
+ type: string
27834
+ status:
27835
+ type: string
27836
+ resultSummary:
27837
+ type: string
27838
+ inputTokens:
27839
+ type: number
27840
+ outputTokens:
27841
+ type: number
27842
+ createdAt:
27843
+ anyOf:
27844
+ - type: number
27845
+ - type: "null"
27846
+ required:
27847
+ - seq
27848
+ - kind
27849
+ - status
27850
+ - createdAt
27851
+ additionalProperties: false
27852
+ required:
27853
+ - runId
27854
+ - leaves
27855
+ additionalProperties: false
27856
+ "404":
27857
+ description: Run not found
27776
27858
  /v1/workflows/runs/{id}/resume:
27777
27859
  post:
27778
27860
  operationId: workflows_runs_by_id_resume_post
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.9.0-dev.202606181513.fd39213",
3
+ "version": "0.9.0-dev.202606181735.ae6e1d8",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -190,13 +190,16 @@ describe("plugin tool contributions", () => {
190
190
  registerPlugin(plugin);
191
191
 
192
192
  await bootstrapPlugins();
193
- // No tool should have been registered.
194
- expect(getAllTools()).toHaveLength(0);
193
+ // `bootstrapPlugins` also registers the first-party defaults (the advisor
194
+ // default contributes the `advisor` tool), so the global tool set is not
195
+ // empty. What matters here is that the no-tools plugin contributed nothing
196
+ // of its own — its tool refcount stays at zero.
197
+ expect(getPluginRefCount("no-tools")).toBe(0);
195
198
 
196
199
  // Shutdown must also be safe — `unregisterPluginTools` is idempotent for
197
200
  // plugins that never contributed any tools.
198
201
  await runShutdownHooks("test-shutdown");
199
- expect(getAllTools()).toHaveLength(0);
202
+ expect(getPluginRefCount("no-tools")).toBe(0);
200
203
  });
201
204
 
202
205
  test("tools declared before init() runs are only visible after bootstrap", async () => {
@@ -0,0 +1,53 @@
1
+ /**
2
+ * `workflow_completed` SSE event.
3
+ *
4
+ * Server → client notification that a `run_workflow` run has reached a
5
+ * terminal state. Carries `runId`, the terminal `status`, cumulative
6
+ * `agentsSpawned`/`inputTokens`/`outputTokens` counters, and an optional
7
+ * human-readable `summary`.
8
+ *
9
+ * `conversationId` is present for a run launched from a conversation, so
10
+ * clients can route the event to that conversation's inline workflow card. It
11
+ * is omitted for a conversationless run (e.g. a scheduled workflow with no
12
+ * wake/origin conversation), which broadcasts unscoped for raw SSE listeners.
13
+ *
14
+ * Canonical wire-contract source. Daemon code imports the type
15
+ * directly from this file; external consumers import via
16
+ * `@vellumai/assistant-api`.
17
+ */
18
+
19
+ import { z } from "zod";
20
+
21
+ /**
22
+ * Lifecycle status of a `run_workflow` run. `running` is the live
23
+ * state; the remainder are terminal. `cap_exceeded` is reached when a
24
+ * run hits its configured agent/token cap; `interrupted` when the run
25
+ * is halted by an external signal.
26
+ */
27
+ export const WorkflowRunStatusSchema = z.enum([
28
+ "running",
29
+ "completed",
30
+ "failed",
31
+ "aborted",
32
+ "cap_exceeded",
33
+ "interrupted",
34
+ ]);
35
+
36
+ export type WorkflowRunStatus = z.infer<typeof WorkflowRunStatusSchema>;
37
+
38
+ export const WorkflowCompletedEventSchema = z
39
+ .object({
40
+ type: z.literal("workflow_completed"),
41
+ runId: z.string(),
42
+ conversationId: z.string().optional(),
43
+ status: WorkflowRunStatusSchema,
44
+ agentsSpawned: z.number(),
45
+ inputTokens: z.number(),
46
+ outputTokens: z.number(),
47
+ summary: z.string().optional(),
48
+ })
49
+ .strict();
50
+
51
+ export type WorkflowCompletedEvent = z.infer<
52
+ typeof WorkflowCompletedEventSchema
53
+ >;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `workflow_leaf_finished` SSE event.
3
+ *
4
+ * Server → client notification that a leaf agent within a
5
+ * `run_workflow` run has finished. Carries `runId`, the parent
6
+ * `conversationId`, the monotonic `seq` identifying the leaf within the
7
+ * run, the terminal `status`, optional `inputTokens`/`outputTokens`
8
+ * counters, and optional human-readable display fields (`label`,
9
+ * `resultSummary`).
10
+ *
11
+ * `conversationId` is present so clients can route the event to the
12
+ * originating conversation's inline workflow card and its live leaf
13
+ * tree.
14
+ *
15
+ * Canonical wire-contract source. Daemon code imports the type
16
+ * directly from this file; external consumers import via
17
+ * `@vellumai/assistant-api`.
18
+ */
19
+
20
+ import { z } from "zod";
21
+
22
+ export const WorkflowLeafFinishedEventSchema = z
23
+ .object({
24
+ type: z.literal("workflow_leaf_finished"),
25
+ runId: z.string(),
26
+ conversationId: z.string(),
27
+ seq: z.number().int(),
28
+ status: z.enum(["completed", "failed"]),
29
+ label: z.string().optional(),
30
+ inputTokens: z.number().optional(),
31
+ outputTokens: z.number().optional(),
32
+ resultSummary: z.string().optional(),
33
+ })
34
+ .strict();
35
+
36
+ export type WorkflowLeafFinishedEvent = z.infer<
37
+ typeof WorkflowLeafFinishedEventSchema
38
+ >;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `workflow_leaf_started` SSE event.
3
+ *
4
+ * Server → client notification that a leaf agent within a
5
+ * `run_workflow` run has started. Carries `runId`, the parent
6
+ * `conversationId`, the monotonic `seq` identifying the leaf within the
7
+ * run, and optional human-readable display fields (`label`, `phase`,
8
+ * `promptSummary`).
9
+ *
10
+ * `conversationId` is present so clients can route the event to the
11
+ * originating conversation's inline workflow card and its live leaf
12
+ * tree.
13
+ *
14
+ * Canonical wire-contract source. Daemon code imports the type
15
+ * directly from this file; external consumers import via
16
+ * `@vellumai/assistant-api`.
17
+ */
18
+
19
+ import { z } from "zod";
20
+
21
+ export const WorkflowLeafStartedEventSchema = z
22
+ .object({
23
+ type: z.literal("workflow_leaf_started"),
24
+ runId: z.string(),
25
+ conversationId: z.string(),
26
+ seq: z.number().int(),
27
+ label: z.string().optional(),
28
+ phase: z.string().optional(),
29
+ promptSummary: z.string().optional(),
30
+ })
31
+ .strict();
32
+
33
+ export type WorkflowLeafStartedEvent = z.infer<
34
+ typeof WorkflowLeafStartedEventSchema
35
+ >;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * `workflow_progress` SSE event.
3
+ *
4
+ * Server → client incremental progress update for an in-flight
5
+ * `run_workflow` run. Carries `runId`, the running `agentsSpawned` count,
6
+ * and optional human-readable display fields (`phase`, `label`, `message`).
7
+ *
8
+ * `conversationId` is present for a run launched from a conversation, so
9
+ * clients can route the event to that conversation's inline workflow card. It
10
+ * is omitted for a conversationless run (e.g. a scheduled workflow with no
11
+ * wake/origin conversation), which broadcasts unscoped for raw SSE listeners.
12
+ *
13
+ * Canonical wire-contract source. Daemon code imports the type
14
+ * directly from this file; external consumers import via
15
+ * `@vellumai/assistant-api`.
16
+ */
17
+
18
+ import { z } from "zod";
19
+
20
+ export const WorkflowProgressEventSchema = z
21
+ .object({
22
+ type: z.literal("workflow_progress"),
23
+ runId: z.string(),
24
+ conversationId: z.string().optional(),
25
+ agentsSpawned: z.number(),
26
+ phase: z.string().optional(),
27
+ label: z.string().optional(),
28
+ message: z.string().optional(),
29
+ })
30
+ .strict();
31
+
32
+ export type WorkflowProgressEvent = z.infer<typeof WorkflowProgressEventSchema>;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * `workflow_started` SSE event.
3
+ *
4
+ * Server → client notification that a `run_workflow` run has begun.
5
+ * Carries `runId`, the parent `conversationId`, an optional anchoring
6
+ * `toolUseId`, and an optional human-readable `label`.
7
+ *
8
+ * `conversationId` is present so clients can route the event to the
9
+ * originating conversation's inline workflow card. `toolUseId` (the
10
+ * `skill_execute` block id that launched the run) anchors that inline
11
+ * card to the exact spawn tool call, mirroring
12
+ * `subagent_spawned.parentToolUseId`.
13
+ *
14
+ * Canonical wire-contract source. Daemon code imports the type
15
+ * directly from this file; external consumers import via
16
+ * `@vellumai/assistant-api`.
17
+ */
18
+
19
+ import { z } from "zod";
20
+
21
+ export const WorkflowStartedEventSchema = z
22
+ .object({
23
+ type: z.literal("workflow_started"),
24
+ runId: z.string(),
25
+ conversationId: z.string(),
26
+ toolUseId: z.string().optional(),
27
+ label: z.string().optional(),
28
+ })
29
+ .strict();
30
+
31
+ export type WorkflowStartedEvent = z.infer<typeof WorkflowStartedEventSchema>;
package/src/api/index.ts CHANGED
@@ -52,6 +52,11 @@ import { UISurfaceUpdateEventSchema } from "./events/ui-surface-update.js";
52
52
  import { UsageProgressEventSchema } from "./events/usage-progress.js";
53
53
  import { UsageUpdateEventSchema } from "./events/usage-update.js";
54
54
  import { UserMessageEchoEventSchema } from "./events/user-message-echo.js";
55
+ import { WorkflowCompletedEventSchema } from "./events/workflow-completed.js";
56
+ import { WorkflowLeafFinishedEventSchema } from "./events/workflow-leaf-finished.js";
57
+ import { WorkflowLeafStartedEventSchema } from "./events/workflow-leaf-started.js";
58
+ import { WorkflowProgressEventSchema } from "./events/workflow-progress.js";
59
+ import { WorkflowStartedEventSchema } from "./events/workflow-started.js";
55
60
 
56
61
  export {
57
62
  CALL_SITE_COMPACTION_AGENT,
@@ -326,6 +331,28 @@ export {
326
331
  type UserMessageEchoEvent,
327
332
  UserMessageEchoEventSchema,
328
333
  } from "./events/user-message-echo.js";
334
+ export {
335
+ type WorkflowCompletedEvent,
336
+ WorkflowCompletedEventSchema,
337
+ type WorkflowRunStatus,
338
+ WorkflowRunStatusSchema,
339
+ } from "./events/workflow-completed.js";
340
+ export {
341
+ type WorkflowLeafFinishedEvent,
342
+ WorkflowLeafFinishedEventSchema,
343
+ } from "./events/workflow-leaf-finished.js";
344
+ export {
345
+ type WorkflowLeafStartedEvent,
346
+ WorkflowLeafStartedEventSchema,
347
+ } from "./events/workflow-leaf-started.js";
348
+ export {
349
+ type WorkflowProgressEvent,
350
+ WorkflowProgressEventSchema,
351
+ } from "./events/workflow-progress.js";
352
+ export {
353
+ type WorkflowStartedEvent,
354
+ WorkflowStartedEventSchema,
355
+ } from "./events/workflow-started.js";
329
356
  export {
330
357
  type DictationContext,
331
358
  DictationContextSchema,
@@ -443,6 +470,12 @@ export {
443
470
  type SubagentDetailResponse,
444
471
  SubagentDetailResponseSchema,
445
472
  } from "./responses/subagent-detail.js";
473
+ export {
474
+ type WorkflowJournalResponse,
475
+ WorkflowJournalResponseSchema,
476
+ type WorkflowLeaf,
477
+ WorkflowLeafSchema,
478
+ } from "./responses/workflow-journal.js";
446
479
 
447
480
  /**
448
481
  * Canonical SSE event schema for the assistant runtime.
@@ -510,6 +543,11 @@ export const AssistantEventSchema = z.discriminatedUnion("type", [
510
543
  UsageProgressEventSchema,
511
544
  UsageUpdateEventSchema,
512
545
  UserMessageEchoEventSchema,
546
+ WorkflowCompletedEventSchema,
547
+ WorkflowLeafFinishedEventSchema,
548
+ WorkflowLeafStartedEventSchema,
549
+ WorkflowProgressEventSchema,
550
+ WorkflowStartedEventSchema,
513
551
  ]);
514
552
 
515
553
  /**
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Wire contract for the workflow-journal REST endpoint. Returns a
3
+ * `run_workflow` run's live status, cumulative usage/agent counters,
4
+ * current phase, and the ordered list of leaf agents spawned by the
5
+ * run.
6
+ *
7
+ * Reuses the canonical `WorkflowRunStatusSchema` defined alongside the
8
+ * `workflow_completed` SSE event so the polled REST journal and the
9
+ * streamed lifecycle events share one status shape.
10
+ *
11
+ * Canonical wire-contract source. Assistant code imports the types
12
+ * directly from this file via relative paths; external consumers
13
+ * (web client, gateway, evals) import via `@vellumai/assistant-api`.
14
+ */
15
+
16
+ import { z } from "zod";
17
+
18
+ import { WorkflowRunStatusSchema } from "../events/workflow-completed.js";
19
+
20
+ /**
21
+ * A single leaf within a workflow run. `kind` distinguishes a leaf
22
+ * agent from a nested workflow; `status` is an open string rather than
23
+ * a closed enum since it covers both lifecycle states and terminal
24
+ * results.
25
+ */
26
+ export const WorkflowLeafSchema = z.object({
27
+ seq: z.number().int(),
28
+ kind: z.enum(["agent", "workflow"]),
29
+ label: z.string().optional(),
30
+ phase: z.string().optional(),
31
+ promptSummary: z.string().optional(),
32
+ status: z.string(),
33
+ resultSummary: z.string().optional(),
34
+ inputTokens: z.number().optional(),
35
+ outputTokens: z.number().optional(),
36
+ createdAt: z.number().nullable(),
37
+ });
38
+
39
+ export type WorkflowLeaf = z.infer<typeof WorkflowLeafSchema>;
40
+
41
+ export const WorkflowJournalResponseSchema = z.object({
42
+ runId: z.string(),
43
+ status: WorkflowRunStatusSchema.optional(),
44
+ agentsSpawned: z.number().optional(),
45
+ inputTokens: z.number().optional(),
46
+ outputTokens: z.number().optional(),
47
+ phase: z.string().optional(),
48
+ leaves: z.array(WorkflowLeafSchema),
49
+ });
50
+
51
+ export type WorkflowJournalResponse = z.infer<
52
+ typeof WorkflowJournalResponseSchema
53
+ >;
@@ -18,6 +18,13 @@ import type { WorkflowRunStatus } from "../../workflows/journal-store.js";
18
18
  export interface WorkflowProgress {
19
19
  type: "workflow_progress";
20
20
  runId: string;
21
+ /**
22
+ * Originating conversation id, when launched from one; lets `broadcastMessage`
23
+ * auto-scope + seq-stamp the event to that conversation's SSE stream. Omitted
24
+ * for a conversationless run (e.g. a scheduled workflow), which broadcasts
25
+ * unscoped for raw SSE listeners and the DB record.
26
+ */
27
+ conversationId?: string;
21
28
  /** Latest phase title, when this emission came from a `phase(...)` call. */
22
29
  phase?: string;
23
30
  /** Run label (the workflow's `meta.name`), for client display. */
@@ -36,6 +43,13 @@ export interface WorkflowProgress {
36
43
  export interface WorkflowCompleted {
37
44
  type: "workflow_completed";
38
45
  runId: string;
46
+ /**
47
+ * Originating conversation id, when launched from one; lets `broadcastMessage`
48
+ * auto-scope + seq-stamp the event to that conversation's SSE stream. Omitted
49
+ * for a conversationless run (e.g. a scheduled workflow), which broadcasts
50
+ * unscoped for raw SSE listeners and the DB record.
51
+ */
52
+ conversationId?: string;
39
53
  status: WorkflowRunStatus;
40
54
  agentsSpawned: number;
41
55
  inputTokens: number;
@@ -44,6 +58,74 @@ export interface WorkflowCompleted {
44
58
  summary?: string;
45
59
  }
46
60
 
61
+ /**
62
+ * A workflow run has started. Emitted once at launch, before any leaf events.
63
+ */
64
+ export interface WorkflowStarted {
65
+ type: "workflow_started";
66
+ runId: string;
67
+ /**
68
+ * Originating conversation id; lets `broadcastMessage` auto-scope +
69
+ * seq-stamp the event to the conversation's SSE stream.
70
+ */
71
+ conversationId: string;
72
+ /**
73
+ * Tool-use id of the `skill_execute` block that launched this run, for
74
+ * anchoring the inline workflow card to the exact spawn tool call.
75
+ */
76
+ toolUseId?: string;
77
+ /** Run label (the workflow's `meta.name`), for client display. */
78
+ label?: string;
79
+ }
80
+
81
+ /**
82
+ * A leaf agent within a workflow run has started. `seq` orders leaves within
83
+ * the run for stable client-side tree placement.
84
+ */
85
+ export interface WorkflowLeafStarted {
86
+ type: "workflow_leaf_started";
87
+ runId: string;
88
+ /**
89
+ * Originating conversation id; lets `broadcastMessage` auto-scope +
90
+ * seq-stamp the event to the conversation's SSE stream.
91
+ */
92
+ conversationId: string;
93
+ seq: number;
94
+ /** Leaf label, for client display. */
95
+ label?: string;
96
+ /** Phase the leaf belongs to, when the workflow declares phases. */
97
+ phase?: string;
98
+ /** Short summary of the leaf's prompt, for client display. */
99
+ promptSummary?: string;
100
+ }
101
+
102
+ /**
103
+ * A leaf agent within a workflow run has finished. `seq` matches the
104
+ * corresponding `workflow_leaf_started` event.
105
+ */
106
+ export interface WorkflowLeafFinished {
107
+ type: "workflow_leaf_finished";
108
+ runId: string;
109
+ /**
110
+ * Originating conversation id; lets `broadcastMessage` auto-scope +
111
+ * seq-stamp the event to the conversation's SSE stream.
112
+ */
113
+ conversationId: string;
114
+ seq: number;
115
+ status: "completed" | "failed";
116
+ /** Leaf label, for client display. */
117
+ label?: string;
118
+ inputTokens?: number;
119
+ outputTokens?: number;
120
+ /** Short summary of the leaf's result, for client display. */
121
+ resultSummary?: string;
122
+ }
123
+
47
124
  // --- Domain-level union aliases (consumed by the barrel file) ---
48
125
 
49
- export type _WorkflowsServerMessages = WorkflowProgress | WorkflowCompleted;
126
+ export type _WorkflowsServerMessages =
127
+ | WorkflowProgress
128
+ | WorkflowCompleted
129
+ | WorkflowStarted
130
+ | WorkflowLeafStarted
131
+ | WorkflowLeafFinished;
@@ -224,6 +224,7 @@ import {
224
224
  migrateUsageLlmCallCount,
225
225
  migrateVoiceInviteColumns,
226
226
  migrateVoiceInviteDisplayMetadata,
227
+ migrateWorkflowJournalLeafTokens,
227
228
  migrateWorkflowRuns,
228
229
  migrateWorkflowRunTrust,
229
230
  recoverCrashedMigrations,
@@ -523,6 +524,7 @@ export function initializeDb(): void {
523
524
  migrateScheduleCapabilities,
524
525
  migrateContactChannelsRenormalizeAddresses,
525
526
  migrateScheduleDefaultNoReuseConversation,
527
+ migrateWorkflowJournalLeafTokens,
526
528
  ];
527
529
 
528
530
  // Run each migration step, catching and logging individual failures so one
@@ -0,0 +1,32 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { getSqliteFrom } from "../db-connection.js";
3
+
4
+ /**
5
+ * Add nullable `input_tokens` / `output_tokens` columns to `workflow_journal`.
6
+ *
7
+ * Persists per-leaf token usage so the journal route can attribute usage to each
8
+ * leaf. The client then computes run-level token metrics from the per-leaf sum
9
+ * (a single source of truth), counting each leaf exactly once regardless of
10
+ * whether its usage arrives via a live `leaf_finished` event or a journal
11
+ * backfill — which avoids the undercount that arose when a mid-run journal
12
+ * aggregate counted a leaf the journal could not itself attribute.
13
+ *
14
+ * Nullable — legacy rows and non-completed leaves (failures, nested
15
+ * `workflow`-kind entries) stay NULL and contribute zero to the sum.
16
+ *
17
+ * Idempotent — each ALTER is wrapped so a re-run (column already present) is a
18
+ * no-op.
19
+ */
20
+ export function migrateWorkflowJournalLeafTokens(database: DrizzleDb): void {
21
+ const raw = getSqliteFrom(database);
22
+ try {
23
+ raw.exec(`ALTER TABLE workflow_journal ADD COLUMN input_tokens INTEGER`);
24
+ } catch {
25
+ /* Column already exists */
26
+ }
27
+ try {
28
+ raw.exec(`ALTER TABLE workflow_journal ADD COLUMN output_tokens INTEGER`);
29
+ } catch {
30
+ /* Column already exists */
31
+ }
32
+ }
@@ -284,6 +284,7 @@ export { migrateContactChannelsUniqueExtUser } from "./289-contact-channels-uniq
284
284
  export { migrateScheduleCapabilities } from "./290-schedule-capabilities.js";
285
285
  export { migrateContactChannelsRenormalizeAddresses } from "./291-contact-channels-renormalize-addresses.js";
286
286
  export { migrateScheduleDefaultNoReuseConversation } from "./292-schedule-default-no-reuse-conversation.js";
287
+ export { migrateWorkflowJournalLeafTokens } from "./293-workflow-journal-leaf-tokens.js";
287
288
  export {
288
289
  MIGRATION_REGISTRY,
289
290
  type MigrationRegistryEntry,
@@ -0,0 +1,43 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+
3
+ import type { Message } from "../../../../providers/types.js";
4
+ import {
5
+ getCapture,
6
+ recordMessages,
7
+ recordSystemPrompt,
8
+ resetAdvisorStateForTests,
9
+ seedCapture,
10
+ } from "../advisor-state-store.js";
11
+
12
+ const userMsg = (t: string): Message => ({
13
+ role: "user",
14
+ content: [{ type: "text", text: t }],
15
+ });
16
+
17
+ afterEach(() => {
18
+ resetAdvisorStateForTests();
19
+ });
20
+
21
+ describe("advisor state store", () => {
22
+ test("records system prompt and messages independently per conversation", () => {
23
+ recordSystemPrompt("c1", "system A");
24
+ recordMessages("c1", [userMsg("hello")]);
25
+ recordSystemPrompt("c2", "system B");
26
+
27
+ expect(getCapture("c1")?.systemPrompt).toBe("system A");
28
+ expect(getCapture("c1")?.messages).toEqual([userMsg("hello")]);
29
+ expect(getCapture("c2")?.systemPrompt).toBe("system B");
30
+ expect(getCapture("c2")?.messages).toEqual([]);
31
+ });
32
+
33
+ test("seedCapture snapshots (copies) the array", () => {
34
+ const live: Message[] = [userMsg("one")];
35
+ seedCapture("c1", live);
36
+ live.push(userMsg("two"));
37
+ expect(getCapture("c1")?.messages).toEqual([userMsg("one")]);
38
+ });
39
+
40
+ test("getCapture returns undefined for an unseen conversation", () => {
41
+ expect(getCapture("nope")).toBeUndefined();
42
+ });
43
+ });