paseo-bm-plugin 0.0.0-placeholder.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 (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -0
  3. package/client/agent-tree.ts +308 -0
  4. package/client/answer-state.ts +62 -0
  5. package/client/bead-chips.tsx +147 -0
  6. package/client/beads-header-button.ts +108 -0
  7. package/client/beads-model.ts +581 -0
  8. package/client/beads-screen.tsx +516 -0
  9. package/client/beads-tab.tsx +58 -0
  10. package/client/chat-card.tsx +636 -0
  11. package/client/chat-cards.ts +1038 -0
  12. package/client/dashboard-actions.tsx +255 -0
  13. package/client/dashboard-model.ts +947 -0
  14. package/client/dashboard-view.ts +215 -0
  15. package/client/dashboard.tsx +318 -0
  16. package/client/launch-manager.ts +323 -0
  17. package/client/launcher.tsx +516 -0
  18. package/client/markdown-view.tsx +112 -0
  19. package/client/markdown.ts +145 -0
  20. package/client/settings.tsx +104 -0
  21. package/client/setup-model.ts +552 -0
  22. package/client/setup-screen.tsx +913 -0
  23. package/client/slot.ts +47 -0
  24. package/client/tree.tsx +204 -0
  25. package/client/ui.tsx +262 -0
  26. package/client/waiting-pills-model.ts +156 -0
  27. package/client/waiting-pills.tsx +201 -0
  28. package/index.client.tsx +232 -0
  29. package/index.server.ts +168 -0
  30. package/package.json +35 -0
  31. package/paseo-plugin.json +6 -0
  32. package/roles/manager.md +181 -0
  33. package/roles/reviewer.md +160 -0
  34. package/roles/worker.md +407 -0
  35. package/server/agent-labels.ts +194 -0
  36. package/server/agent-role.ts +102 -0
  37. package/server/answer-marks.ts +120 -0
  38. package/server/bead-actions.ts +88 -0
  39. package/server/bead-work.ts +80 -0
  40. package/server/beads-store.ts +342 -0
  41. package/server/bm-report.ts +433 -0
  42. package/server/chat-peers.ts +65 -0
  43. package/server/chat-rpc.ts +122 -0
  44. package/server/chat-waiting.ts +182 -0
  45. package/server/collector.ts +629 -0
  46. package/server/config-writer.ts +222 -0
  47. package/server/cost.ts +88 -0
  48. package/server/dashboard-rpc.ts +662 -0
  49. package/server/fallback-detect.ts +183 -0
  50. package/server/fallback-handover.ts +365 -0
  51. package/server/fallback-manager.ts +170 -0
  52. package/server/fallback-reviewer.ts +198 -0
  53. package/server/fallback-rpc.ts +306 -0
  54. package/server/fallback-settings.ts +322 -0
  55. package/server/fallback-state.ts +518 -0
  56. package/server/fallback-switch.ts +191 -0
  57. package/server/fallback-wait.ts +188 -0
  58. package/server/format-check.ts +352 -0
  59. package/server/install-home.ts +187 -0
  60. package/server/live-timeline.ts +129 -0
  61. package/server/manager-instructions.ts +9 -0
  62. package/server/manager.ts +647 -0
  63. package/server/model-costs.ts +238 -0
  64. package/server/notice-queue.ts +315 -0
  65. package/server/notices.ts +81 -0
  66. package/server/paseo-cli.ts +115 -0
  67. package/server/provider-id.ts +12 -0
  68. package/server/review-budget.ts +208 -0
  69. package/server/reviewer-instructions.ts +9 -0
  70. package/server/role-choices.ts +161 -0
  71. package/server/role-extras.ts +270 -0
  72. package/server/role-hook.ts +347 -0
  73. package/server/role-mode.ts +397 -0
  74. package/server/role-settings-rpc.ts +325 -0
  75. package/server/roles.ts +96 -0
  76. package/server/settings-notices.ts +112 -0
  77. package/server/setup-rpc.ts +70 -0
  78. package/server/setup-skills.ts +121 -0
  79. package/server/setup-tools.ts +162 -0
  80. package/server/shell.ts +68 -0
  81. package/server/stop-propagation.ts +365 -0
  82. package/server/tools-check.ts +118 -0
  83. package/server/trace-store.ts +1137 -0
  84. package/server/traces.ts +1356 -0
  85. package/server/worker-instructions.ts +9 -0
  86. package/server/workflow-steps.ts +422 -0
  87. package/shared/bead-ids.ts +25 -0
  88. package/shared/bm-fallback.ts +91 -0
  89. package/shared/bm-format.ts +424 -0
  90. package/shared/bm-questions.ts +213 -0
  91. package/shared/bm-report.ts +433 -0
  92. package/shared/contracts.ts +1371 -0
  93. package/shared/fallback-patterns.ts +201 -0
  94. package/shared/fallback.ts +46 -0
  95. package/shared/new-request.ts +20 -0
  96. package/shared/order.ts +22 -0
  97. package/shared/prices.ts +65 -0
  98. package/shared/settings.ts +57 -0
  99. package/shared/sole-worker.ts +20 -0
  100. package/shared/version.ts +6 -0
  101. package/tsconfig.json +16 -0
@@ -0,0 +1,1371 @@
1
+ import { defineRpc } from "@getpaseo/plugin";
2
+ import { z } from "zod";
3
+ import { modelPriceSchema } from "./prices";
4
+
5
+ /**
6
+ * RPC contracts for the paseo-bm plugin.
7
+ *
8
+ * Source of truth: docs/design/paseo-bm.md §5 ("Hợp đồng RPC của plugin").
9
+ * This module is `shared/`, so it must stay free of Node and React Native
10
+ * runtime imports: Zod schemas and plain values only.
11
+ *
12
+ * WP-108 delivers the contracts. Handler behaviour lives in `server/` (WP-112)
13
+ * and the surfaces that call them live in `client/` (WP-113).
14
+ */
15
+
16
+ /** Value of the `bm.role` agent label that paseo-bm sets when it creates an agent. */
17
+ export const bmRoleSchema = z.enum(["manager", "worker", "reviewer"]);
18
+
19
+ /** Identifier of a Paseo workspace, as returned by the Paseo SDK. */
20
+ export const workspaceIdSchema = z.string().min(1);
21
+
22
+ /** Identifier of a Paseo agent, as returned by the Paseo SDK. */
23
+ export const agentIdSchema = z.string().min(1);
24
+
25
+ /**
26
+ * `manager.ensure` — find the live Manager of a workspace by its `bm.role=manager`
27
+ * label, or create one from the `bm-manager` profile with the instructions in
28
+ * `roles/manager.md`. `created` reports which of the two happened.
29
+ *
30
+ * `otherManagerIds` lists the other live Managers of the same workspace (newest
31
+ * first) when more than one exists; `agentId` is then the newest. They are
32
+ * reported, never deleted or archived (design §9.3). NOTE: this field is not in
33
+ * the design §5 table yet — see the WP-112 report; it is the only channel for
34
+ * the "báo trong panel" requirement.
35
+ *
36
+ * Failures are thrown as errors whose message starts with a registry code
37
+ * (`E_PROVIDER_UNAVAILABLE`).
38
+ */
39
+ export const managerEnsureRpc = defineRpc({
40
+ name: "manager.ensure",
41
+ input: z.object({
42
+ workspaceId: workspaceIdSchema,
43
+ }),
44
+ output: z.object({
45
+ agentId: agentIdSchema,
46
+ created: z.boolean(),
47
+ otherManagerIds: z.array(agentIdSchema),
48
+ /**
49
+ * Why an existing Manager was not switched to its no-prompt mode, or `null`
50
+ * (delta 20260918 §4.1). Added field: a client that does not know it drops it.
51
+ */
52
+ modeNotice: z.string().nullable(),
53
+ /**
54
+ * Set when the Manager just created has no Paseo tools (delta 20260921
55
+ * §4.2.4). Optional so an older server's answer still parses; the server
56
+ * always sends it.
57
+ */
58
+ toolsNotice: z.string().nullable().optional(),
59
+ }),
60
+ });
61
+
62
+ /**
63
+ * Role shown for a listed agent: one of the three roles, or `unknown` when the
64
+ * `bm.role` label is missing or holds another value (design §5, "không rõ vai trò").
65
+ */
66
+ export const agentRoleSchema = z.enum(["manager", "worker", "reviewer", "unknown"]);
67
+
68
+ /**
69
+ * One node of the agent tree the workspace panel draws.
70
+ *
71
+ * `role` is `unknown` when the agent carries no readable `bm.role` label; the
72
+ * panel renders that instead of dropping the agent (design §5).
73
+ * `title` mirrors the SDK snapshot, which may be `null` (untitled agent).
74
+ * `parentId` is the `paseo.parent-agent-id` label when that parent is itself in
75
+ * the list, otherwise `null` — so an orphaned Worker (its Manager deleted or
76
+ * archived) is a root of the tree (design §8).
77
+ */
78
+ export const agentNodeSchema = z.object({
79
+ id: agentIdSchema,
80
+ role: agentRoleSchema,
81
+ title: z.string().nullable(),
82
+ status: z.string(),
83
+ parentId: agentIdSchema.nullable(),
84
+ updatedAt: z.string(),
85
+ /**
86
+ * False when the agent has no valid `bm.role` label: its role came from its
87
+ * provider, or it is an unlabelled descendant (delta 20260918g §4.4).
88
+ * Defaults to true for older servers.
89
+ */
90
+ labelled: z.boolean().default(true),
91
+ /**
92
+ * The agent that replaced this one after a fallback switch: its `bm.replacedBy`
93
+ * label, else the replacement of a `switched` incident (delta 20260921
94
+ * §4.4.8). Defaults to null for older servers.
95
+ */
96
+ replacedBy: z.string().nullable().default(null),
97
+ });
98
+
99
+ /**
100
+ * `agents.list` — the agents of one workspace, flat. The caller builds the tree
101
+ * from `role` and `parentId`.
102
+ */
103
+ export const agentsListRpc = defineRpc({
104
+ name: "agents.list",
105
+ input: z.object({
106
+ workspaceId: workspaceIdSchema,
107
+ }),
108
+ output: z.object({
109
+ agents: z.array(agentNodeSchema),
110
+ }),
111
+ });
112
+
113
+ /**
114
+ * One row of the effective role configuration the panel shows (REQ-032d).
115
+ *
116
+ * Read from the Paseo configuration in effect (errata bm-dnc): `provider` is the
117
+ * base provider the role's derived provider extends, `model` comes from the
118
+ * role's agent profile, `paseoTools` is whether the derived provider currently
119
+ * has the Paseo agent tools enabled, and `instructionsPath` is the name of the
120
+ * role's instructions inside the payload (e.g. `roles/manager.md`), not an
121
+ * on-disk path.
122
+ */
123
+ export const roleDescriptorSchema = z.object({
124
+ role: bmRoleSchema,
125
+ provider: z.string(),
126
+ model: z.string(),
127
+ paseoTools: z.boolean(),
128
+ instructionsPath: z.string(),
129
+ });
130
+
131
+ /**
132
+ * `roles.describe` — the role configuration in effect right now. Takes no input.
133
+ */
134
+ export const rolesDescribeRpc = defineRpc({
135
+ name: "roles.describe",
136
+ input: z.object({}),
137
+ output: z.object({
138
+ roles: z.array(roleDescriptorSchema),
139
+ }),
140
+ });
141
+
142
+ export type BmRole = z.infer<typeof bmRoleSchema>;
143
+ /** Input shape: `labelled` may be absent (meaning labelled), as from a server older than delta 20260918g. */
144
+ export type AgentNode = z.input<typeof agentNodeSchema>;
145
+ export type RoleDescriptor = z.infer<typeof roleDescriptorSchema>;
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // Dashboard (Phase 2a) — WP-201.
149
+ //
150
+ // Source of truth: docs/design/paseo-bm-dashboard.md §§3.2–3.3 (persisted
151
+ // record), §4 (data model) and §5 (RPC + error codes). Field names are part of
152
+ // a *persisted* format: renaming one later costs a schema migration, so they
153
+ // follow the design literally.
154
+ //
155
+ // Everything below is read-only from the caller's point of view except
156
+ // `traces.delete` and `traces.reassign`, which only ever touch paseo-bm's own
157
+ // trace store (REQ-047).
158
+ // ---------------------------------------------------------------------------
159
+
160
+ /**
161
+ * How sure the server is about a derived conclusion. Rendered to the user, never
162
+ * hidden: `inferred` and `unknown` are the honest answers the Dashboard is
163
+ * required to show instead of a plausible guess (REQ-041d, REQ-044c, REQ-045).
164
+ */
165
+ export const confidenceSchema = z.enum(["exact", "inferred", "unknown"]);
166
+
167
+ /** One traceable piece of evidence behind a derived conclusion (design §4.1). */
168
+ export const evidenceSchema = z.object({
169
+ /** `skill`: the agent loaded an agent skill; `detail` is the skill name. */
170
+ kind: z.enum(["report", "shell", "file", "agent", "timeline", "skill"]),
171
+ detail: z.string(),
172
+ agentId: agentIdSchema.nullable(),
173
+ at: z.string().nullable(),
174
+ });
175
+
176
+ /**
177
+ * Token and cost roll-up (REQ-052). `costBasis` is what keeps the number
178
+ * honest: `provider` is the figure the tool itself reported, `estimated` comes
179
+ * from the dated price table bundled with the release, and `unavailable` means
180
+ * the model is not in that table — then `costUsd` is null and only tokens show.
181
+ */
182
+ export const usageSchema = z.object({
183
+ inputTokens: z.number().int().nonnegative(),
184
+ cachedInputTokens: z.number().int().nonnegative(),
185
+ outputTokens: z.number().int().nonnegative(),
186
+ costUsd: z.number().nonnegative().nullable(),
187
+ costBasis: z.enum(["provider", "estimated", "unavailable"]),
188
+ model: z.string().nullable(),
189
+ pricesUpdatedAt: z.string().nullable(),
190
+ });
191
+
192
+ /** Lifecycle of one request, derived in the fixed order of design §7.3. */
193
+ export const traceStateSchema = z.enum([
194
+ "running",
195
+ "waiting_user",
196
+ "completed",
197
+ "stopped",
198
+ "failed",
199
+ "unknown",
200
+ ]);
201
+
202
+ /** Request size the Worker classified itself into (REQ-036, REQ-045e). */
203
+ export const tierSchema = z.enum(["Small", "Medium", "Large"]);
204
+
205
+ /**
206
+ * Whether the workspace a trace belongs to still exists in Paseo (design §3.7).
207
+ * `unknown` is used when `workspaces.list()` failed — one failed call must never
208
+ * be turned into the conclusion "this workspace is gone" (REQ-057a).
209
+ */
210
+ export const workspaceStateSchema = z.enum(["live", "archived", "orphaned", "unknown"]);
211
+
212
+ /** A bead count plus how sure it is (REQ-044b, REQ-044c). */
213
+ export const beadCountSchema = z.object({
214
+ count: z.number().int().nonnegative(),
215
+ confidence: confidenceSchema,
216
+ });
217
+
218
+ /** Self-reported guardrail counters, verbatim from a `BM-REPORT` (REQ-042c). */
219
+ export const guardrailReportSchema = z.object({
220
+ batchId: z.string().nullable(),
221
+ batchReviews: z.number().int().nonnegative().nullable(),
222
+ batchMax: z.number().int().nonnegative().nullable(),
223
+ polish: z.number().int().nonnegative().nullable(),
224
+ polishMax: z.number().int().nonnegative().nullable(),
225
+ total: z.number().int().nonnegative().nullable(),
226
+ budget: z.number().int().nonnegative().nullable(),
227
+ userAllowedExtra: z.number().int().nonnegative().nullable(),
228
+ raw: z.string(),
229
+ });
230
+
231
+ /** One row of the trace list (design §4.2). */
232
+ export const traceSummarySchema = z.object({
233
+ traceId: z.string().min(1),
234
+ requestId: z.string().nullable(),
235
+ requestedAt: z.string(),
236
+ /** Null when no user turn was captured, so the screen says so rather than showing "". */
237
+ excerpt: z.string().nullable(),
238
+ /**
239
+ * Which turn of the request this row is, when the user asked more than once
240
+ * (delta 20260917e §4.3). `null` for a request that was never followed up, so
241
+ * those rows look exactly as they always did — no `lượt 1` noise.
242
+ *
243
+ * Rows of one request share a `traceId` on purpose: opening any of them leads
244
+ * to the same request. The list key is the pair.
245
+ */
246
+ turn: z.object({ index: z.number().int().positive(), total: z.number().int().positive() }).nullable(),
247
+ state: traceStateSchema,
248
+ workerIds: z.array(agentIdSchema),
249
+ reviewerIds: z.array(agentIdSchema),
250
+ reviewCalls: z.number().int().nonnegative().nullable(),
251
+ guardrailReported: guardrailReportSchema.nullable(),
252
+ durationMs: z.number().int().nonnegative().nullable(),
253
+ usage: usageSchema,
254
+ /** Messages sent and received by the request's agents, for the overview. */
255
+ messageCount: z.number().int().nonnegative(),
256
+ /** Messages the user typed directly to one of the request's agents. */
257
+ userMessageCount: z.number().int().nonnegative(),
258
+ /** Tokens and cost per Worker of this request, for the "heaviest Workers" chart. */
259
+ workerUsage: z.array(z.object({ agentId: agentIdSchema, title: z.string().nullable(), usage: usageSchema })),
260
+ /**
261
+ * Tokens and cost per (role, model the agents ran), for the overview's
262
+ * "model × role" chart (delta 20260918 §4.3, REQ-058e). Always sent by this
263
+ * server; optional so an older payload still parses.
264
+ */
265
+ usageByModelRole: z
266
+ .array(z.object({ role: agentRoleSchema, model: z.string().nullable(), usage: usageSchema }))
267
+ .optional(),
268
+ beadCounts: z.object({
269
+ created: beadCountSchema,
270
+ updated: beadCountSchema,
271
+ closed: beadCountSchema,
272
+ ready: beadCountSchema,
273
+ }),
274
+ tier: tierSchema.nullable(),
275
+ linking: confidenceSchema,
276
+ agentsMissing: z.array(agentIdSchema),
277
+ workspaceState: workspaceStateSchema,
278
+ reassignedFrom: workspaceIdSchema.nullable(),
279
+ notices: z.array(z.string()),
280
+ });
281
+
282
+ /** Milestone of a `BM-REPORT` block (roles/worker.md, Reporting). */
283
+ export const reportPhaseSchema = z.enum([
284
+ "received",
285
+ "documents-done",
286
+ "beads-done",
287
+ "bead-implemented",
288
+ "blocked",
289
+ "finished",
290
+ ]);
291
+
292
+ /**
293
+ * One parsed `BM-REPORT`. Every field is nullable on purpose: the parser is
294
+ * required to be tolerant, and a field it could not read becomes "không rõ"
295
+ * rather than breaking the trace (REQ-050a).
296
+ */
297
+ export const parsedReportSchema = z.object({
298
+ agentId: agentIdSchema,
299
+ at: z.string(),
300
+ requestId: z.string().nullable(),
301
+ phase: reportPhaseSchema.nullable(),
302
+ tier: tierSchema.nullable(),
303
+ filesChanged: z.array(z.string()),
304
+ beadsCreated: z.array(z.string()),
305
+ beadsUpdated: z.array(z.string()),
306
+ beadsClosed: z.array(z.string()),
307
+ beadsReady: z.array(z.string()),
308
+ reviewFindingsOpen: z.string().nullable(),
309
+ buildAndTests: z.string().nullable(),
310
+ /** Skills the Worker loaded for the request (delta 20260917 §4.9); defaulted for older records. */
311
+ skillsUsed: z.array(z.string()).default([]),
312
+ blockers: z.string().nullable(),
313
+ guardrail: guardrailReportSchema.nullable(),
314
+ unparsedFields: z.array(z.string()),
315
+ /**
316
+ * Bead-id list fields the parser could not fully read (delta 20260917 §5.1):
317
+ * their ids are a lower bound. Defaulted so records written before the field
318
+ * existed still pass `traceRecordSchema` (the store drops undeclared keys).
319
+ */
320
+ incompleteFields: z.array(z.string()).default([]),
321
+ });
322
+
323
+ /** One parsed `BM-REVIEW` from a Reviewer (roles/reviewer.md). */
324
+ export const parsedReviewSchema = z.object({
325
+ agentId: agentIdSchema,
326
+ at: z.string(),
327
+ batchId: z.string().nullable(),
328
+ verdict: z.string().nullable(),
329
+ blockingCount: z.number().int().nonnegative().nullable(),
330
+ });
331
+
332
+ /** A message the Dashboard shows as "sent" or "received" (REQ-043a, REQ-043b). */
333
+ export const traceMessageSchema = z.object({
334
+ agentId: agentIdSchema.nullable(),
335
+ at: z.string(),
336
+ text: z.string(),
337
+ truncated: z.boolean(),
338
+ /**
339
+ * Who wrote an inbound message: `user` when Paseo's app sent it (the
340
+ * timeline item carries `clientMessageId`), `agent` when another agent sent
341
+ * it with `send_agent_prompt`. Absent on records written before this field
342
+ * existed — never read that as "user".
343
+ */
344
+ origin: z.enum(["user", "agent"]).optional(),
345
+ });
346
+
347
+ /** Timing of one agent inside a trace (design §7.1). */
348
+ export const agentTimingSchema = z.object({
349
+ agentId: agentIdSchema,
350
+ role: agentRoleSchema,
351
+ startedAt: z.string().nullable(),
352
+ lastActivityAt: z.string().nullable(),
353
+ ms: z.number().int().nonnegative().nullable(),
354
+ state: traceStateSchema,
355
+ });
356
+
357
+ /** Timing of one Manager turn (design §7.1). */
358
+ export const turnTimingSchema = z.object({
359
+ turnId: z.string().nullable(),
360
+ startedAt: z.string().nullable(),
361
+ endedAt: z.string().nullable(),
362
+ ms: z.number().int().nonnegative().nullable(),
363
+ });
364
+
365
+ /**
366
+ * The twelve fixed feature-workflow steps the Dashboard reports on (REQ-045a;
367
+ * `review_plan` added by delta 20260917-workflow-skills §5.5).
368
+ */
369
+ export const workflowStepSchema = z.enum([
370
+ "classify_tier",
371
+ "prd",
372
+ "design",
373
+ "adr",
374
+ "plan",
375
+ "review_plan",
376
+ "convert_to_beads",
377
+ "polish_beads",
378
+ "implement",
379
+ "review_batches",
380
+ "build_and_tests",
381
+ "close_with_evidence",
382
+ ]);
383
+
384
+ /**
385
+ * One row of the workflow table. `skipped` is only legal when the Worker stated
386
+ * a tier that REQ-036 allows to skip that step; everything else is `unknown`
387
+ * (REQ-045d) — the rule that keeps the table from accusing a good run.
388
+ */
389
+ export const workflowStepResultSchema = z.object({
390
+ step: workflowStepSchema,
391
+ status: z.enum(["done", "skipped", "unknown"]),
392
+ confidence: confidenceSchema,
393
+ evidence: z.array(evidenceSchema),
394
+ note: z.string().nullable(),
395
+ });
396
+
397
+ /** A bead the trace touched, with its current state read from the store (REQ-044). */
398
+ export const traceBeadSchema = z.object({
399
+ id: z.string().min(1),
400
+ title: z.string().nullable(),
401
+ statusNow: z.string().nullable(),
402
+ action: z.enum(["created", "updated", "closed", "ready"]),
403
+ confidence: confidenceSchema,
404
+ evidence: z.array(evidenceSchema),
405
+ });
406
+
407
+ /**
408
+ * A provider-internal sub-agent seen only as a tool-call trace. Paseo 0.8 gives
409
+ * plugins no way to list them (`listProviderSubagents` is `DaemonClient`-only),
410
+ * so these are counted as traces, and labelled as such (REQ-042e).
411
+ */
412
+ export const subAgentTraceSchema = z.object({
413
+ agentId: agentIdSchema,
414
+ subAgentType: z.string().nullable(),
415
+ description: z.string().nullable(),
416
+ count: z.number().int().nonnegative(),
417
+ });
418
+
419
+ /** Full detail of one trace (design §4.3). */
420
+ /**
421
+ * One combination of model, thinking option and mode an agent ran on, and in
422
+ * how many turns (delta 20260918 §4.3, REQ-058 a–c). `recorded: false` means the
423
+ * turn predates the collector recording it (or its snapshot could not be read):
424
+ * the model then comes from `usage` and thinking/mode are unknown — which is not
425
+ * the same as `recorded: true` with `thinkingOptionId: null`, the provider's default.
426
+ */
427
+ export const runtimeRowSchema = z.object({
428
+ model: z.string().nullable(),
429
+ thinkingOptionId: z.string().nullable(),
430
+ modeId: z.string().nullable(),
431
+ recorded: z.boolean(),
432
+ turns: z.number().int().positive(),
433
+ });
434
+
435
+ export const traceDetailSchema = traceSummarySchema.extend({
436
+ sent: z.object({
437
+ userRequest: traceMessageSchema.nullable(),
438
+ workerInitialPrompts: z.array(traceMessageSchema),
439
+ reviewRequests: z.array(traceMessageSchema.extend({ batchId: z.string().nullable() })),
440
+ }),
441
+ received: z.object({
442
+ reports: z.array(parsedReportSchema),
443
+ reviews: z.array(parsedReviewSchema),
444
+ managerReplies: z.array(traceMessageSchema),
445
+ }),
446
+ timing: z.object({
447
+ totalMs: z.number().int().nonnegative().nullable(),
448
+ managerTurns: z.array(turnTimingSchema),
449
+ workers: z.array(agentTimingSchema),
450
+ reviewers: z.array(agentTimingSchema),
451
+ basis: z.string(),
452
+ }),
453
+ usageByAgent: z.array(
454
+ z.object({
455
+ agentId: agentIdSchema,
456
+ role: agentRoleSchema,
457
+ usage: usageSchema,
458
+ /** What this agent ran on, one row per distinct combination (delta 20260918 §4.3). */
459
+ runtime: z.array(runtimeRowSchema).optional(),
460
+ }),
461
+ ),
462
+ /** Tokens and cost per model the agents actually ran (delta 20260918 §4.3, REQ-058d). */
463
+ usageByModel: z.array(z.object({ model: z.string().nullable(), usage: usageSchema })).optional(),
464
+ beads: z.array(traceBeadSchema),
465
+ workflowSteps: z.array(workflowStepResultSchema),
466
+ subAgentTraces: z.array(subAgentTraceSchema),
467
+ /** Messages the user typed directly to an agent of this request, oldest first. */
468
+ userMessages: z.array(traceMessageSchema),
469
+ /** Skills each agent loaded, oldest first. */
470
+ skills: z.array(z.object({ agentId: agentIdSchema, skill: z.string(), at: z.string().nullable() })),
471
+ });
472
+
473
+ /**
474
+ * Bead statistics of one workspace (REQ-046). `present: false` means the
475
+ * workspace has no `.beads/issues.jsonl` — an empty state, not an error.
476
+ */
477
+ export const beadStatsSchema = z.object({
478
+ total: z.number().int().nonnegative(),
479
+ open: z.number().int().nonnegative(),
480
+ inProgress: z.number().int().nonnegative(),
481
+ blocked: z.number().int().nonnegative(),
482
+ closed: z.number().int().nonnegative(),
483
+ ready: z.number().int().nonnegative(),
484
+ readAt: z.string(),
485
+ source: z.string(),
486
+ skippedLines: z.number().int().nonnegative(),
487
+ present: z.boolean(),
488
+ });
489
+
490
+ /** Size of the trace store, measured with `stat` and never by reading content. */
491
+ export const storeSizeSchema = z.object({
492
+ bytes: z.number().int().nonnegative(),
493
+ workspaceBytes: z.number().int().nonnegative(),
494
+ });
495
+
496
+ // ---------------------------------------------------------------------------
497
+ // Persisted format, schema version 1 (design §3.2, §3.3).
498
+ //
499
+ // These shapes live on disk under `<install home>/traces/`. A reader that meets
500
+ // a higher `schemaVersion` reads what it understands and refuses to write
501
+ // (`E_TRACE_STORE_SCHEMA_TOO_NEW`), so a new record must only ever *add*
502
+ // optional fields.
503
+ // ---------------------------------------------------------------------------
504
+
505
+ /** Current version of the persisted trace record and store metadata. */
506
+ export const TRACE_STORE_SCHEMA_VERSION = 1;
507
+
508
+ /** `<install home>/traces/meta.json`. */
509
+ export const traceStoreMetaSchema = z.object({
510
+ schemaVersion: z.number().int().positive(),
511
+ createdAt: z.string(),
512
+ updatedAt: z.string(),
513
+ });
514
+
515
+ /** `<install home>/traces/<workspaceId>/meta.json` — lets an orphaned workspace still be recognisable (REQ-057a). */
516
+ export const traceWorkspaceMetaSchema = z.object({
517
+ lastKnownName: z.string().nullable(),
518
+ lastKnownDirectory: z.string().nullable(),
519
+ lastSeenAt: z.string(),
520
+ });
521
+
522
+ /**
523
+ * What the agent actually ran on during one turn (delta 20260918 §4.2): the
524
+ * running values the provider reports (`runtimeInfo`) first, the snapshot's
525
+ * configuration only as a fallback. `thinkingOptionId: null` means the
526
+ * provider's default.
527
+ *
528
+ * `provider` was added by delta 20260921 §4.2.7 (F7) so a model missing from
529
+ * the bundled price table can be priced from its provider's model list. It is
530
+ * optional for the same reason `runtime` is: records written before it, and
531
+ * readers built before it, keep working — `v` stays 1, nothing migrates. `null`
532
+ * when the snapshot names no provider.
533
+ */
534
+ export const traceRuntimeSchema = z.object({
535
+ model: z.string().nullable(),
536
+ thinkingOptionId: z.string().nullable(),
537
+ modeId: z.string().nullable(),
538
+ provider: z.string().nullable().optional(),
539
+ });
540
+
541
+ /** One line of `events-<YYYYMM>.jsonl`: everything one agent turn produced. */
542
+ export const traceRecordSchema = z.object({
543
+ v: z.number().int().positive(),
544
+ kind: z.literal("turn"),
545
+ at: z.string(),
546
+ workspaceId: workspaceIdSchema,
547
+ agentId: agentIdSchema,
548
+ role: agentRoleSchema,
549
+ turnId: z.string().nullable(),
550
+ requestId: z.string().nullable(),
551
+ parentAgentId: agentIdSchema.nullable(),
552
+ agentCreatedAt: z.string().nullable(),
553
+ startedAt: z.string().nullable(),
554
+ endedAt: z.string(),
555
+ outcome: z.enum(["completed", "failed", "canceled"]),
556
+ sent: z.array(traceMessageSchema),
557
+ received: z.array(traceMessageSchema),
558
+ reports: z.array(parsedReportSchema),
559
+ reviews: z.array(parsedReviewSchema),
560
+ evidence: z.array(evidenceSchema),
561
+ usage: usageSchema.nullable(),
562
+ /**
563
+ * Added by delta 20260918 — optional, so records written before it (and
564
+ * readers built before it) keep working: `v` stays 1, nothing migrates. `null`
565
+ * when the snapshot could not be read for this turn.
566
+ */
567
+ runtime: traceRuntimeSchema.nullable().optional(),
568
+ });
569
+
570
+ // ---------------------------------------------------------------------------
571
+ // Error codes.
572
+ //
573
+ // Registered in the single product-wide registry (design gốc §4.4, delta
574
+ // `design-delta-20260916-trace-store`). They travel over the plugin RPC channel
575
+ // so they have no CLI exit code.
576
+ // ---------------------------------------------------------------------------
577
+
578
+ export const DASHBOARD_ERROR_CODES = [
579
+ "E_TIMELINE_UNAVAILABLE",
580
+ "E_BEADS_STORE_UNREADABLE",
581
+ "E_TRACE_NOT_FOUND",
582
+ "E_TRACE_STORE_UNWRITABLE",
583
+ "E_TRACE_STORE_SCHEMA_TOO_NEW",
584
+ "E_TRACE_REASSIGN_INVALID",
585
+ "E_BEAD_NOT_FOUND",
586
+ "E_ROLE_EXTRA_INVALID",
587
+ "E_TOOL_PRESENT",
588
+ "E_TOOL_INSTALL_FAILED",
589
+ // Delta 20260921 §4.3.3–§4.3.4 (ADR-008): the plugin's own writes of the role settings.
590
+ "E_ROLE_SETTINGS_INVALID",
591
+ "E_ROLE_SETTINGS_CONFLICT",
592
+ "E_ROLE_SETTINGS_WRITE_FAILED",
593
+ // Delta 20260921 §4.4.6: the fallback card's actions.
594
+ "E_FALLBACK_NOT_FOUND",
595
+ "E_FALLBACK_NOT_PENDING",
596
+ "E_FALLBACK_NO_CANDIDATE",
597
+ "E_FALLBACK_NO_RESET",
598
+ "E_FALLBACK_CREATE_FAILED",
599
+ ] as const;
600
+
601
+ export type DashboardErrorCode = (typeof DASHBOARD_ERROR_CODES)[number];
602
+
603
+ /**
604
+ * Coded failure of a Dashboard RPC. The message starts with the code so it
605
+ * survives transports that only forward `message`, exactly like
606
+ * `ManagerEnsureError` does for `manager.ensure`.
607
+ */
608
+ export class DashboardError extends Error {
609
+ readonly code: DashboardErrorCode;
610
+
611
+ constructor(code: DashboardErrorCode, detail: string, options?: { cause?: unknown }) {
612
+ super(`${code}: ${detail}`, options);
613
+ this.name = "DashboardError";
614
+ this.code = code;
615
+ }
616
+ }
617
+
618
+ // ---------------------------------------------------------------------------
619
+ // RPC contracts (design §5). All five are new; the three Phase 1 contracts
620
+ // above are unchanged.
621
+ // ---------------------------------------------------------------------------
622
+
623
+ /** Largest page `traces.list` will return, and its default (REQ-049a, Q-034). */
624
+ export const TRACE_LIST_LIMIT = 50;
625
+
626
+ /** `traces.list` — one page of trace rows for a workspace, newest first. */
627
+ export const tracesListRpc = defineRpc({
628
+ name: "traces.list",
629
+ input: z.object({
630
+ workspaceId: workspaceIdSchema,
631
+ limit: z.number().int().positive().max(TRACE_LIST_LIMIT).optional(),
632
+ cursor: z.string().min(1).optional(),
633
+ }),
634
+ output: z.object({
635
+ traces: z.array(traceSummarySchema),
636
+ nextCursor: z.string().nullable(),
637
+ truncated: z.boolean(),
638
+ store: storeSizeSchema,
639
+ notices: z.array(z.string()),
640
+ }),
641
+ });
642
+
643
+ /** `traces.get` — full detail of one trace; unknown id fails `E_TRACE_NOT_FOUND`. */
644
+ export const tracesGetRpc = defineRpc({
645
+ name: "traces.get",
646
+ input: z.object({
647
+ workspaceId: workspaceIdSchema,
648
+ traceId: z.string().min(1),
649
+ }),
650
+ output: z.object({
651
+ trace: traceDetailSchema,
652
+ }),
653
+ });
654
+
655
+ /**
656
+ * Scope of a deletion. Exactly one of the three shapes (REQ-054a); the union is
657
+ * what stops a caller from half-specifying a destructive operation.
658
+ */
659
+ export const traceDeleteScopeSchema = z.union([
660
+ z.object({ traceId: z.string().min(1) }),
661
+ z.object({ before: z.string().min(1) }),
662
+ z.object({ allOfWorkspace: z.literal(true) }),
663
+ ]);
664
+
665
+ /** `traces.delete` — `dryRun: true` only counts, so the UI can confirm first. */
666
+ export const tracesDeleteRpc = defineRpc({
667
+ name: "traces.delete",
668
+ input: z.object({
669
+ workspaceId: workspaceIdSchema,
670
+ scope: traceDeleteScopeSchema,
671
+ dryRun: z.boolean().optional(),
672
+ }),
673
+ output: z.object({
674
+ deleted: z.object({
675
+ traces: z.number().int().nonnegative(),
676
+ bytes: z.number().int().nonnegative(),
677
+ /** Requests in scope that still have a running agent (REQ-054e). */
678
+ running: z.number().int().nonnegative(),
679
+ }),
680
+ store: storeSizeSchema,
681
+ }),
682
+ });
683
+
684
+ /**
685
+ * `traces.reassign` — move the traces of a workspace that is gone onto one that
686
+ * exists (REQ-057d). Never automatic: only a user action reaches this RPC.
687
+ */
688
+ export const tracesReassignRpc = defineRpc({
689
+ name: "traces.reassign",
690
+ input: z.object({
691
+ fromWorkspaceId: workspaceIdSchema,
692
+ toWorkspaceId: workspaceIdSchema,
693
+ dryRun: z.boolean().optional(),
694
+ }),
695
+ output: z.object({
696
+ moved: z.object({
697
+ traces: z.number().int().nonnegative(),
698
+ bytes: z.number().int().nonnegative(),
699
+ }),
700
+ store: storeSizeSchema,
701
+ }),
702
+ });
703
+
704
+ /** `beads.stats` — counts of the workspace's bead store; missing file is not an error. */
705
+ export const beadsStatsRpc = defineRpc({
706
+ name: "beads.stats",
707
+ input: z.object({
708
+ workspaceId: workspaceIdSchema,
709
+ }),
710
+ output: z.object({
711
+ stats: beadStatsSchema,
712
+ }),
713
+ });
714
+
715
+ /** One bead in the Beads screen list (delta 20260916-beads-screen). */
716
+ /**
717
+ * `workspaces.overview` — per listed workspace: its bead counts (null when the
718
+ * workspace has no readable bead store) and how many Workers are running now.
719
+ * One call for the whole Beads Manager screen.
720
+ */
721
+ export const workspacesOverviewRpc = defineRpc({
722
+ name: "workspaces.overview",
723
+ input: z.object({}),
724
+ output: z.object({
725
+ workspaces: z.array(
726
+ z.object({
727
+ workspaceId: workspaceIdSchema,
728
+ beads: z
729
+ .object({
730
+ total: z.number().int().nonnegative(),
731
+ inProgress: z.number().int().nonnegative(),
732
+ blocked: z.number().int().nonnegative(),
733
+ ready: z.number().int().nonnegative(),
734
+ })
735
+ .nullable(),
736
+ /** Kept for readers that predate `runningAgents`; equals `runningAgents.worker`. */
737
+ runningWorkers: z.number().int().nonnegative(),
738
+ /** Running agents of this workspace, per role (delta 20260917e §4.2). */
739
+ runningAgents: z.object({
740
+ manager: z.number().int().nonnegative(),
741
+ worker: z.number().int().nonnegative(),
742
+ reviewer: z.number().int().nonnegative(),
743
+ }),
744
+ }),
745
+ ),
746
+ }),
747
+ });
748
+
749
+ export type WorkspaceOverview = z.infer<typeof workspacesOverviewRpc.output>["workspaces"][number];
750
+
751
+ /**
752
+ * `agents.stop-all` — asks every running Worker and Reviewer of one workspace to
753
+ * stop (delta 20260917e §4.4). It ASKS: Paseo gives plugins no agent cancel, so
754
+ * the counts below are notices delivered, not turns killed.
755
+ */
756
+ export const agentsStopAllRpc = defineRpc({
757
+ name: "agents.stop-all",
758
+ input: z.object({ workspaceId: workspaceIdSchema }),
759
+ output: z.object({
760
+ workers: z.number().int().nonnegative(),
761
+ reviewers: z.number().int().nonnegative(),
762
+ skipped: z.number().int().nonnegative(),
763
+ }),
764
+ });
765
+
766
+ const answerMarksOutput = z.object({
767
+ /** Card keys (`answeredKey`) the user marked as answered, oldest first. */
768
+ keys: z.array(z.string()),
769
+ notices: z.array(z.string()),
770
+ });
771
+
772
+ /** `answers.marks` — the cards the user marked as answered (delta 20260918d §4.9). */
773
+ export const answersMarksRpc = defineRpc({
774
+ name: "answers.marks",
775
+ input: z.object({}),
776
+ output: answerMarksOutput,
777
+ });
778
+
779
+ /** `answers.mark` — marks one card as answered, or removes the mark (delta 20260918d §4.9). */
780
+ export const answersMarkRpc = defineRpc({
781
+ name: "answers.mark",
782
+ input: z.object({ key: z.string().min(1).max(400), marked: z.boolean() }),
783
+ output: answerMarksOutput,
784
+ });
785
+
786
+ // ---------------------------------------------------------------------------
787
+ // Setup screen (delta 20260916-setup-screen).
788
+ // ---------------------------------------------------------------------------
789
+
790
+ export const setupRoleSchema = z.enum(["manager", "worker", "reviewer"]);
791
+ const skillStateSchema = z.enum(["ok", "missing", "broken"]);
792
+
793
+ /** One Paseo-tools check (`plugin/server/tools-check.ts`). */
794
+ export const toolsSeenSchema = z.object({
795
+ state: z.enum(["ok", "missing", "unknown"]),
796
+ agentId: z.string(),
797
+ provider: z.string(),
798
+ at: z.string(),
799
+ });
800
+
801
+ export const setupStatusSchema = z.object({
802
+ tools: z.array(
803
+ z.object({
804
+ id: z.enum(["br", "bv", "bd"]),
805
+ name: z.string(),
806
+ purpose: z.string(),
807
+ required: z.boolean(),
808
+ path: z.string().nullable(),
809
+ version: z.string().nullable(),
810
+ latestKnown: z.string().nullable(),
811
+ installCommand: z.string().nullable(),
812
+ updateCommand: z.string().nullable(),
813
+ homepage: z.string(),
814
+ }),
815
+ ),
816
+ latestCheckedOn: z.string(),
817
+ /**
818
+ * `pi` and `opencode` (delta 20260921 §4.2.6) are optional so a payload
819
+ * from before them still parses; a client shows those columns only when
820
+ * they are present.
821
+ */
822
+ skills: z.object({
823
+ checkedAt: z.string(),
824
+ dirs: z.object({
825
+ shared: z.string(),
826
+ claude: z.string(),
827
+ codex: z.string(),
828
+ pi: z.string().optional(),
829
+ opencode: z.string().optional(),
830
+ }),
831
+ skills: z.array(
832
+ z.object({
833
+ name: z.string(),
834
+ required: z.boolean(),
835
+ claude: skillStateSchema,
836
+ codex: skillStateSchema,
837
+ pi: skillStateSchema.optional(),
838
+ opencode: skillStateSchema.optional(),
839
+ problem: z.string().nullable(),
840
+ }),
841
+ ),
842
+ missingRequired: z.object({
843
+ claude: z.number().int(),
844
+ codex: z.number().int(),
845
+ pi: z.number().int().optional(),
846
+ opencode: z.number().int().optional(),
847
+ }),
848
+ installCommand: z.string(),
849
+ }),
850
+ /** Characters of additional instructions per role; 0 when none. */
851
+ extras: z.object({ manager: z.number().int(), worker: z.number().int(), reviewer: z.number().int() }),
852
+ /**
853
+ * The last Paseo-tools check of a new Manager and of a new Worker in this
854
+ * plugin run, `null` when none ran (delta 20260921 §4.2.4). Optional: an
855
+ * older server does not send it.
856
+ */
857
+ paseoTools: z
858
+ .object({ manager: toolsSeenSchema.nullable(), worker: toolsSeenSchema.nullable() })
859
+ .optional(),
860
+ });
861
+
862
+ export type SetupStatus = z.infer<typeof setupStatusSchema>;
863
+
864
+ /** `setup.status` — tools, skills and extras at a glance. Read-only; runs `--version` only. */
865
+ export const setupStatusRpc = defineRpc({ name: "setup.status", input: z.object({}), output: setupStatusSchema });
866
+
867
+ /** `setup.install-tool` — runs the documented installer for a missing `br` or `bv`, after the user confirmed. */
868
+ export const setupInstallToolRpc = defineRpc({
869
+ name: "setup.install-tool",
870
+ /** `confirmed` must be `true`: the Setup screen sends it only after the user confirmed the exact command. */
871
+ input: z.object({ tool: z.enum(["br", "bv"]), confirmed: z.literal(true) }),
872
+ output: z.object({ command: z.string(), code: z.number().int(), tail: z.array(z.string()) }),
873
+ });
874
+
875
+ /** `roles.instructions` — a role's base instructions, the user's additions and the result. */
876
+ export const rolesInstructionsRpc = defineRpc({
877
+ name: "roles.instructions",
878
+ input: z.object({ role: setupRoleSchema }),
879
+ output: z.object({ base: z.string(), extra: z.string(), full: z.string(), path: z.string().nullable(), maxChars: z.number().int() }),
880
+ });
881
+
882
+ /** `roles.save-extra` — replaces one role's additional instructions. Applies to agents created afterwards. */
883
+ export const rolesSaveExtraRpc = defineRpc({
884
+ name: "roles.save-extra",
885
+ input: z.object({ role: setupRoleSchema, text: z.string() }),
886
+ output: z.object({ extra: z.string(), full: z.string() }),
887
+ });
888
+
889
+ // ---------------------------------------------------------------------------
890
+ // Roles & models (delta 20260921 §4.3.2, REQ-064 a/b). Read-only; saving is
891
+ // `roles.save-settings` (§4.3.4). `roles.describe` stays for older clients.
892
+ // ---------------------------------------------------------------------------
893
+
894
+ /**
895
+ * How a provider lets paseo-bm choose a role's start mode (delta 20260921
896
+ * §4.2.1): `tiered` (modes carry a `colorTier`: Claude, Codex), `untiered`
897
+ * (modes without one: OpenCode), `none` (no modes, no error: Pi), `unknown`
898
+ * (the modes could not be read).
899
+ */
900
+ export const providerCapabilitySchema = z.enum(["tiered", "untiered", "none", "unknown"]);
901
+
902
+ /**
903
+ * One role as Paseo's configuration has it right now: `baseProvider` and
904
+ * `label` from the derived provider `bm-<role>` (`extends`, `label`); `model`,
905
+ * `thinkingOptionId`, `modeId` and `featureValues` from the agent profile
906
+ * `bm-<role>`. A field the configuration does not set is `null`
907
+ * (`featureValues`: `{}`); `capability` is that of `baseProvider`.
908
+ */
909
+ export const roleSettingSchema = z.object({
910
+ role: bmRoleSchema,
911
+ providerId: z.enum(["bm-manager", "bm-worker", "bm-reviewer"]),
912
+ baseProvider: z.string().nullable(),
913
+ label: z.string().nullable(),
914
+ model: z.string().nullable(),
915
+ thinkingOptionId: z.string().nullable(),
916
+ modeId: z.string().nullable(),
917
+ featureValues: z.record(z.string(), z.unknown()),
918
+ capability: providerCapabilitySchema,
919
+ });
920
+
921
+ /**
922
+ * One entry of a role's fallback chain as the user saves it (delta 20260921
923
+ * §4.4.2–§4.4.3): a BASE provider (never a `bm-*` alias), one of its models,
924
+ * and optional thinking and mode (`null` = not set).
925
+ */
926
+ export const fallbackEntryInputSchema = z.object({
927
+ baseProvider: z.string().min(1).max(200),
928
+ model: z.string().min(1).max(200),
929
+ thinkingOptionId: z.string().min(1).max(200).nullable(),
930
+ modeId: z.string().min(1).max(200).nullable(),
931
+ });
932
+
933
+ /**
934
+ * A role's fallback chain as Roles & models shows it: the policy (`ask`
935
+ * shows a card when the role hits a plan limit, `off` does nothing; `auto`
936
+ * only from phase 2a-18), the entries in order with the alias each runs on
937
+ * (`bm-<role>-fallback-<position>`), the capability and listed price of each
938
+ * entry's provider and model, and whether detection patterns come from the
939
+ * user's `role-fallback.json`.
940
+ */
941
+ export const fallbackSettingsSchema = z.object({
942
+ role: bmRoleSchema,
943
+ policy: z.enum(["ask", "off", "auto"]),
944
+ entries: z.array(
945
+ fallbackEntryInputSchema.extend({
946
+ position: z.number().int().min(1).max(3),
947
+ alias: z.string(),
948
+ capability: providerCapabilitySchema,
949
+ cost: modelPriceSchema.nullable(),
950
+ }),
951
+ ),
952
+ patternsFromFile: z.boolean(),
953
+ });
954
+
955
+ /**
956
+ * `roles.settings` — the three roles, always in the order manager, worker,
957
+ * reviewer. `revision` is what `roles.save-settings` must send back: sha256 of
958
+ * the canonical JSON of every `bm-*` provider and the whole profile array
959
+ * (§4.3.2). `fallback` holds the chain of each role whose chain is offered
960
+ * (`FALLBACK_ROLES`, §4.4.3); `null` when none is.
961
+ * `warnings` are English sentences for the user, e.g. the shared-plan warning
962
+ * when the Manager and the Worker extend the same base provider.
963
+ */
964
+ export const rolesSettingsRpc = defineRpc({
965
+ name: "roles.settings",
966
+ input: z.object({}),
967
+ output: z.object({
968
+ revision: z.string(),
969
+ roles: z.array(roleSettingSchema),
970
+ fallback: z
971
+ .object({
972
+ manager: fallbackSettingsSchema.optional(),
973
+ worker: fallbackSettingsSchema.optional(),
974
+ reviewer: fallbackSettingsSchema.optional(),
975
+ })
976
+ .nullable(),
977
+ warnings: z.array(z.string()),
978
+ /** The base providers Paseo reports as available (never a `bm-*` alias), for the Edit form's Provider picker. */
979
+ providers: z.array(z.string()),
980
+ }),
981
+ });
982
+
983
+ /** A model a role can run, as `providers.listModels` lists it; `cost` from its `metadata.cost` (§4.2.7). */
984
+ export const roleModelOptionSchema = z.object({
985
+ id: z.string(),
986
+ label: z.string(),
987
+ thinkingOptions: z.array(z.object({ id: z.string(), label: z.string() })),
988
+ defaultThinkingOptionId: z.string().nullable(),
989
+ cost: modelPriceSchema.nullable(),
990
+ });
991
+
992
+ /** A mode as `providers.listModes` lists it; `colorTier` is `null` when the provider gives none. */
993
+ export const roleModeOptionSchema = z.object({
994
+ id: z.string(),
995
+ label: z.string(),
996
+ colorTier: z.string().nullable(),
997
+ });
998
+
999
+ /**
1000
+ * `roles.options` — what the Edit form of Roles & models may offer for one
1001
+ * BASE provider (never a `bm-*` alias: refused with `E_ROLE_SETTINGS_INVALID`).
1002
+ * Only values Paseo lists; a list Paseo cannot give is empty. `autoAccept` is
1003
+ * true when an `untiered` provider offers the `auto_accept` toggle.
1004
+ */
1005
+ export const rolesOptionsRpc = defineRpc({
1006
+ name: "roles.options",
1007
+ input: z.object({ provider: z.string().min(1) }),
1008
+ output: z.object({
1009
+ provider: z.string(),
1010
+ capability: providerCapabilitySchema,
1011
+ models: z.array(roleModelOptionSchema),
1012
+ modes: z.array(roleModeOptionSchema),
1013
+ autoAccept: z.boolean(),
1014
+ }),
1015
+ });
1016
+
1017
+ /**
1018
+ * `roles.save-settings` — writes one role's base provider, model, thinking and
1019
+ * mode into Paseo's config (delta 20260921 §4.3.2–§4.3.4, ADR-008). `null`
1020
+ * thinking / mode means "not set" (the key is removed). `notified` counts the
1021
+ * live agents told a changed child mode (BM-SETTINGS, §4.3.5).
1022
+ */
1023
+ export const rolesSaveSettingsRpc = defineRpc({
1024
+ name: "roles.save-settings",
1025
+ input: z.object({
1026
+ revision: z.string().min(1),
1027
+ role: bmRoleSchema,
1028
+ baseProvider: z.string().min(1).max(200),
1029
+ model: z.string().min(1).max(200),
1030
+ thinkingOptionId: z.string().min(1).max(200).nullable(),
1031
+ modeId: z.string().min(1).max(200).nullable(),
1032
+ }),
1033
+ output: z.object({
1034
+ revision: z.string(),
1035
+ role: roleSettingSchema,
1036
+ warnings: z.array(z.string()),
1037
+ notified: z.number().int(),
1038
+ }),
1039
+ });
1040
+
1041
+ /**
1042
+ * `roles.save-fallback` — saves one role's policy and 0–3 fallback entries
1043
+ * (delta 20260921 §4.4.3, §4.6): the aliases through the config writer (so
1044
+ * `revision` guards them like `roles.save-settings`), then `role-fallback.json`.
1045
+ */
1046
+ export const rolesSaveFallbackRpc = defineRpc({
1047
+ name: "roles.save-fallback",
1048
+ input: z.object({
1049
+ revision: z.string().min(1),
1050
+ role: bmRoleSchema,
1051
+ /** `auto` (REQ-067, phase 2a-18): the plugin decides at once, by the same rules as the card's buttons. */
1052
+ policy: z.enum(["ask", "off", "auto"]),
1053
+ entries: z.array(fallbackEntryInputSchema).max(3),
1054
+ }),
1055
+ output: z.object({
1056
+ revision: z.string(),
1057
+ fallback: fallbackSettingsSchema,
1058
+ warnings: z.array(z.string()),
1059
+ }),
1060
+ });
1061
+
1062
+ /**
1063
+ * One recorded fallback incident (delta 20260921 §4.4.2, §4.4.5, REQ-065 j):
1064
+ * an agent whose turn ended on a provider-plan failure, what was learnt about
1065
+ * it (class, verbatim message cut to 500 characters, reset time), the next
1066
+ * candidate of its role's chain, and what became of it. Stored in
1067
+ * `<install home>/role-fallback-state.json`.
1068
+ */
1069
+ export const fallbackIncidentSchema = z.object({
1070
+ id: z.string().regex(/^fb-[0-9a-f]{12}$/),
1071
+ role: bmRoleSchema,
1072
+ workspaceId: z.string(),
1073
+ /** `null` for a Manager, which serves a workspace, not a request. */
1074
+ requestId: z.string().nullable(),
1075
+ agentId: z.string(),
1076
+ agentProvider: z.string(),
1077
+ agentModel: z.string().nullable(),
1078
+ /** The Worker of a Reviewer; the Manager of a Worker; `null` for a Manager. */
1079
+ parentId: z.string().nullable(),
1080
+ /** The chat whose card shows the incident. */
1081
+ managerId: z.string().nullable(),
1082
+ class: z.enum(["L1", "L2", "L4", "L5"]),
1083
+ /** N1 (`failed`) or N2 (`completed`). */
1084
+ signal: z.enum(["failed", "completed"]),
1085
+ message: z.string().max(500),
1086
+ perModelWindow: z.boolean(),
1087
+ resetsAt: z.string().nullable(),
1088
+ candidate: fallbackEntryInputSchema
1089
+ .extend({ position: z.number().int().min(1).max(3), alias: z.string() })
1090
+ .nullable(),
1091
+ status: z.enum(["pending", "switched", "waiting", "resumed", "dismissed", "exhausted", "expired", "failed"]),
1092
+ detectedAt: z.string(),
1093
+ decidedAt: z.string().nullable(),
1094
+ waitUntil: z.string().nullable(),
1095
+ replacementId: z.string().nullable(),
1096
+ error: z.string().nullable(),
1097
+ });
1098
+
1099
+ /**
1100
+ * `fallback.incidents` — the recorded incidents (§4.4.6), oldest first,
1101
+ * filtered by workspace and/or ids when given. The fallback card and the
1102
+ * waiting pill read their state here, never from the notice text, so an old
1103
+ * notice never shows a button that no longer applies.
1104
+ */
1105
+ export const fallbackIncidentsRpc = defineRpc({
1106
+ name: "fallback.incidents",
1107
+ input: z.object({ workspaceId: z.string().min(1).optional(), ids: z.array(z.string().min(1)).max(200).optional() }),
1108
+ output: z.object({ incidents: z.array(fallbackIncidentSchema) }),
1109
+ });
1110
+
1111
+ /**
1112
+ * `fallback.act` — the user's choice on the card for a `pending` incident:
1113
+ * switch to the candidate (§4.4.7, §4.5.1), wait for the reset (§4.4.9), or
1114
+ * handle it themselves (`dismiss`, §4.4.10). `resend` sends a switched
1115
+ * Reviewer's instructions to its Worker again when the notice queue lost them
1116
+ * (§7, "Resend to Worker"). Returns the incident as it is afterwards.
1117
+ */
1118
+ export const fallbackActRpc = defineRpc({
1119
+ name: "fallback.act",
1120
+ input: z.object({ incidentId: z.string().min(1), action: z.enum(["switch", "wait", "dismiss", "resend"]) }),
1121
+ output: z.object({ incident: fallbackIncidentSchema }),
1122
+ });
1123
+
1124
+ /**
1125
+ * Furthest ahead a `resetsAt` may lie for the card to offer "Wait until …"
1126
+ * and for `fallback.act` `wait` to accept it: 7 days (§4.4.6, §4.4.9).
1127
+ */
1128
+ export const FALLBACK_MAX_WAIT_MS = 7 * 24 * 60 * 60 * 1000;
1129
+
1130
+ export type ProviderCapabilityClass = z.infer<typeof providerCapabilitySchema>;
1131
+ export type FallbackActInput = z.infer<typeof fallbackActRpc.input>;
1132
+ export type FallbackIncident = z.infer<typeof fallbackIncidentSchema>;
1133
+ export type FallbackEntryInput = z.infer<typeof fallbackEntryInputSchema>;
1134
+ export type FallbackSettings = z.infer<typeof fallbackSettingsSchema>;
1135
+ export type RolesSaveFallbackInput = z.infer<typeof rolesSaveFallbackRpc.input>;
1136
+ export type RolesSaveSettingsInput = z.infer<typeof rolesSaveSettingsRpc.input>;
1137
+ export type RoleSetting = z.infer<typeof roleSettingSchema>;
1138
+ export type RolesSettings = z.infer<typeof rolesSettingsRpc.output>;
1139
+ export type RoleModelOption = z.infer<typeof roleModelOptionSchema>;
1140
+ export type RoleModeOption = z.infer<typeof roleModeOptionSchema>;
1141
+ export type RolesOptions = z.infer<typeof rolesOptionsRpc.output>;
1142
+
1143
+ /** One paseo-bm agent as a chat card needs it. */
1144
+ export const chatPeerSchema = z.object({
1145
+ id: agentIdSchema,
1146
+ role: agentRoleSchema,
1147
+ title: z.string().nullable(),
1148
+ status: z.string(),
1149
+ parentId: z.string().nullable(),
1150
+ /** `bm.requestId` / `bm.batchId` labels. */
1151
+ requestId: z.string().nullable(),
1152
+ batchId: z.string().nullable(),
1153
+ /**
1154
+ * False when the agent has no `bm.role` label and was recognised by its
1155
+ * provider only (delta 20260918g §4.4). Defaults to true for older servers.
1156
+ */
1157
+ labelled: z.boolean().default(true),
1158
+ /** Archived in Paseo: never a recipient, since a message would bring it back (delta 20260918f F12). */
1159
+ archived: z.boolean(),
1160
+ /**
1161
+ * A fallback agent took over from this one (`bm.replacedBy`, or the agent of
1162
+ * a `switched` incident, delta 20260921 §4.4.8): never the request's Worker
1163
+ * again. Defaults to false for older servers.
1164
+ */
1165
+ replaced: z.boolean().default(false),
1166
+ });
1167
+
1168
+ /** One Worker waiting for the user's answer in a Manager's chat (delta 20260918d §4.8). */
1169
+ export const waitingWorkerSchema = z.object({
1170
+ managerId: agentIdSchema,
1171
+ workspaceId: workspaceIdSchema,
1172
+ workerId: agentIdSchema,
1173
+ workerTitle: z.string().nullable(),
1174
+ requestId: z.string(),
1175
+ /** The report message as the Manager received it: the client builds the same card from it. */
1176
+ text: z.string(),
1177
+ at: z.string().nullable(),
1178
+ });
1179
+
1180
+ export type WaitingWorker = z.infer<typeof waitingWorkerSchema>;
1181
+
1182
+ /**
1183
+ * One `pending` fallback incident waiting for the user's decision in a live
1184
+ * Manager's chat (delta 20260921 §4.4.6): `managerId` is the incident's, and
1185
+ * `workspaceId` that Manager's, where its pill goes.
1186
+ */
1187
+ export const waitingFallbackSchema = z.object({
1188
+ managerId: agentIdSchema,
1189
+ workspaceId: workspaceIdSchema,
1190
+ incident: fallbackIncidentSchema,
1191
+ });
1192
+
1193
+ export type WaitingFallback = z.infer<typeof waitingFallbackSchema>;
1194
+
1195
+ /**
1196
+ * `chat.waiting` — for every paseo-bm Manager, the idle Workers whose latest
1197
+ * report to it is `blocked` with a `BM-QUESTIONS` block (delta 20260918d §4.8,
1198
+ * REQ-059 j), and the `pending` fallback incidents whose card is in its chat
1199
+ * (delta 20260921 §4.4.6), read from the incidents file, not the timeline, so
1200
+ * they show even when the Manager's own turn failed. `fallback` defaults to
1201
+ * empty for a server built before it. Read-only.
1202
+ */
1203
+ export const chatWaitingRpc = defineRpc({
1204
+ name: "chat.waiting",
1205
+ input: z.object({}),
1206
+ output: z.object({ waiting: z.array(waitingWorkerSchema), fallback: z.array(waitingFallbackSchema).default([]) }),
1207
+ });
1208
+
1209
+ /**
1210
+ * `chat.peers` — the paseo-bm agent that owns a chat and the other paseo-bm
1211
+ * agents of its workspace (delta 20260916-chat-cards §4.3). Read-only.
1212
+ */
1213
+ export const chatPeersRpc = defineRpc({
1214
+ name: "chat.peers",
1215
+ input: z.object({ agentId: agentIdSchema }),
1216
+ output: z.object({
1217
+ owner: chatPeerSchema.nullable(),
1218
+ peers: z.array(chatPeerSchema),
1219
+ /** The owner's workspace; null for an agent that is not paseo-bm's. */
1220
+ workspaceId: z.string().nullable(),
1221
+ }),
1222
+ });
1223
+
1224
+
1225
+ /** A Worker and the moment it did something to a bead. */
1226
+ export const beadWorkMarkSchema = z.object({
1227
+ agentId: z.string().min(1),
1228
+ at: z.string(),
1229
+ /** The agent's title in Paseo; null when Paseo no longer lists the agent. */
1230
+ title: z.string().nullable(),
1231
+ /** The agent's status now (`running`, `idle`, …); null when it is gone. */
1232
+ status: z.string().nullable(),
1233
+ });
1234
+
1235
+ /**
1236
+ * Who is working on an `in_progress` bead, from the trace store. `started` is
1237
+ * the Worker command that moved the bead to `in_progress`; `last` is the latest
1238
+ * Worker command or report that named the bead. Either can be unknown.
1239
+ */
1240
+ export const beadWorkSchema = z.object({
1241
+ started: beadWorkMarkSchema.nullable(),
1242
+ last: beadWorkMarkSchema.nullable(),
1243
+ });
1244
+
1245
+ export const beadRowSchema = z.object({
1246
+ id: z.string().min(1),
1247
+ title: z.string().nullable(),
1248
+ status: z.string(),
1249
+ issueType: z.string(),
1250
+ /** 0 (highest) to 4; null when the store has none. */
1251
+ priority: z.number().int().nullable(),
1252
+ labels: z.array(z.string()),
1253
+ createdAt: z.string().nullable(),
1254
+ updatedAt: z.string().nullable(),
1255
+ closedAt: z.string().nullable(),
1256
+ /** Derived like `br ready`: open, every blocker closed, not an epic. */
1257
+ ready: z.boolean(),
1258
+ parentId: z.string().nullable(),
1259
+ /** Only for `in_progress` beads; null when nothing was recorded. */
1260
+ work: beadWorkSchema.nullable(),
1261
+ });
1262
+
1263
+ export const beadDetailSchema = beadRowSchema.extend({
1264
+ description: z.string().nullable(),
1265
+ closeReason: z.string().nullable(),
1266
+ blockedBy: z.array(z.string()),
1267
+ children: z.array(z.string()),
1268
+ });
1269
+
1270
+ /** `beads.list` — every bead of a workspace, read from `.beads/issues.jsonl`. */
1271
+ export const beadsListRpc = defineRpc({
1272
+ name: "beads.list",
1273
+ input: z.object({ workspaceId: workspaceIdSchema }),
1274
+ output: z.object({ beads: z.array(beadRowSchema), stats: beadStatsSchema }),
1275
+ });
1276
+
1277
+ /** `beads.get` — one bead in full; an unknown id fails `E_BEAD_NOT_FOUND`. */
1278
+ export const beadsGetRpc = defineRpc({
1279
+ name: "beads.get",
1280
+ input: z.object({ workspaceId: workspaceIdSchema, id: z.string().min(1) }),
1281
+ output: z.object({ bead: beadDetailSchema }),
1282
+ });
1283
+
1284
+ export const beadActionSchema = z.enum(["implement", "delete", "close"]);
1285
+
1286
+ /**
1287
+ * `beads.action` — hands a bead to the workspace's Manager, which delegates it
1288
+ * to a Worker. The plugin itself never writes the bead store.
1289
+ */
1290
+ export const beadsActionRpc = defineRpc({
1291
+ name: "beads.action",
1292
+ input: z.object({ workspaceId: workspaceIdSchema, id: z.string().min(1), action: beadActionSchema }),
1293
+ output: z.object({ managerId: z.string(), created: z.boolean() }),
1294
+ });
1295
+
1296
+ /**
1297
+ * `traces.workspaces` — every workspace that has trace history, with what
1298
+ * Paseo says about it now. This is how a closed workspace's history stays
1299
+ * reachable: the launcher only lists workspaces Paseo still has.
1300
+ */
1301
+ export const tracesWorkspacesRpc = defineRpc({
1302
+ name: "traces.workspaces",
1303
+ input: z.object({}),
1304
+ output: z.object({
1305
+ workspaces: z.array(
1306
+ z.object({
1307
+ workspaceId: workspaceIdSchema,
1308
+ state: workspaceStateSchema,
1309
+ lastKnownName: z.string().nullable(),
1310
+ lastKnownDirectory: z.string().nullable(),
1311
+ lastSeenAt: z.string().nullable(),
1312
+ bytes: z.number().int().nonnegative(),
1313
+ }),
1314
+ ),
1315
+ }),
1316
+ });
1317
+
1318
+ export type BeadRow = z.infer<typeof beadRowSchema>;
1319
+ /** Input shape: `labelled` may be absent (meaning labelled), as from a server older than delta 20260918g. */
1320
+ export type ChatPeer = z.input<typeof chatPeerSchema>;
1321
+ export type BeadWork = z.infer<typeof beadWorkSchema>;
1322
+ export type BeadDetail = z.infer<typeof beadDetailSchema>;
1323
+ export type BeadAction = z.infer<typeof beadActionSchema>;
1324
+
1325
+ export type Confidence = z.infer<typeof confidenceSchema>;
1326
+ export type Evidence = z.infer<typeof evidenceSchema>;
1327
+ export type Usage = z.infer<typeof usageSchema>;
1328
+ export type TraceState = z.infer<typeof traceStateSchema>;
1329
+ export type Tier = z.infer<typeof tierSchema>;
1330
+ export type WorkspaceState = z.infer<typeof workspaceStateSchema>;
1331
+ export type GuardrailReport = z.infer<typeof guardrailReportSchema>;
1332
+ export type TraceSummary = z.infer<typeof traceSummarySchema>;
1333
+ export type TraceDetail = z.infer<typeof traceDetailSchema>;
1334
+ export type TraceMessage = z.infer<typeof traceMessageSchema>;
1335
+ export type AgentTiming = z.infer<typeof agentTimingSchema>;
1336
+ export type TurnTiming = z.infer<typeof turnTimingSchema>;
1337
+ export type ParsedReport = z.infer<typeof parsedReportSchema>;
1338
+ export type ParsedReview = z.infer<typeof parsedReviewSchema>;
1339
+ export type ReportPhase = z.infer<typeof reportPhaseSchema>;
1340
+ export type WorkflowStep = z.infer<typeof workflowStepSchema>;
1341
+ export type WorkflowStepResult = z.infer<typeof workflowStepResultSchema>;
1342
+ export type TraceBead = z.infer<typeof traceBeadSchema>;
1343
+ export type SubAgentTrace = z.infer<typeof subAgentTraceSchema>;
1344
+ export type BeadStats = z.infer<typeof beadStatsSchema>;
1345
+ export type StoreSize = z.infer<typeof storeSizeSchema>;
1346
+ export type TraceRecord = z.infer<typeof traceRecordSchema>;
1347
+ export type TraceRuntime = z.infer<typeof traceRuntimeSchema>;
1348
+ export type RuntimeRow = z.infer<typeof runtimeRowSchema>;
1349
+ export type TraceStoreMeta = z.infer<typeof traceStoreMetaSchema>;
1350
+ export type TraceWorkspaceMeta = z.infer<typeof traceWorkspaceMetaSchema>;
1351
+ export type TraceDeleteScope = z.infer<typeof traceDeleteScopeSchema>;
1352
+
1353
+ /** `beads.lookup` — the beads among `ids` that the workspace's bead store has. Read-only. */
1354
+ export const beadsLookupRpc = defineRpc({
1355
+ name: "beads.lookup",
1356
+ input: z.object({ workspaceId: workspaceIdSchema, ids: z.array(z.string().min(1)).max(100) }),
1357
+ output: z.object({ beads: z.array(beadRowSchema) }),
1358
+ });
1359
+
1360
+ /**
1361
+ * `chat.beads` — beads named in an agent's recent chat (messages and shell
1362
+ * commands), newest mention first. Read-only.
1363
+ */
1364
+ export const chatBeadsRpc = defineRpc({
1365
+ name: "chat.beads",
1366
+ input: z.object({ workspaceId: workspaceIdSchema, agentId: agentIdSchema }),
1367
+ output: z.object({
1368
+ beads: z.array(z.object({ bead: beadRowSchema, mentions: z.number().int(), lastMentionedAt: z.string().nullable() })),
1369
+ scannedItems: z.number().int(),
1370
+ }),
1371
+ });