agent-tempo 1.3.1 → 1.4.1

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 (199) hide show
  1. package/CLAUDE.md +39 -5
  2. package/README.md +6 -2
  3. package/dashboard/dist/assets/{index-D6Xyje_n.js → index-jmYe6rmS.js} +2 -2
  4. package/dashboard/dist/assets/index-jmYe6rmS.js.map +1 -0
  5. package/dashboard/dist/index.html +1 -1
  6. package/dashboard/package.json +1 -1
  7. package/dist/activities/outbox.d.ts +30 -1
  8. package/dist/activities/outbox.js +96 -3
  9. package/dist/adapters/base.js +5 -0
  10. package/dist/adapters/index.d.ts +1 -1
  11. package/dist/adapters/index.js +7 -0
  12. package/dist/adapters/pi/adapter.d.ts +2 -0
  13. package/dist/adapters/pi/adapter.js +43 -0
  14. package/dist/adapters/pi/index.d.ts +16 -0
  15. package/dist/adapters/pi/index.js +10 -0
  16. package/dist/client/core.js +9 -2
  17. package/dist/client/interface.d.ts +6 -0
  18. package/dist/config.d.ts +79 -0
  19. package/dist/config.js +74 -0
  20. package/dist/daemon.js +32 -1
  21. package/dist/http/aggregate.d.ts +22 -1
  22. package/dist/http/aggregate.js +41 -0
  23. package/dist/http/auth.d.ts +94 -8
  24. package/dist/http/auth.js +93 -9
  25. package/dist/http/body.d.ts +4 -1
  26. package/dist/http/body.js +6 -3
  27. package/dist/http/event-bus.js +1 -0
  28. package/dist/http/event-types.d.ts +34 -2
  29. package/dist/http/event-types.js +1 -0
  30. package/dist/http/gate-audit.d.ts +12 -0
  31. package/dist/http/gate-audit.js +95 -0
  32. package/dist/http/gate-registry.d.ts +167 -0
  33. package/dist/http/gate-registry.js +163 -0
  34. package/dist/http/gate-routes.d.ts +48 -0
  35. package/dist/http/gate-routes.js +102 -0
  36. package/dist/http/ingest-registry.d.ts +30 -0
  37. package/dist/http/ingest-registry.js +108 -0
  38. package/dist/http/inner-loop-routes.d.ts +66 -0
  39. package/dist/http/inner-loop-routes.js +182 -0
  40. package/dist/http/inner-loop.d.ts +92 -0
  41. package/dist/http/inner-loop.js +155 -0
  42. package/dist/http/server.d.ts +38 -3
  43. package/dist/http/server.js +211 -6
  44. package/dist/http/snapshot.d.ts +6 -0
  45. package/dist/http/snapshot.js +6 -0
  46. package/dist/pi/cue-pump.d.ts +61 -0
  47. package/dist/pi/cue-pump.js +95 -0
  48. package/dist/pi/extension.d.ts +45 -0
  49. package/dist/pi/extension.js +407 -0
  50. package/dist/pi/gate-client.d.ts +54 -0
  51. package/dist/pi/gate-client.js +136 -0
  52. package/dist/pi/headless.d.ts +85 -0
  53. package/dist/pi/headless.js +250 -0
  54. package/dist/pi/index.d.ts +28 -0
  55. package/dist/pi/index.js +43 -0
  56. package/dist/pi/inner-loop-client.d.ts +67 -0
  57. package/dist/pi/inner-loop-client.js +164 -0
  58. package/dist/pi/inner-loop-publisher.d.ts +187 -0
  59. package/dist/pi/inner-loop-publisher.js +236 -0
  60. package/dist/pi/lazy-proxy.d.ts +37 -0
  61. package/dist/pi/lazy-proxy.js +55 -0
  62. package/dist/pi/mission-control/actions.d.ts +48 -0
  63. package/dist/pi/mission-control/actions.js +98 -0
  64. package/dist/pi/mission-control/board.d.ts +88 -0
  65. package/dist/pi/mission-control/board.js +141 -0
  66. package/dist/pi/mission-control/extension.d.ts +51 -0
  67. package/dist/pi/mission-control/extension.js +330 -0
  68. package/dist/pi/mission-control/index.d.ts +15 -0
  69. package/dist/pi/mission-control/index.js +32 -0
  70. package/dist/pi/mission-control/inner-tail.d.ts +48 -0
  71. package/dist/pi/mission-control/inner-tail.js +76 -0
  72. package/dist/pi/mission-control/pi-ui.d.ts +43 -0
  73. package/dist/pi/mission-control/pi-ui.js +10 -0
  74. package/dist/pi/mission-control/render.d.ts +6 -0
  75. package/dist/pi/mission-control/render.js +98 -0
  76. package/dist/pi/phase-driver.d.ts +74 -0
  77. package/dist/pi/phase-driver.js +122 -0
  78. package/dist/pi/pi-types.d.ts +222 -0
  79. package/dist/pi/pi-types.js +21 -0
  80. package/dist/pi/probe.d.ts +99 -0
  81. package/dist/pi/probe.js +179 -0
  82. package/dist/pi/render-tools.d.ts +17 -0
  83. package/dist/pi/render-tools.js +56 -0
  84. package/dist/pi/reset-pump.d.ts +47 -0
  85. package/dist/pi/reset-pump.js +85 -0
  86. package/dist/pi/session-seed.d.ts +74 -0
  87. package/dist/pi/session-seed.js +103 -0
  88. package/dist/pi/tool-capability.d.ts +60 -0
  89. package/dist/pi/tool-capability.js +156 -0
  90. package/dist/pi/workflow-client.d.ts +158 -0
  91. package/dist/pi/workflow-client.js +289 -0
  92. package/dist/pi/zod-to-typebox.d.ts +74 -0
  93. package/dist/pi/zod-to-typebox.js +191 -0
  94. package/dist/server-tools.d.ts +2 -0
  95. package/dist/server-tools.js +50 -46
  96. package/dist/spawn.d.ts +55 -0
  97. package/dist/spawn.js +72 -0
  98. package/dist/tools/agent-types.d.ts +2 -2
  99. package/dist/tools/agent-types.js +22 -17
  100. package/dist/tools/attachment-info.d.ts +2 -2
  101. package/dist/tools/attachment-info.js +38 -33
  102. package/dist/tools/broadcast.d.ts +2 -2
  103. package/dist/tools/broadcast.js +69 -64
  104. package/dist/tools/cancel-stage.d.ts +2 -2
  105. package/dist/tools/cancel-stage.js +20 -15
  106. package/dist/tools/clear-state.d.ts +2 -2
  107. package/dist/tools/clear-state.js +25 -20
  108. package/dist/tools/coat-check-evict.d.ts +2 -2
  109. package/dist/tools/coat-check-evict.js +29 -24
  110. package/dist/tools/coat-check-get.d.ts +2 -2
  111. package/dist/tools/coat-check-get.js +38 -33
  112. package/dist/tools/coat-check-list.d.ts +2 -2
  113. package/dist/tools/coat-check-list.js +48 -43
  114. package/dist/tools/coat-check-put.d.ts +2 -2
  115. package/dist/tools/coat-check-put.js +38 -33
  116. package/dist/tools/cue.d.ts +2 -2
  117. package/dist/tools/cue.js +57 -52
  118. package/dist/tools/descriptor.d.ts +72 -0
  119. package/dist/tools/descriptor.js +39 -0
  120. package/dist/tools/destroy.d.ts +2 -2
  121. package/dist/tools/destroy.js +153 -148
  122. package/dist/tools/ensemble.d.ts +2 -2
  123. package/dist/tools/ensemble.js +71 -66
  124. package/dist/tools/evaluate-gate.d.ts +2 -2
  125. package/dist/tools/evaluate-gate.js +33 -27
  126. package/dist/tools/fetch-state.d.ts +2 -2
  127. package/dist/tools/fetch-state.js +42 -37
  128. package/dist/tools/gates.d.ts +2 -2
  129. package/dist/tools/gates.js +39 -34
  130. package/dist/tools/hosts.d.ts +2 -2
  131. package/dist/tools/hosts.js +25 -20
  132. package/dist/tools/listen.d.ts +2 -2
  133. package/dist/tools/listen.js +23 -18
  134. package/dist/tools/load-lineup.d.ts +2 -2
  135. package/dist/tools/load-lineup.js +324 -319
  136. package/dist/tools/migrate.d.ts +2 -2
  137. package/dist/tools/migrate.js +45 -40
  138. package/dist/tools/pause.d.ts +2 -2
  139. package/dist/tools/pause.js +34 -29
  140. package/dist/tools/play.d.ts +2 -2
  141. package/dist/tools/play.js +53 -48
  142. package/dist/tools/quality-gate.d.ts +2 -2
  143. package/dist/tools/quality-gate.js +26 -21
  144. package/dist/tools/recall.d.ts +2 -2
  145. package/dist/tools/recall.js +32 -27
  146. package/dist/tools/recruit.d.ts +2 -2
  147. package/dist/tools/recruit.js +340 -256
  148. package/dist/tools/release.d.ts +2 -2
  149. package/dist/tools/release.js +85 -80
  150. package/dist/tools/report.d.ts +2 -2
  151. package/dist/tools/report.js +28 -23
  152. package/dist/tools/reset.d.ts +3 -0
  153. package/dist/tools/reset.js +51 -0
  154. package/dist/tools/restart.d.ts +2 -2
  155. package/dist/tools/restart.js +51 -46
  156. package/dist/tools/restore.d.ts +2 -2
  157. package/dist/tools/restore.js +76 -71
  158. package/dist/tools/save-lineup.d.ts +2 -2
  159. package/dist/tools/save-lineup.js +32 -27
  160. package/dist/tools/save-state.d.ts +2 -2
  161. package/dist/tools/save-state.js +31 -26
  162. package/dist/tools/schedule.d.ts +2 -2
  163. package/dist/tools/schedule.js +133 -128
  164. package/dist/tools/schedules.d.ts +2 -2
  165. package/dist/tools/schedules.js +41 -36
  166. package/dist/tools/set-ensemble-description.d.ts +2 -2
  167. package/dist/tools/set-ensemble-description.js +26 -21
  168. package/dist/tools/set-name.d.ts +2 -2
  169. package/dist/tools/set-name.js +38 -33
  170. package/dist/tools/set-part.d.ts +2 -2
  171. package/dist/tools/set-part.js +20 -15
  172. package/dist/tools/shutdown.d.ts +2 -2
  173. package/dist/tools/shutdown.js +39 -34
  174. package/dist/tools/stage.d.ts +2 -2
  175. package/dist/tools/stage.js +28 -23
  176. package/dist/tools/stages.d.ts +2 -2
  177. package/dist/tools/stages.js +36 -31
  178. package/dist/tools/unschedule.d.ts +2 -2
  179. package/dist/tools/unschedule.js +30 -25
  180. package/dist/tools/who-am-i.d.ts +2 -2
  181. package/dist/tools/who-am-i.js +36 -31
  182. package/dist/tools/worktree.d.ts +2 -2
  183. package/dist/tools/worktree.js +134 -129
  184. package/dist/tui/index.js +6 -6
  185. package/dist/types.d.ts +47 -2
  186. package/dist/types.js +1 -1
  187. package/dist/utils/default-part.js +1 -0
  188. package/dist/utils/sdk-probe.d.ts +23 -0
  189. package/dist/utils/sdk-probe.js +46 -7
  190. package/dist/worker.d.ts +3 -1
  191. package/dist/worker.js +6 -2
  192. package/dist/workflows/session.js +70 -2
  193. package/dist/workflows/signals.d.ts +32 -2
  194. package/dist/workflows/signals.js +25 -2
  195. package/package.json +4 -1
  196. package/workflow-bundle.js +97 -6
  197. package/dashboard/dist/assets/index-D6Xyje_n.js.map +0 -1
  198. package/dist/tools/helpers.d.ts +0 -21
  199. package/dist/tools/helpers.js +0 -25
@@ -0,0 +1,289 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PiWorkflowClient = exports.PI_LEASE_MS = exports.PI_HEARTBEAT_MS = void 0;
4
+ /**
5
+ * Thin CLIENT-SIDE Temporal wrapper for the Pi extension (D4 = (a)).
6
+ *
7
+ * The durable Temporal `Worker` stays in the daemon. This holds only a
8
+ * `WorkflowClient` that signals/queries/updates the session workflow — it never
9
+ * runs workflow code, so NOTHING here (or anywhere in src/pi/) is imported into
10
+ * the V8 workflow sandbox. The determinism boundary is preserved.
11
+ *
12
+ * Reuses the existing wire surface verbatim:
13
+ * - `claimAttachment` / `heartbeat` — lease lifecycle (drives `attached`)
14
+ * - `processingStart` / `processingEnd` — drives `processing` / `awaiting`
15
+ * - `requestDetach` + `adapterExited` — drives `draining` → `detached`
16
+ * - `submitOutbox` — report routing (outbox compliance)
17
+ * - `pendingMessages` + `markDelivered` — cue intake + ack
18
+ */
19
+ const client_1 = require("@temporalio/client");
20
+ const connection_1 = require("../connection");
21
+ const config_1 = require("../config");
22
+ const signals_1 = require("../workflows/signals");
23
+ /** Adapter identity advertised on claim. Pi interactive players are `interactive`-class. */
24
+ const PI_ADAPTER_ID = 'pi';
25
+ /**
26
+ * MD-A liveness timings (Phase 2, maintainer-approved): 30s heartbeat / 90s
27
+ * lease, holding the **lease = 3×heartbeat** invariant — a single (or even
28
+ * second) missed beat never expires the lease, but a dead process is reaped
29
+ * within ~one lease window. Parity with the other SDK adapters' `heartbeatMs`.
30
+ * Overridable per-session via `PiWorkflowClientOptions.{leaseMs,heartbeatMs}`.
31
+ * Exported so a guard test can assert the invariant doesn't silently drift.
32
+ */
33
+ exports.PI_HEARTBEAT_MS = 30_000;
34
+ exports.PI_LEASE_MS = 90_000;
35
+ const log = (...args) => {
36
+ // eslint-disable-next-line no-console
37
+ console.error('[agent-tempo:pi]', ...args);
38
+ };
39
+ /**
40
+ * One instance per attached Pi session — holds the session `WorkflowHandle`,
41
+ * lease token, and heartbeat timer for one fixed-workflowId player. Tool
42
+ * handlers reach this player's handle via the D11 lazy proxy (`get handle()`).
43
+ */
44
+ class PiWorkflowClient {
45
+ client;
46
+ config;
47
+ metadata;
48
+ leaseMs;
49
+ heartbeatMs;
50
+ expectedAttachmentId;
51
+ coarseProvider;
52
+ wfHandle = null;
53
+ token = null;
54
+ heartbeatTimer = null;
55
+ constructor(opts) {
56
+ this.client = opts.client;
57
+ this.config = opts.config;
58
+ this.metadata = opts.metadata;
59
+ this.leaseMs = opts.leaseMs ?? exports.PI_LEASE_MS;
60
+ this.heartbeatMs = opts.heartbeatMs ?? exports.PI_HEARTBEAT_MS;
61
+ this.expectedAttachmentId = opts.expectedAttachmentId;
62
+ this.coarseProvider = opts.coarseProvider;
63
+ }
64
+ /**
65
+ * 3c — wire the coarse-activity provider after construction (the inner-loop
66
+ * publisher is created alongside, then handed in). Each heartbeat samples it.
67
+ */
68
+ setCoarseProvider(provider) {
69
+ this.coarseProvider = provider;
70
+ }
71
+ /** Build a client from config (connection-pooled; safe to share — see D12a). */
72
+ static async connect(config = (0, config_1.getConfig)()) {
73
+ const connection = await (0, connection_1.createTemporalConnection)(config);
74
+ return new client_1.Client({ connection, namespace: config.temporalNamespace });
75
+ }
76
+ get attachmentId() {
77
+ return this.token?.attachmentId ?? null;
78
+ }
79
+ /**
80
+ * The live session `WorkflowHandle`, or `null` before {@link ensureSessionWorkflow}.
81
+ * Exposed so the D11 lazy-proxy (src/pi/lazy-proxy.ts) can resolve the player's
82
+ * OWN handle per tool call — tool handlers route through it (e.g. `submitOutbox`)
83
+ * and never `.signal()` a peer workflow directly.
84
+ */
85
+ get handle() {
86
+ return this.wfHandle;
87
+ }
88
+ get workflowId() {
89
+ return (0, config_1.sessionWorkflowId)(this.metadata.ensemble, this.metadata.playerId);
90
+ }
91
+ /**
92
+ * Ensure the session workflow exists and grab a handle. For a human-launched
93
+ * `pi`, the workflow may not exist yet; `USE_EXISTING` makes this idempotent
94
+ * when `agent-tempo up`/recruit already started it.
95
+ */
96
+ async ensureSessionWorkflow() {
97
+ if (this.wfHandle)
98
+ return this.wfHandle;
99
+ this.wfHandle = await this.client.workflow.start('agentSessionWorkflow', {
100
+ workflowId: this.workflowId,
101
+ taskQueue: this.config.taskQueue,
102
+ workflowIdConflictPolicy: 'USE_EXISTING',
103
+ args: [{ metadata: this.metadata, phase: 'booting' }],
104
+ });
105
+ return this.wfHandle;
106
+ }
107
+ requireHandle() {
108
+ if (!this.wfHandle) {
109
+ throw new Error('PiWorkflowClient: call ensureSessionWorkflow() before lifecycle ops');
110
+ }
111
+ return this.wfHandle;
112
+ }
113
+ /**
114
+ * Claim (or, with a handoff token, ADOPT) the attachment: phase
115
+ * `booting → attached`. Fresh claim when `expectedAttachmentId` is absent;
116
+ * renewal/adoption branch when the restart tool handed one in. Stores the token.
117
+ */
118
+ async claim() {
119
+ const handle = this.requireHandle();
120
+ this.token = await handle.executeUpdate(signals_1.claimAttachmentUpdate, {
121
+ args: [
122
+ {
123
+ host: this.metadata.hostname,
124
+ adapterId: PI_ADAPTER_ID,
125
+ adapterClass: 'interactive',
126
+ leaseMs: this.leaseMs,
127
+ ...(this.expectedAttachmentId ? { expectedAttachmentId: this.expectedAttachmentId } : {}),
128
+ },
129
+ ],
130
+ });
131
+ log(`${this.expectedAttachmentId ? 'renewed' : 'claimed'} attachment ` +
132
+ `${this.token.attachmentId} (lease ${this.leaseMs}ms)`);
133
+ return this.token;
134
+ }
135
+ /**
136
+ * Liveness heartbeat — renews the lease. This is the ENTIRE reason an abrupt
137
+ * Pi death is detectable: stop heart-beating and `expiresAt` lapses (see
138
+ * src/pi/README.md "Abrupt-death finding" / MD-A).
139
+ */
140
+ async heartbeat() {
141
+ if (!this.token)
142
+ return;
143
+ const handle = this.requireHandle();
144
+ // 3c Tier-1 — piggyback the latest coarse sample (currentTool + context).
145
+ // Sampled per-beat; a throwing/absent provider degrades to a plain heartbeat.
146
+ let coarse;
147
+ try {
148
+ coarse = this.coarseProvider?.();
149
+ }
150
+ catch {
151
+ coarse = undefined;
152
+ }
153
+ await handle.signal(signals_1.heartbeatSignal, {
154
+ attachmentId: this.token.attachmentId,
155
+ at: new Date().toISOString(),
156
+ ...(coarse ? coarse : {}),
157
+ });
158
+ }
159
+ /** Start the heartbeat loop. The timer is `unref`'d so it never holds the process open. */
160
+ startHeartbeat() {
161
+ if (this.heartbeatTimer)
162
+ return;
163
+ this.heartbeatTimer = setInterval(() => {
164
+ this.heartbeat().catch((err) => log('heartbeat failed:', err));
165
+ }, this.heartbeatMs);
166
+ if (typeof this.heartbeatTimer.unref === 'function')
167
+ this.heartbeatTimer.unref();
168
+ }
169
+ stopHeartbeat() {
170
+ if (this.heartbeatTimer) {
171
+ clearInterval(this.heartbeatTimer);
172
+ this.heartbeatTimer = null;
173
+ }
174
+ }
175
+ /** agent_start → phase `processing`. */
176
+ async processingStart(messageId) {
177
+ const handle = this.requireHandle();
178
+ await handle.executeUpdate(signals_1.processingStartUpdate, {
179
+ args: [{ messageId, expectedAttachmentId: this.token?.attachmentId }],
180
+ });
181
+ }
182
+ /** agent_end → phase `awaiting`. */
183
+ async processingEnd(messageId) {
184
+ const handle = this.requireHandle();
185
+ await handle.executeUpdate(signals_1.processingEndUpdate, {
186
+ args: [{ messageId, expectedAttachmentId: this.token?.attachmentId }],
187
+ });
188
+ }
189
+ /**
190
+ * Graceful self-initiated detach: stopHeartbeat → adapterExited (→ detached).
191
+ *
192
+ * `adapterExited` ALONE collapses any live phase (attached/processing/awaiting/
193
+ * draining) straight to `detached` — see the workflow handler at
194
+ * `session.ts` adapterExitedSignal (returns early only on detached/gone). No
195
+ * `requestDetach` is needed for a self-exit; that signal is the EXTERNAL ask to
196
+ * drain a running adapter someone else controls. This matches the proven
197
+ * `BaseAttachment.stopV2Lifecycle(graceful)` path. Idempotent-safe to call on
198
+ * `session_shutdown` and from the headless exit sequence.
199
+ */
200
+ async detach(reason = 'agent-exited') {
201
+ if (!this.wfHandle || !this.token)
202
+ return;
203
+ this.stopHeartbeat();
204
+ // Signal-DELIVERY (not a processing-ack) is sufficient by design, ruled won't-fix:
205
+ // the session workflow runs in the DAEMON worker, not this Pi process, so once
206
+ // signal() resolves the exit is durably in history and the worker transitions to
207
+ // `detached` regardless of this process disposing/exiting. stopHeartbeat() above is
208
+ // the independent backstop — a missed transition still reaps via lease expiry. No
209
+ // caller reads phase synchronously at detach()-return, so an executeUpdate ack would
210
+ // only add shutdown latency + drag determinism-sensitive workflow code into scope.
211
+ // If a future synchronous re-claim/migrate ever needs an OBSERVED `detached`, poll the
212
+ // existing attachmentInfoQuery client-side — never add an adapterExitedUpdate.
213
+ await this.wfHandle.signal(signals_1.adapterExitedSignal, {
214
+ attachmentId: this.token.attachmentId,
215
+ reason,
216
+ });
217
+ log(`detached (${reason})`);
218
+ }
219
+ /**
220
+ * Stash the currently-active Pi SessionManager session id onto durable
221
+ * workflow metadata (`metadata.sessionId`). This is the MUTABLE resume pointer
222
+ * — SEPARATE from the fixed workflowId (identity). A later restart of this
223
+ * player reads it to resume the same conversation via Pi `continueSession`
224
+ * (reuses the #334 sessionId resume field). No-op when the id is absent.
225
+ */
226
+ async updateSessionId(sessionId) {
227
+ if (!sessionId)
228
+ return;
229
+ const handle = this.requireHandle();
230
+ await handle.signal(signals_1.updateMetadataSignal, { sessionId });
231
+ }
232
+ // ── Outbox ──
233
+ /** Route an outbox entry through the workflow outbox (player's own handle). */
234
+ async submitOutbox(entry) {
235
+ const handle = this.requireHandle();
236
+ return handle.executeUpdate(signals_1.submitOutboxUpdate, { args: [entry] });
237
+ }
238
+ // ── Cue intake ──
239
+ /** Pull undelivered messages (cues) queued on the workflow. */
240
+ async fetchPending() {
241
+ const handle = this.requireHandle();
242
+ return handle.query(signals_1.pendingMessagesQuery);
243
+ }
244
+ /** Acknowledge delivered cue ids so the workflow stops re-serving them. */
245
+ async ackDelivered(messageIds) {
246
+ if (messageIds.length === 0)
247
+ return;
248
+ const handle = this.requireHandle();
249
+ await handle.signal(signals_1.markDeliveredSignal, messageIds);
250
+ }
251
+ // ── Reset intake (3d D14) ──
252
+ /** Poll the workflow's single-slot pending reset (null = none). */
253
+ async fetchPendingReset() {
254
+ const handle = this.requireHandle();
255
+ return handle.query(signals_1.pendingResetQuery);
256
+ }
257
+ /**
258
+ * Ack a performed reset by id — the workflow clears the slot ONLY if the id
259
+ * still matches (race-safe: a newer reset that landed mid-wipe is preserved).
260
+ */
261
+ async ackReset(resetId) {
262
+ const handle = this.requireHandle();
263
+ await handle.signal(signals_1.ackResetSignal, resetId);
264
+ }
265
+ /**
266
+ * Dispatch a {@link WorkflowAction} produced by {@link PhaseDriver}. Centralizes
267
+ * the action→wire-call mapping so the extension wiring stays declarative.
268
+ */
269
+ async performAction(action) {
270
+ switch (action.kind) {
271
+ case 'claim':
272
+ await this.claim();
273
+ this.startHeartbeat();
274
+ return;
275
+ case 'processingStart':
276
+ await this.processingStart(action.messageId);
277
+ return;
278
+ case 'processingEnd':
279
+ await this.processingEnd(action.messageId);
280
+ return;
281
+ case 'detach':
282
+ await this.detach();
283
+ return;
284
+ case 'none':
285
+ return;
286
+ }
287
+ }
288
+ }
289
+ exports.PiWorkflowClient = PiWorkflowClient;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Narrow zod → TypeBox converter (D1 / MD-B, Phase 1).
3
+ *
4
+ * agent-tempo tools declare their parameters as a `z.ZodRawShape`
5
+ * (`Record<string, z.ZodTypeAny>`) — the exact object handed to
6
+ * `McpServer.tool(...)` today (see `src/tools/helpers.ts` `defineTool`). The Pi
7
+ * front-end (`pi.registerTool({ parameters })`) wants a TypeBox schema instead.
8
+ * This module derives the TypeBox object schema from the zod shape so **zod
9
+ * stays the single source of truth** and the two surfaces cannot drift.
10
+ *
11
+ * DESIGN: this is deliberately NOT a general-purpose zod→TypeBox shim. It
12
+ * handles ONLY the feature subset the ~30 tools actually use (audited
13
+ * 2026-06-03) and **throws** — naming the field + feature — on anything else.
14
+ * Failing loud is the whole point (D1): a faithless silent conversion would
15
+ * ship a wrong tool schema to the model. The CI parity test (Track A / S5b)
16
+ * asserts the converter raised on nothing across the real tool set, so any new
17
+ * unaudited zod construct trips CI instead of producing a bad schema.
18
+ *
19
+ * Supported subset:
20
+ * - z.string() (+ .min/.max → minLength/maxLength, .regex → pattern)
21
+ * - z.number() (+ .int() → integer, .min/.max → minimum/maximum [inclusive])
22
+ * - z.boolean()
23
+ * - z.enum([...]) → Type.Union of Type.Literal (anyOf of const)
24
+ * - z.array(inner) (+ .min/.max → minItems/maxItems)
25
+ * - z.object({...}) (nested; appears as an array element today)
26
+ * - z.union([...]) SCALAR members only (string/number/boolean/enum/
27
+ * literal) — object/array/nested-union members throw
28
+ * - .optional() → Type.Optional (drops the field from `required`)
29
+ * - .describe(s) → TypeBox `{ description: s }` (Pi surfaces it)
30
+ *
31
+ * Everything else (.default, .nullable, .refine/.transform, .coerce, .email,
32
+ * .gt/.lt, z.record/any/unknown/null/date/tuple/intersection/nativeEnum, …)
33
+ * raises {@link UnsupportedZodFeatureError}.
34
+ *
35
+ * `.min`/`.max` are CONTEXT-SENSITIVE — the same modifier maps to
36
+ * minLength/maxLength (string), minimum/maximum (number), or minItems/maxItems
37
+ * (array) depending on the base zod type. The mapping is keyed on the base
38
+ * type, never on the modifier alone.
39
+ *
40
+ * TypeBox note (D12, confirmed): a TypeBox `Type.*` value IS a plain JSON-Schema
41
+ * object at runtime (`{ type, required, properties, … }`), so the output drops
42
+ * straight into Pi's `parameters` field with no further transform.
43
+ */
44
+ import { z } from 'zod';
45
+ import { type TObject } from 'typebox';
46
+ /** Where in the schema we are, for actionable error messages. */
47
+ interface ConvertContext {
48
+ /** Tool name, when the caller supplies it (richer errors). */
49
+ readonly tool?: string;
50
+ /** Dotted field path within the shape, e.g. `evaluations.status`. */
51
+ readonly path: string;
52
+ }
53
+ /**
54
+ * Thrown when a tool param schema uses a zod construct outside the audited
55
+ * subset. Carries the field path + offending feature so CI / callers can point
56
+ * at the exact source. A named class lets the parity test assert "raised on
57
+ * nothing" precisely.
58
+ */
59
+ export declare class UnsupportedZodFeatureError extends Error {
60
+ readonly context: ConvertContext;
61
+ readonly feature: string;
62
+ constructor(context: ConvertContext, feature: string);
63
+ }
64
+ /**
65
+ * Convert a tool's zod param shape into a TypeBox object schema suitable for
66
+ * `pi.registerTool({ parameters })`. This is the public entry point consumed by
67
+ * the Pi front-end (`src/pi/render-tools.ts`).
68
+ *
69
+ * @param shape The tool's `z.ZodRawShape` (the `paramsSchema` from `defineTool`).
70
+ * @param toolName Optional tool name, woven into error messages.
71
+ * @throws {UnsupportedZodFeatureError} on any zod construct outside the subset.
72
+ */
73
+ export declare function zodShapeToTypeBox(shape: z.ZodRawShape, toolName?: string): TObject;
74
+ export {};
@@ -0,0 +1,191 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UnsupportedZodFeatureError = void 0;
4
+ exports.zodShapeToTypeBox = zodShapeToTypeBox;
5
+ const typebox_1 = require("typebox");
6
+ /**
7
+ * Thrown when a tool param schema uses a zod construct outside the audited
8
+ * subset. Carries the field path + offending feature so CI / callers can point
9
+ * at the exact source. A named class lets the parity test assert "raised on
10
+ * nothing" precisely.
11
+ */
12
+ class UnsupportedZodFeatureError extends Error {
13
+ context;
14
+ feature;
15
+ constructor(context, feature) {
16
+ const where = `${context.tool ? `${context.tool}.` : ''}${context.path || '<root>'}`;
17
+ super(`zod→TypeBox: unsupported feature "${feature}" at ${where}. ` +
18
+ `Extend src/pi/zod-to-typebox.ts (and a unit test) if this construct is intentional.`);
19
+ this.context = context;
20
+ this.feature = feature;
21
+ this.name = 'UnsupportedZodFeatureError';
22
+ }
23
+ }
24
+ exports.UnsupportedZodFeatureError = UnsupportedZodFeatureError;
25
+ function defOf(zt) {
26
+ return zt._def;
27
+ }
28
+ const SCALAR_TYPE_NAMES = new Set([
29
+ 'ZodString',
30
+ 'ZodNumber',
31
+ 'ZodBoolean',
32
+ 'ZodEnum',
33
+ 'ZodLiteral',
34
+ ]);
35
+ /**
36
+ * Convert a tool's zod param shape into a TypeBox object schema suitable for
37
+ * `pi.registerTool({ parameters })`. This is the public entry point consumed by
38
+ * the Pi front-end (`src/pi/render-tools.ts`).
39
+ *
40
+ * @param shape The tool's `z.ZodRawShape` (the `paramsSchema` from `defineTool`).
41
+ * @param toolName Optional tool name, woven into error messages.
42
+ * @throws {UnsupportedZodFeatureError} on any zod construct outside the subset.
43
+ */
44
+ function zodShapeToTypeBox(shape, toolName) {
45
+ return buildObject(shape, { tool: toolName, path: '' });
46
+ }
47
+ function buildObject(shape, ctx, meta = {}) {
48
+ const props = {};
49
+ for (const [key, zt] of Object.entries(shape)) {
50
+ const fieldCtx = {
51
+ tool: ctx.tool,
52
+ path: ctx.path ? `${ctx.path}.${key}` : key,
53
+ };
54
+ const { inner, isOptional, outerDescription } = stripOptional(zt);
55
+ const schema = convert(inner, fieldCtx, outerDescription);
56
+ props[key] = isOptional ? typebox_1.Type.Optional(schema) : schema;
57
+ }
58
+ return typebox_1.Type.Object(props, meta);
59
+ }
60
+ /**
61
+ * Peel a single `.optional()` wrapper. Captures the description authored on the
62
+ * wrapper (e.g. `z.string().optional().describe('…')` puts it on the optional)
63
+ * so it survives onto the inner schema.
64
+ */
65
+ function stripOptional(zt) {
66
+ const def = defOf(zt);
67
+ if (def?.typeName === 'ZodOptional') {
68
+ return { inner: def.innerType, isOptional: true, outerDescription: def.description };
69
+ }
70
+ return { inner: zt, isOptional: false };
71
+ }
72
+ /** Convert a NON-optional zod type. `descOverride` wins over the node's own description. */
73
+ function convert(zt, ctx, descOverride) {
74
+ const def = defOf(zt);
75
+ const typeName = def?.typeName;
76
+ const description = descOverride ?? def?.description;
77
+ const meta = description !== undefined ? { description } : {};
78
+ switch (typeName) {
79
+ case 'ZodString':
80
+ return typebox_1.Type.String({ ...meta, ...stringConstraints(zt, ctx) });
81
+ case 'ZodNumber': {
82
+ const { isInt, opts } = numberConstraints(zt, ctx);
83
+ const numMeta = { ...meta, ...opts };
84
+ return isInt ? typebox_1.Type.Integer(numMeta) : typebox_1.Type.Number(numMeta);
85
+ }
86
+ case 'ZodBoolean':
87
+ return typebox_1.Type.Boolean(meta);
88
+ case 'ZodEnum': {
89
+ const values = def.values;
90
+ return typebox_1.Type.Union(values.map((v) => typebox_1.Type.Literal(v)), meta);
91
+ }
92
+ case 'ZodLiteral':
93
+ return typebox_1.Type.Literal(def.value, meta);
94
+ case 'ZodArray':
95
+ return typebox_1.Type.Array(arrayElement(def.type, ctx), {
96
+ ...meta,
97
+ ...arrayConstraints(zt, ctx),
98
+ });
99
+ case 'ZodObject':
100
+ return buildObject(def.shape(), ctx, meta);
101
+ case 'ZodUnion':
102
+ return convertUnion(zt, ctx, meta);
103
+ default:
104
+ throw new UnsupportedZodFeatureError(ctx, typeName ?? 'unknown (not a zod type)');
105
+ }
106
+ }
107
+ /** Array elements are converted as non-optional types; an optional element is rejected. */
108
+ function arrayElement(el, ctx) {
109
+ const { inner, isOptional } = stripOptional(el);
110
+ if (isOptional) {
111
+ throw new UnsupportedZodFeatureError({ ...ctx, path: `${ctx.path}[]` }, 'optional array element');
112
+ }
113
+ return convert(inner, { ...ctx, path: `${ctx.path}[]` });
114
+ }
115
+ function stringConstraints(zt, ctx) {
116
+ const out = {};
117
+ for (const check of (defOf(zt).checks ?? [])) {
118
+ switch (check.kind) {
119
+ case 'min':
120
+ out.minLength = check.value;
121
+ break;
122
+ case 'max':
123
+ out.maxLength = check.value;
124
+ break;
125
+ case 'regex':
126
+ out.pattern = check.regex.source;
127
+ break;
128
+ default:
129
+ throw new UnsupportedZodFeatureError(ctx, `z.string().${String(check.kind)}()`);
130
+ }
131
+ }
132
+ return out;
133
+ }
134
+ function numberConstraints(zt, ctx) {
135
+ let isInt = false;
136
+ const opts = {};
137
+ for (const check of (defOf(zt).checks ?? [])) {
138
+ switch (check.kind) {
139
+ case 'int':
140
+ isInt = true;
141
+ break;
142
+ case 'min':
143
+ if (check.inclusive === false) {
144
+ throw new UnsupportedZodFeatureError(ctx, 'z.number() exclusive minimum (.gt)');
145
+ }
146
+ opts.minimum = check.value;
147
+ break;
148
+ case 'max':
149
+ if (check.inclusive === false) {
150
+ throw new UnsupportedZodFeatureError(ctx, 'z.number() exclusive maximum (.lt)');
151
+ }
152
+ opts.maximum = check.value;
153
+ break;
154
+ default:
155
+ throw new UnsupportedZodFeatureError(ctx, `z.number().${String(check.kind)}()`);
156
+ }
157
+ }
158
+ return { isInt, opts };
159
+ }
160
+ function arrayConstraints(zt, ctx) {
161
+ // NOTE: zod stores array length bounds as named `_def` FIELDS
162
+ // (`minLength`/`maxLength`/`exactLength`), NOT as `_def.checks[]` entries like
163
+ // string and number bounds. Don't "unify" this with stringConstraints /
164
+ // numberConstraints — they read a genuinely different zod representation.
165
+ const def = defOf(zt);
166
+ const out = {};
167
+ if (def.exactLength) {
168
+ throw new UnsupportedZodFeatureError(ctx, 'z.array().length() (exact length)');
169
+ }
170
+ if (def.minLength)
171
+ out.minItems = def.minLength.value;
172
+ if (def.maxLength)
173
+ out.maxItems = def.maxLength.value;
174
+ return out;
175
+ }
176
+ function convertUnion(zt, ctx, meta) {
177
+ const options = defOf(zt).options;
178
+ const members = options.map((member, i) => {
179
+ const memberCtx = { ...ctx, path: `${ctx.path}|${i}` };
180
+ const { inner, isOptional } = stripOptional(member);
181
+ if (isOptional) {
182
+ throw new UnsupportedZodFeatureError(memberCtx, 'optional union member');
183
+ }
184
+ const memberTypeName = defOf(inner)?.typeName;
185
+ if (memberTypeName === undefined || !SCALAR_TYPE_NAMES.has(memberTypeName)) {
186
+ throw new UnsupportedZodFeatureError(memberCtx, `non-scalar union member (${memberTypeName ?? 'unknown'}); only string/number/boolean/enum/literal members are supported`);
187
+ }
188
+ return convert(inner, memberCtx);
189
+ });
190
+ return typebox_1.Type.Union(members, meta);
191
+ }
@@ -20,6 +20,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
20
20
  import { Client, WorkflowHandle } from '@temporalio/client';
21
21
  import { Config } from './config';
22
22
  import { AgentType } from './types';
23
+ import { type TempoToolDescriptor } from './tools/descriptor';
23
24
  /**
24
25
  * Identity + state context every tool registration consumes. The two
25
26
  * surfaces (stdio MCP server in `src/server.ts`, in-process MCP server in
@@ -57,6 +58,7 @@ export interface RegisterAllTempoToolsOpts {
57
58
  * `opts.isConductor` — non-conductor players don't see them on either
58
59
  * surface.
59
60
  */
61
+ export declare function buildAllTempoTools(opts: RegisterAllTempoToolsOpts): TempoToolDescriptor[];
60
62
  export declare function registerAllTempoTools(server: McpServer, opts: RegisterAllTempoToolsOpts): void;
61
63
  /**
62
64
  * Identity + ensemble context for {@link buildServerInstructions}.