agent-tempo 1.2.0 → 1.4.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 (281) hide show
  1. package/CLAUDE.md +253 -219
  2. package/LICENSE +21 -21
  3. package/README.md +293 -289
  4. package/assets/icon-dark.svg +9 -9
  5. package/assets/icon.svg +9 -9
  6. package/assets/logo-dark.svg +11 -11
  7. package/assets/logo-light.svg +11 -11
  8. package/dashboard/README.md +91 -91
  9. package/dashboard/dist/assets/{index-D6Xyje_n.js → index-jmYe6rmS.js} +2 -2
  10. package/dashboard/dist/assets/index-jmYe6rmS.js.map +1 -0
  11. package/dashboard/dist/index.html +20 -20
  12. package/dashboard/package.json +47 -47
  13. package/dist/activities/outbox.d.ts +30 -1
  14. package/dist/activities/outbox.js +96 -3
  15. package/dist/adapters/base.js +5 -0
  16. package/dist/adapters/copilot/adapter.js +12 -1
  17. package/dist/adapters/index.d.ts +1 -1
  18. package/dist/adapters/index.js +7 -0
  19. package/dist/adapters/pi/adapter.d.ts +2 -0
  20. package/dist/adapters/pi/adapter.js +43 -0
  21. package/dist/adapters/pi/index.d.ts +16 -0
  22. package/dist/adapters/pi/index.js +10 -0
  23. package/dist/cli/global-wrapper.d.ts +19 -0
  24. package/dist/cli/global-wrapper.js +169 -0
  25. package/dist/cli/help-text.js +97 -97
  26. package/dist/cli/startup.js +11 -0
  27. package/dist/cli/upgrade-command.js +81 -81
  28. package/dist/cli.js +12 -0
  29. package/dist/client/core.js +9 -2
  30. package/dist/client/interface.d.ts +6 -0
  31. package/dist/config.d.ts +79 -0
  32. package/dist/config.js +74 -0
  33. package/dist/daemon.js +37 -1
  34. package/dist/http/aggregate.d.ts +22 -1
  35. package/dist/http/aggregate.js +41 -0
  36. package/dist/http/auth.d.ts +94 -8
  37. package/dist/http/auth.js +93 -9
  38. package/dist/http/body.d.ts +4 -1
  39. package/dist/http/body.js +6 -3
  40. package/dist/http/event-bus.js +1 -0
  41. package/dist/http/event-types.d.ts +34 -2
  42. package/dist/http/event-types.js +1 -0
  43. package/dist/http/gate-audit.d.ts +12 -0
  44. package/dist/http/gate-audit.js +95 -0
  45. package/dist/http/gate-registry.d.ts +167 -0
  46. package/dist/http/gate-registry.js +163 -0
  47. package/dist/http/gate-routes.d.ts +48 -0
  48. package/dist/http/gate-routes.js +102 -0
  49. package/dist/http/ingest-registry.d.ts +30 -0
  50. package/dist/http/ingest-registry.js +108 -0
  51. package/dist/http/inner-loop-routes.d.ts +66 -0
  52. package/dist/http/inner-loop-routes.js +182 -0
  53. package/dist/http/inner-loop.d.ts +92 -0
  54. package/dist/http/inner-loop.js +155 -0
  55. package/dist/http/server.d.ts +38 -3
  56. package/dist/http/server.js +211 -6
  57. package/dist/http/snapshot.d.ts +6 -0
  58. package/dist/http/snapshot.js +6 -0
  59. package/dist/pi/cue-pump.d.ts +61 -0
  60. package/dist/pi/cue-pump.js +95 -0
  61. package/dist/pi/extension.d.ts +45 -0
  62. package/dist/pi/extension.js +407 -0
  63. package/dist/pi/gate-client.d.ts +54 -0
  64. package/dist/pi/gate-client.js +136 -0
  65. package/dist/pi/headless.d.ts +85 -0
  66. package/dist/pi/headless.js +224 -0
  67. package/dist/pi/index.d.ts +28 -0
  68. package/dist/pi/index.js +43 -0
  69. package/dist/pi/inner-loop-client.d.ts +67 -0
  70. package/dist/pi/inner-loop-client.js +164 -0
  71. package/dist/pi/inner-loop-publisher.d.ts +187 -0
  72. package/dist/pi/inner-loop-publisher.js +236 -0
  73. package/dist/pi/lazy-proxy.d.ts +37 -0
  74. package/dist/pi/lazy-proxy.js +55 -0
  75. package/dist/pi/mission-control/actions.d.ts +48 -0
  76. package/dist/pi/mission-control/actions.js +98 -0
  77. package/dist/pi/mission-control/board.d.ts +53 -0
  78. package/dist/pi/mission-control/board.js +104 -0
  79. package/dist/pi/mission-control/extension.d.ts +44 -0
  80. package/dist/pi/mission-control/extension.js +251 -0
  81. package/dist/pi/mission-control/index.d.ts +15 -0
  82. package/dist/pi/mission-control/index.js +32 -0
  83. package/dist/pi/mission-control/inner-tail.d.ts +48 -0
  84. package/dist/pi/mission-control/inner-tail.js +76 -0
  85. package/dist/pi/mission-control/pi-ui.d.ts +43 -0
  86. package/dist/pi/mission-control/pi-ui.js +10 -0
  87. package/dist/pi/mission-control/render.d.ts +6 -0
  88. package/dist/pi/mission-control/render.js +95 -0
  89. package/dist/pi/phase-driver.d.ts +74 -0
  90. package/dist/pi/phase-driver.js +122 -0
  91. package/dist/pi/pi-types.d.ts +208 -0
  92. package/dist/pi/pi-types.js +21 -0
  93. package/dist/pi/probe.d.ts +80 -0
  94. package/dist/pi/probe.js +154 -0
  95. package/dist/pi/render-tools.d.ts +17 -0
  96. package/dist/pi/render-tools.js +51 -0
  97. package/dist/pi/reset-pump.d.ts +47 -0
  98. package/dist/pi/reset-pump.js +85 -0
  99. package/dist/pi/tool-capability.d.ts +60 -0
  100. package/dist/pi/tool-capability.js +156 -0
  101. package/dist/pi/workflow-client.d.ts +158 -0
  102. package/dist/pi/workflow-client.js +289 -0
  103. package/dist/pi/zod-to-typebox.d.ts +74 -0
  104. package/dist/pi/zod-to-typebox.js +191 -0
  105. package/dist/scripts/verify-daemon-isolation-guard.js +24 -24
  106. package/dist/server-tools.d.ts +2 -0
  107. package/dist/server-tools.js +50 -46
  108. package/dist/server.js +4 -0
  109. package/dist/spawn.d.ts +55 -0
  110. package/dist/spawn.js +84 -12
  111. package/dist/tools/agent-types.d.ts +2 -2
  112. package/dist/tools/agent-types.js +22 -17
  113. package/dist/tools/attachment-info.d.ts +2 -2
  114. package/dist/tools/attachment-info.js +38 -33
  115. package/dist/tools/broadcast.d.ts +2 -2
  116. package/dist/tools/broadcast.js +69 -64
  117. package/dist/tools/cancel-stage.d.ts +2 -2
  118. package/dist/tools/cancel-stage.js +20 -15
  119. package/dist/tools/clear-state.d.ts +2 -2
  120. package/dist/tools/clear-state.js +25 -20
  121. package/dist/tools/coat-check-evict.d.ts +2 -2
  122. package/dist/tools/coat-check-evict.js +30 -25
  123. package/dist/tools/coat-check-get.d.ts +2 -2
  124. package/dist/tools/coat-check-get.js +39 -34
  125. package/dist/tools/coat-check-list.d.ts +2 -2
  126. package/dist/tools/coat-check-list.js +48 -43
  127. package/dist/tools/coat-check-put.d.ts +2 -2
  128. package/dist/tools/coat-check-put.js +41 -36
  129. package/dist/tools/cue.d.ts +2 -2
  130. package/dist/tools/cue.js +57 -52
  131. package/dist/tools/descriptor.d.ts +72 -0
  132. package/dist/tools/descriptor.js +39 -0
  133. package/dist/tools/destroy.d.ts +2 -2
  134. package/dist/tools/destroy.js +153 -148
  135. package/dist/tools/ensemble.d.ts +2 -2
  136. package/dist/tools/ensemble.js +71 -66
  137. package/dist/tools/evaluate-gate.d.ts +2 -2
  138. package/dist/tools/evaluate-gate.js +33 -27
  139. package/dist/tools/fetch-state.d.ts +2 -2
  140. package/dist/tools/fetch-state.js +43 -38
  141. package/dist/tools/gates.d.ts +2 -2
  142. package/dist/tools/gates.js +39 -34
  143. package/dist/tools/hosts.d.ts +2 -2
  144. package/dist/tools/hosts.js +25 -20
  145. package/dist/tools/listen.d.ts +2 -2
  146. package/dist/tools/listen.js +23 -18
  147. package/dist/tools/load-lineup.d.ts +2 -2
  148. package/dist/tools/load-lineup.js +324 -319
  149. package/dist/tools/migrate.d.ts +2 -2
  150. package/dist/tools/migrate.js +45 -40
  151. package/dist/tools/pause.d.ts +2 -2
  152. package/dist/tools/pause.js +34 -29
  153. package/dist/tools/play.d.ts +2 -2
  154. package/dist/tools/play.js +53 -48
  155. package/dist/tools/quality-gate.d.ts +2 -2
  156. package/dist/tools/quality-gate.js +26 -21
  157. package/dist/tools/recall.d.ts +2 -2
  158. package/dist/tools/recall.js +32 -27
  159. package/dist/tools/recruit.d.ts +2 -2
  160. package/dist/tools/recruit.js +325 -256
  161. package/dist/tools/release.d.ts +2 -2
  162. package/dist/tools/release.js +85 -80
  163. package/dist/tools/report.d.ts +2 -2
  164. package/dist/tools/report.js +28 -23
  165. package/dist/tools/reset.d.ts +3 -0
  166. package/dist/tools/reset.js +51 -0
  167. package/dist/tools/restart.d.ts +2 -2
  168. package/dist/tools/restart.js +51 -46
  169. package/dist/tools/restore.d.ts +2 -2
  170. package/dist/tools/restore.js +76 -71
  171. package/dist/tools/save-lineup.d.ts +2 -2
  172. package/dist/tools/save-lineup.js +32 -27
  173. package/dist/tools/save-state.d.ts +2 -2
  174. package/dist/tools/save-state.js +43 -38
  175. package/dist/tools/schedule.d.ts +2 -2
  176. package/dist/tools/schedule.js +133 -128
  177. package/dist/tools/schedules.d.ts +2 -2
  178. package/dist/tools/schedules.js +41 -36
  179. package/dist/tools/set-ensemble-description.d.ts +2 -2
  180. package/dist/tools/set-ensemble-description.js +26 -21
  181. package/dist/tools/set-name.d.ts +2 -2
  182. package/dist/tools/set-name.js +38 -33
  183. package/dist/tools/set-part.d.ts +2 -2
  184. package/dist/tools/set-part.js +20 -15
  185. package/dist/tools/shutdown.d.ts +2 -2
  186. package/dist/tools/shutdown.js +39 -34
  187. package/dist/tools/stage.d.ts +2 -2
  188. package/dist/tools/stage.js +28 -23
  189. package/dist/tools/stages.d.ts +2 -2
  190. package/dist/tools/stages.js +36 -31
  191. package/dist/tools/unschedule.d.ts +2 -2
  192. package/dist/tools/unschedule.js +30 -25
  193. package/dist/tools/who-am-i.d.ts +2 -2
  194. package/dist/tools/who-am-i.js +36 -31
  195. package/dist/tools/worktree.d.ts +2 -2
  196. package/dist/tools/worktree.js +134 -129
  197. package/dist/tui/index.js +6 -6
  198. package/dist/types.d.ts +47 -2
  199. package/dist/types.js +1 -1
  200. package/dist/utils/default-part.js +1 -0
  201. package/dist/utils/grpc-shutdown-guard.d.ts +52 -0
  202. package/dist/utils/grpc-shutdown-guard.js +88 -0
  203. package/dist/utils/sdk-probe.d.ts +23 -0
  204. package/dist/utils/sdk-probe.js +46 -7
  205. package/dist/worker.d.ts +3 -1
  206. package/dist/worker.js +6 -2
  207. package/dist/workflows/session.js +70 -2
  208. package/dist/workflows/signals.d.ts +32 -2
  209. package/dist/workflows/signals.js +25 -2
  210. package/examples/agents/tempo-composer.md +56 -56
  211. package/examples/agents/tempo-conductor.md +117 -117
  212. package/examples/agents/tempo-critic.md +73 -73
  213. package/examples/agents/tempo-improv.md +74 -74
  214. package/examples/agents/tempo-liner.md +75 -75
  215. package/examples/agents/tempo-roadie.md +61 -61
  216. package/examples/agents/tempo-soloist.md +71 -71
  217. package/examples/agents/tempo-tuner.md +94 -94
  218. package/examples/ensembles/tempo-big-band.yaml +146 -146
  219. package/examples/ensembles/tempo-dev-team.yaml +58 -58
  220. package/examples/ensembles/tempo-headless-jam.yaml +77 -77
  221. package/examples/ensembles/tempo-jam-session.yaml +41 -41
  222. package/examples/ensembles/tempo-mock-jam.yaml +79 -79
  223. package/examples/ensembles/tempo-review-squad.yaml +32 -32
  224. package/package.json +176 -173
  225. package/packaging/launchd/com.agent.tempo.plist +46 -46
  226. package/packaging/systemd/agent-tempo.service +32 -32
  227. package/packaging/windows/install-task.ps1 +71 -71
  228. package/scenarios/conductor-recruit-mock.yaml +33 -33
  229. package/scenarios/echo-roundtrip.yaml +15 -15
  230. package/scenarios/multi-player-handoff.yaml +38 -38
  231. package/scenarios/recruit-cascade.yaml +38 -38
  232. package/scenarios/two-player-conversation.yaml +33 -33
  233. package/workflow-bundle.js +97 -6
  234. package/dashboard/dist/assets/index-D6Xyje_n.js.map +0 -1
  235. package/dist/activities/claude-stop.d.ts +0 -21
  236. package/dist/activities/claude-stop.js +0 -94
  237. package/dist/channel.d.ts +0 -3
  238. package/dist/channel.js +0 -48
  239. package/dist/copilot-bridge.d.ts +0 -22
  240. package/dist/copilot-bridge.js +0 -565
  241. package/dist/scripts/258-spotcheck.js +0 -303
  242. package/dist/tools/detach.d.ts +0 -4
  243. package/dist/tools/detach.js +0 -45
  244. package/dist/tools/encore.d.ts +0 -4
  245. package/dist/tools/encore.js +0 -31
  246. package/dist/tools/helpers.d.ts +0 -21
  247. package/dist/tools/helpers.js +0 -25
  248. package/dist/tools/pause-ensemble.d.ts +0 -4
  249. package/dist/tools/pause-ensemble.js +0 -58
  250. package/dist/tools/resume-ensemble.d.ts +0 -4
  251. package/dist/tools/resume-ensemble.js +0 -79
  252. package/dist/tools/stop.d.ts +0 -4
  253. package/dist/tools/stop.js +0 -29
  254. package/dist/tui/client.d.ts +0 -6
  255. package/dist/tui/client.js +0 -9
  256. package/dist/tui/components/ActivityLog.d.ts +0 -16
  257. package/dist/tui/components/ActivityLog.js +0 -36
  258. package/dist/tui/components/CommandOverlay.d.ts +0 -15
  259. package/dist/tui/components/CommandOverlay.js +0 -34
  260. package/dist/tui/components/ConductorChat.d.ts +0 -16
  261. package/dist/tui/components/ConductorChat.js +0 -32
  262. package/dist/tui/components/EnsembleListView.d.ts +0 -14
  263. package/dist/tui/components/EnsembleListView.js +0 -32
  264. package/dist/tui/components/EnsemblePanel.d.ts +0 -12
  265. package/dist/tui/components/EnsemblePanel.js +0 -40
  266. package/dist/tui/components/InputBar.d.ts +0 -13
  267. package/dist/tui/components/InputBar.js +0 -58
  268. package/dist/tui/components/ScheduleOverlay.d.ts +0 -13
  269. package/dist/tui/components/ScheduleOverlay.js +0 -113
  270. package/dist/tui/components/TopBar.d.ts +0 -12
  271. package/dist/tui/components/TopBar.js +0 -15
  272. package/dist/tui/core-api.d.ts +0 -26
  273. package/dist/tui/core-api.js +0 -67
  274. package/dist/tui/hooks/useEnsembleDiscovery.d.ts +0 -3
  275. package/dist/tui/hooks/useEnsembleDiscovery.js +0 -30
  276. package/dist/tui/hooks/useMaestroPoller.d.ts +0 -3
  277. package/dist/tui/hooks/useMaestroPoller.js +0 -36
  278. package/dist/tui/hooks/useSendCommand.d.ts +0 -7
  279. package/dist/tui/hooks/useSendCommand.js +0 -29
  280. package/dist/utils/bg-preflight.d.ts +0 -25
  281. package/dist/utils/bg-preflight.js +0 -154
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GateClient = exports.INGEST_TOKEN_ENV = void 0;
4
+ /**
5
+ * Gate poll-bridge client (3d / MD-G) — the SUBPROCESS side of the operator gate.
6
+ *
7
+ * When the Pi `tool_call` handler engages the gate, it awaits {@link
8
+ * GateClient.awaitDecision}, which polls the daemon's resolution route
9
+ * (`GET /v1/players/:e/:p/gate/:requestId/resolution`, ingest-token auth, same
10
+ * loopback boundary as the 3c inner-loop) until the operator decides OR the
11
+ * daemon's authoritative 45s auto-allow lands. R1 (resolved): Pi awaits the
12
+ * returned `tool_call` Promise, so this inline poll-await is safe.
13
+ *
14
+ * TWO bounds, BOTH required (architect/conductor):
15
+ * - `signal` (Pi's `ctx.signal`): if the turn is cancelled (Esc/abort), the
16
+ * poll stops immediately and resolves `allow` (the tool won't run anyway —
17
+ * never leave the loop hung; Pi #2381).
18
+ * - `timeoutMs` (default just beyond the daemon's 45s): a safety net for an
19
+ * UNREACHABLE daemon — autonomous-first, so a timeout resolves `allow`.
20
+ * In the normal case the daemon answers first (operator decision, or its lazy
21
+ * 45s auto-allow), so this subprocess deadline is only the daemon-down backstop.
22
+ *
23
+ * Effect mapping: operator `deny` → `deny` (block the tool); `allow` /
24
+ * `auto-allow` / timeout / abort → `allow` (permit). Network/transport errors on
25
+ * a single poll are swallowed (retry next tick); only the bounds end the loop.
26
+ *
27
+ * Client-side ONLY (src/pi). Not imported by workflows.
28
+ */
29
+ const port_file_1 = require("../http/port-file");
30
+ /** Env var carrying the per-player ingest token (threaded in at spawn, shared w/ inner-loop). */
31
+ exports.INGEST_TOKEN_ENV = 'AGENT_TEMPO_INGEST_TOKEN';
32
+ /** Default poll cadence — reuse the inner-loop short-poll so a fresh decision lands within ~1s. */
33
+ const DEFAULT_POLL_MS = 1_000;
34
+ /**
35
+ * Subprocess deadline — slightly beyond the daemon's 45s auto-allow so, when the
36
+ * daemon is reachable, its authoritative answer always wins; this only fires when
37
+ * the daemon is unreachable.
38
+ */
39
+ const DEFAULT_TIMEOUT_MS = 50_000;
40
+ const log = (...args) => {
41
+ // eslint-disable-next-line no-console
42
+ console.error('[agent-tempo:pi]', ...args);
43
+ };
44
+ function resolveFetch() {
45
+ const g = globalThis.fetch;
46
+ return typeof g === 'function' ? g : null;
47
+ }
48
+ /** Default cancellable sleep — resolves after `ms`, or early if `signal` aborts. */
49
+ function defaultSleep(ms, signal) {
50
+ return new Promise((resolve) => {
51
+ if (signal?.aborted) {
52
+ resolve();
53
+ return;
54
+ }
55
+ const timer = setTimeout(() => { signal?.removeEventListener('abort', onAbort); resolve(); }, ms);
56
+ const onAbort = () => { clearTimeout(timer); resolve(); };
57
+ signal?.addEventListener('abort', onAbort, { once: true });
58
+ });
59
+ }
60
+ /**
61
+ * Loopback poll-bridge to the daemon gate. Construct one per headless player.
62
+ * The `awaitDecision` poll is the only public surface used by the engagement path.
63
+ */
64
+ class GateClient {
65
+ ensemble;
66
+ playerId;
67
+ ingestToken;
68
+ readPort;
69
+ fetchFn;
70
+ pollIntervalMs;
71
+ timeoutMs;
72
+ now;
73
+ sleep;
74
+ constructor(opts) {
75
+ this.ensemble = opts.ensemble;
76
+ this.playerId = opts.playerId;
77
+ this.ingestToken = opts.ingestToken ?? process.env[exports.INGEST_TOKEN_ENV];
78
+ this.readPort = opts.readPort ?? (() => (0, port_file_1.readPortFile)());
79
+ this.fetchFn = opts.fetchFn ?? resolveFetch();
80
+ this.pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_POLL_MS;
81
+ this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
82
+ this.now = opts.now ?? Date.now;
83
+ this.sleep = opts.sleep ?? defaultSleep;
84
+ }
85
+ get enabled() {
86
+ return Boolean(this.ingestToken) && this.fetchFn !== null;
87
+ }
88
+ resolutionUrl(port, requestId) {
89
+ return `http://127.0.0.1:${port}/v1/players/` +
90
+ `${encodeURIComponent(this.ensemble)}/${encodeURIComponent(this.playerId)}/gate/` +
91
+ `${encodeURIComponent(requestId)}/resolution`;
92
+ }
93
+ /** One poll. Returns the effect on a resolved answer, or null to keep polling. */
94
+ async pollOnce(requestId) {
95
+ const port = this.readPort();
96
+ if (port == null)
97
+ return null; // daemon HTTP down → keep trying until the bound
98
+ try {
99
+ const res = await this.fetchFn(this.resolutionUrl(port, requestId), {
100
+ method: 'GET',
101
+ headers: { 'X-Ingest-Token': this.ingestToken },
102
+ });
103
+ if (res.status !== 200)
104
+ return null; // 404 (not-yet-registered race) / 403 → retry
105
+ const data = (await res.json());
106
+ if (data.status !== 'resolved')
107
+ return null;
108
+ return data.decision === 'deny' ? 'deny' : 'allow'; // allow + auto-allow → allow
109
+ }
110
+ catch {
111
+ return null; // transport error → retry next tick
112
+ }
113
+ }
114
+ /**
115
+ * Poll the daemon for the operator's decision on `requestId`, blocking until
116
+ * resolved / timeout / abort. FAIL-OPEN: `allow` unless the operator explicitly
117
+ * denied. Without a token/transport (e.g. interactive Pi, daemon HTTP off) →
118
+ * immediate `allow` (the gate is a daemon-mediated feature).
119
+ */
120
+ async awaitDecision(requestId, opts = {}) {
121
+ if (!this.enabled)
122
+ return 'allow';
123
+ const deadline = this.now() + (opts.timeoutMs ?? this.timeoutMs);
124
+ while (this.now() < deadline) {
125
+ if (opts.signal?.aborted)
126
+ return 'allow'; // turn cancelled — don't block a dying turn
127
+ const effect = await this.pollOnce(requestId);
128
+ if (effect !== null)
129
+ return effect;
130
+ await this.sleep(this.pollIntervalMs, opts.signal);
131
+ }
132
+ log(`gate decision timed out for ${requestId} (daemon unreachable?) — auto-allow`);
133
+ return 'allow'; // autonomous-first backstop (daemon-down)
134
+ }
135
+ }
136
+ exports.GateClient = GateClient;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Headless Pi runtime (Phase 3a). Spawned by the daemon (spawnPiHeadless →
3
+ * src/adapters/pi/adapter.ts) for a recruited `agent: 'pi'` player. No human, no
4
+ * terminal: it constructs Pi's `createAgentSession` with the agent-tempo
5
+ * extension injected INLINE, and the module-scope extension singleton
6
+ * (createPiExtension, mode='headless') owns claim/heartbeat/tool-registration/
7
+ * cue-pump on `session_start`. Reuses ~everything from Phases 1–2.
8
+ *
9
+ * Lifecycle:
10
+ * 1. probe Pi SDK; resolve the model via pi-ai `getModel` — a bad/unindexed
11
+ * model fails CLEAN (exit before attach, no orphan — architect's backstop).
12
+ * 2. createAgentSession({ resourceLoader: DefaultResourceLoader({
13
+ * extensionFactories: [ext] }), model? }).
14
+ * 3. await session.bindExtensions({}) → fires session_start → the singleton
15
+ * attaches (claim + heartbeat + tools + cue pump). bindExtensions IS the
16
+ * explicit bootstrap (not "hope session_start fires").
17
+ * 4. stay alive until a shutdown signal (SIGTERM/SIGINT).
18
+ * 5. RELIABLE detach (headless owns the exit): detachAllPiRuntimesForExit()
19
+ * [await adapterExited] → session.dispose() → process exit.
20
+ *
21
+ * ESM note: the Pi SDK is an ESM-only optional dep; we import it via a
22
+ * `Function`-wrapped dynamic `import()` so tsc (module=commonjs) doesn't
23
+ * downlevel it to `require()` — Node resolves the real ESM module at runtime.
24
+ *
25
+ * Determinism boundary: client-side only.
26
+ */
27
+ import { type Config } from '../config';
28
+ import { type PiToolAccess } from './extension';
29
+ export interface RunHeadlessPiOptions {
30
+ config?: Config;
31
+ toolAccess?: PiToolAccess;
32
+ /** `provider/model` selector; absent → Pi default. */
33
+ model?: string;
34
+ /** Restart-resume: prior Pi conversation id (A4 wires SessionManager). */
35
+ continueSessionId?: string;
36
+ }
37
+ /**
38
+ * Build the `DefaultResourceLoader` options for a headless Pi player.
39
+ *
40
+ * SECURITY — S2 (MD-C deny-list soundness). The `restricted` tool gate is a
41
+ * DENY-LIST over shell/exec tool *names* (tool-capability.ts EXEC_TOOLS, via
42
+ * `classify(name) === 'exec'` — F1 replaced extension.ts's former local set). That
43
+ * guarantee — "restricted = no host execution" — holds ONLY IF no third-party
44
+ * extension can register an un-blacklisted execution tool (e.g. a custom
45
+ * `python` / `npm` / `run` tool). It therefore depends on a hard structural
46
+ * fact: which extensions Pi loads.
47
+ *
48
+ * Verified against the installed Pi SDK 0.78 source (NOT assumed):
49
+ * - `DefaultResourceLoader.reload()` (resource-loader.js:271-276) builds
50
+ * `extensionPaths = noExtensions ? cliEnabledExtensions
51
+ * : merge(cliEnabledExtensions, enabledExtensions)`
52
+ * where `enabledExtensions` (line 229) are the DISK/package extensions from
53
+ * `packageManager.resolve()` (`~/.pi/agent/extensions/`, `<cwd>/.pi/extensions/`,
54
+ * installed packages). `loadExtensions(extensionPaths)` then loads them and
55
+ * MERGES with our inline factories (lines 274-276).
56
+ * - `noExtensions` defaults to `false` (constructor, line 132) — so the naive
57
+ * loader DOES load disk extensions. That is the S2 gap.
58
+ *
59
+ * Fix (= security's "exclude the extensions dir", done structurally):
60
+ * - `noExtensions: true` → `extensionPaths` collapses to `cliEnabledExtensions`,
61
+ * which is empty because we pass NO `additionalExtensionPaths`. So
62
+ * `loadExtensions([])` registers nothing from disk/packages.
63
+ * - Inline `extensionFactories` load UNCONDITIONALLY (reload() line 275 is not
64
+ * gated by `noExtensions`), so our agent-tempo extension still attaches.
65
+ * Net: the ONLY tools present are Pi's built-ins (bash/read/edit/write/grep —
66
+ * all covered by the deny-list) + our agent-tempo MCP tools (no exec). No
67
+ * third-party tool can slip past the deny-list. Skills/prompts/themes cannot
68
+ * register tools, so they are not a vector and are left at defaults.
69
+ *
70
+ * Kept as a pure, exported helper so the `noExtensions: true` invariant has a
71
+ * unit regression test (test/pi-headless-loader.test.ts) without needing the Pi
72
+ * SDK installed.
73
+ */
74
+ export declare function buildPiResourceLoaderOptions(params: {
75
+ cwd: string;
76
+ agentDir: string;
77
+ /** The inline agent-tempo extension factory (`createPiExtension(...)`). Typed
78
+ * with a bottom param so any concrete factory arity is assignable. */
79
+ extensionFactory: (pi: never) => void;
80
+ }): Record<string, unknown>;
81
+ /**
82
+ * Boot + run a headless Pi player until shutdown. Resolves when the process has
83
+ * cleanly detached + disposed (it also calls process.exit on the terminal path).
84
+ */
85
+ export declare function runHeadlessPi(opts?: RunHeadlessPiOptions): Promise<void>;
@@ -0,0 +1,224 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildPiResourceLoaderOptions = buildPiResourceLoaderOptions;
4
+ exports.runHeadlessPi = runHeadlessPi;
5
+ /**
6
+ * Headless Pi runtime (Phase 3a). Spawned by the daemon (spawnPiHeadless →
7
+ * src/adapters/pi/adapter.ts) for a recruited `agent: 'pi'` player. No human, no
8
+ * terminal: it constructs Pi's `createAgentSession` with the agent-tempo
9
+ * extension injected INLINE, and the module-scope extension singleton
10
+ * (createPiExtension, mode='headless') owns claim/heartbeat/tool-registration/
11
+ * cue-pump on `session_start`. Reuses ~everything from Phases 1–2.
12
+ *
13
+ * Lifecycle:
14
+ * 1. probe Pi SDK; resolve the model via pi-ai `getModel` — a bad/unindexed
15
+ * model fails CLEAN (exit before attach, no orphan — architect's backstop).
16
+ * 2. createAgentSession({ resourceLoader: DefaultResourceLoader({
17
+ * extensionFactories: [ext] }), model? }).
18
+ * 3. await session.bindExtensions({}) → fires session_start → the singleton
19
+ * attaches (claim + heartbeat + tools + cue pump). bindExtensions IS the
20
+ * explicit bootstrap (not "hope session_start fires").
21
+ * 4. stay alive until a shutdown signal (SIGTERM/SIGINT).
22
+ * 5. RELIABLE detach (headless owns the exit): detachAllPiRuntimesForExit()
23
+ * [await adapterExited] → session.dispose() → process exit.
24
+ *
25
+ * ESM note: the Pi SDK is an ESM-only optional dep; we import it via a
26
+ * `Function`-wrapped dynamic `import()` so tsc (module=commonjs) doesn't
27
+ * downlevel it to `require()` — Node resolves the real ESM module at runtime.
28
+ *
29
+ * Determinism boundary: client-side only.
30
+ */
31
+ const config_1 = require("../config");
32
+ const sdk_probe_1 = require("../utils/sdk-probe");
33
+ const extension_1 = require("./extension");
34
+ const probe_1 = require("./probe");
35
+ const log = (...args) => {
36
+ // eslint-disable-next-line no-console
37
+ console.error('[agent-tempo:pi-headless]', ...args);
38
+ };
39
+ /**
40
+ * True dynamic ESM import that survives tsc's commonjs downleveling. `import(x)`
41
+ * with a literal would be rewritten to a `require`-based helper (breaks on an
42
+ * ESM-only package); the `Function` indirection keeps a native `import()` at
43
+ * runtime so Node loads the real ESM module.
44
+ */
45
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
46
+ const esmImport = new Function('specifier', 'return import(specifier)');
47
+ /**
48
+ * Resolve a Pi `Model` object from a `provider/model` string via pi-ai's
49
+ * `getModel`. Returns `{ model }` on success, `{ fatal }` with an actionable
50
+ * message on an invalid/unindexed model (getModel returns `undefined` — a plain
51
+ * check, no throw), or `{}` when no model was requested (Pi uses its own default
52
+ * — the 3a anthropic-default path).
53
+ */
54
+ async function resolveModel(modelStr) {
55
+ if (!modelStr)
56
+ return {};
57
+ const slash = modelStr.indexOf('/');
58
+ if (slash <= 0 || slash === modelStr.length - 1) {
59
+ return { fatal: `Invalid Pi model "${modelStr}" — expected "provider/model" (e.g. anthropic/claude-opus-4-5).` };
60
+ }
61
+ const provider = modelStr.slice(0, slash);
62
+ const modelName = modelStr.slice(slash + 1);
63
+ try {
64
+ const piAi = await esmImport(probe_1.PI_AI_PACKAGE);
65
+ const getModel = piAi.getModel;
66
+ const model = getModel(provider, modelName);
67
+ if (model === undefined || model === null) {
68
+ return {
69
+ fatal: `Pi model "${modelStr}" not found in Pi's provider index (provider="${provider}"). ` +
70
+ `Check the model id against \`pi --list-models\` / models.dev.`,
71
+ };
72
+ }
73
+ return { model };
74
+ }
75
+ catch (err) {
76
+ return { fatal: `Failed to resolve Pi model "${modelStr}": ${err instanceof Error ? err.message : String(err)}` };
77
+ }
78
+ }
79
+ /**
80
+ * Build the `DefaultResourceLoader` options for a headless Pi player.
81
+ *
82
+ * SECURITY — S2 (MD-C deny-list soundness). The `restricted` tool gate is a
83
+ * DENY-LIST over shell/exec tool *names* (tool-capability.ts EXEC_TOOLS, via
84
+ * `classify(name) === 'exec'` — F1 replaced extension.ts's former local set). That
85
+ * guarantee — "restricted = no host execution" — holds ONLY IF no third-party
86
+ * extension can register an un-blacklisted execution tool (e.g. a custom
87
+ * `python` / `npm` / `run` tool). It therefore depends on a hard structural
88
+ * fact: which extensions Pi loads.
89
+ *
90
+ * Verified against the installed Pi SDK 0.78 source (NOT assumed):
91
+ * - `DefaultResourceLoader.reload()` (resource-loader.js:271-276) builds
92
+ * `extensionPaths = noExtensions ? cliEnabledExtensions
93
+ * : merge(cliEnabledExtensions, enabledExtensions)`
94
+ * where `enabledExtensions` (line 229) are the DISK/package extensions from
95
+ * `packageManager.resolve()` (`~/.pi/agent/extensions/`, `<cwd>/.pi/extensions/`,
96
+ * installed packages). `loadExtensions(extensionPaths)` then loads them and
97
+ * MERGES with our inline factories (lines 274-276).
98
+ * - `noExtensions` defaults to `false` (constructor, line 132) — so the naive
99
+ * loader DOES load disk extensions. That is the S2 gap.
100
+ *
101
+ * Fix (= security's "exclude the extensions dir", done structurally):
102
+ * - `noExtensions: true` → `extensionPaths` collapses to `cliEnabledExtensions`,
103
+ * which is empty because we pass NO `additionalExtensionPaths`. So
104
+ * `loadExtensions([])` registers nothing from disk/packages.
105
+ * - Inline `extensionFactories` load UNCONDITIONALLY (reload() line 275 is not
106
+ * gated by `noExtensions`), so our agent-tempo extension still attaches.
107
+ * Net: the ONLY tools present are Pi's built-ins (bash/read/edit/write/grep —
108
+ * all covered by the deny-list) + our agent-tempo MCP tools (no exec). No
109
+ * third-party tool can slip past the deny-list. Skills/prompts/themes cannot
110
+ * register tools, so they are not a vector and are left at defaults.
111
+ *
112
+ * Kept as a pure, exported helper so the `noExtensions: true` invariant has a
113
+ * unit regression test (test/pi-headless-loader.test.ts) without needing the Pi
114
+ * SDK installed.
115
+ */
116
+ function buildPiResourceLoaderOptions(params) {
117
+ return {
118
+ cwd: params.cwd,
119
+ agentDir: params.agentDir,
120
+ extensionFactories: [params.extensionFactory],
121
+ // SECURITY (S2): hard-exclude all disk/package extensions. Do NOT add
122
+ // `additionalExtensionPaths` here — that would re-introduce the exec-tool
123
+ // vector this flag closes.
124
+ noExtensions: true,
125
+ };
126
+ }
127
+ /**
128
+ * Boot + run a headless Pi player until shutdown. Resolves when the process has
129
+ * cleanly detached + disposed (it also calls process.exit on the terminal path).
130
+ */
131
+ async function runHeadlessPi(opts = {}) {
132
+ const config = opts.config ?? (0, config_1.getConfig)();
133
+ const toolAccess = opts.toolAccess ?? 'restricted';
134
+ // 1) Probe — the spawn entry is the only place the Pi SDK is REQUIRED.
135
+ if (!(0, sdk_probe_1.probeSdkInstall)(probe_1.PI_PACKAGE)) {
136
+ log(`FATAL: ${probe_1.PI_PACKAGE} is not installed — cannot run headless Pi. Exiting.`);
137
+ process.exit(1);
138
+ return;
139
+ }
140
+ // 2) Resolve the model BEFORE creating the session — a bad model fails clean
141
+ // (exit before attach, no half-attached orphan).
142
+ const { model, fatal } = await resolveModel(opts.model);
143
+ if (fatal) {
144
+ log(`FATAL: ${fatal} Exiting without attaching.`);
145
+ process.exit(2);
146
+ return;
147
+ }
148
+ // 3) Inline extension factory — headless mode → the MD-C tool gate is active.
149
+ const extensionFactory = (0, extension_1.createPiExtension)({ mode: 'headless', toolAccess });
150
+ // 4) Construct the Pi SDK session with the extension injected inline.
151
+ const piSdk = await esmImport(probe_1.PI_PACKAGE);
152
+ const createAgentSession = piSdk.createAgentSession;
153
+ const DefaultResourceLoader = piSdk.DefaultResourceLoader;
154
+ // Pi's DefaultResourceLoader REQUIRES agentDir (normalizePath does
155
+ // `.startsWith()` on it) — getAgentDir() resolves ~/.pi/agent. Pass it to BOTH
156
+ // createAgentSession and the loader. (Found in the 3a live smoke — devops.)
157
+ const getAgentDir = piSdk.getAgentDir;
158
+ const agentDir = getAgentDir();
159
+ const resourceLoader = new DefaultResourceLoader(buildPiResourceLoaderOptions({ cwd: process.cwd(), agentDir, extensionFactory }));
160
+ // CRITICAL (3a live smoke — devops): createAgentSession only calls
161
+ // resourceLoader.reload() when IT constructs the loader (sdk.js:99-101). When we
162
+ // pass our OWN loader, reload() is skipped — and DefaultResourceLoader inits
163
+ // `extensionsResult.extensions = []` (resource-loader.js:146) and only populates
164
+ // it during reload(). So without this explicit reload our extension never
165
+ // registers, and session_start fires into an empty handler list (no claim, no
166
+ // heartbeat). The SDK's own doc comment (sdk.js:74-83) prescribes this exact
167
+ // construct → reload() → pass-as-resourceLoader sequence.
168
+ await resourceLoader.reload();
169
+ const { session } = await createAgentSession({
170
+ cwd: process.cwd(),
171
+ agentDir,
172
+ ...(model ? { model } : {}),
173
+ resourceLoader,
174
+ // NOTE (A4): restart-resume via a SessionManager seeded from
175
+ // opts.continueSessionId / ENV.PI_CONTINUE_SESSION lands in A4; 3a proves the
176
+ // loop on a fresh session.
177
+ });
178
+ // 5) Explicit bootstrap — fires session_start → the singleton claims/attaches.
179
+ await session.bindExtensions({});
180
+ // Headless session_start carries NO `session` field (interactive does), so the
181
+ // extension can't wire the cue pump's session ref itself. We hold the SDK
182
+ // session here — set it on the now-claimed runtime so the cue pump can inject.
183
+ // (3a live smoke — devops; the headless-vs-interactive session-wiring gap.)
184
+ const playerId = process.env[config_1.ENV.PLAYER_NAME] || `pi-${process.pid}`;
185
+ (0, extension_1.setRuntimeSession)((0, config_1.sessionWorkflowId)(config.ensemble, playerId), session);
186
+ log(`headless Pi session bound (toolAccess=${toolAccess}, ` +
187
+ `model=${opts.model ?? 'pi-default'}${opts.continueSessionId ? `, continue=${opts.continueSessionId}` : ''}, ` +
188
+ `sessionId=${session.sessionId ?? '?'})`);
189
+ // 6) Stay alive until a shutdown signal, then RELIABLE detach → dispose → exit.
190
+ // Keep the event loop alive with a REF'd timer: the heartbeat + cue-pump timers
191
+ // are `.unref()`'d by design (so they never block a clean exit), and the
192
+ // SIGTERM/SIGINT once-listeners aren't active handles — so WITHOUT this the loop
193
+ // drains and Node exits code 0 immediately after bindExtensions in headless mode.
194
+ // (Found in the 3a live smoke — devops.)
195
+ const keepAlive = setInterval(() => { }, 30_000);
196
+ try {
197
+ await new Promise((resolveShutdown) => {
198
+ const onSignal = (sig) => { log(`received ${sig} — shutting down`); resolveShutdown(); };
199
+ process.once('SIGTERM', () => onSignal('SIGTERM'));
200
+ process.once('SIGINT', () => onSignal('SIGINT'));
201
+ });
202
+ }
203
+ finally {
204
+ clearInterval(keepAlive);
205
+ }
206
+ // Headless owns the exit sequence: await adapterExited (unmaps the runtime)
207
+ // THEN dispose the SDK session (the dispose-fired session_shutdown finds no
208
+ // mapped runtime → no-op, so no double-detach).
209
+ try {
210
+ await (0, extension_1.detachAllPiRuntimesForExit)();
211
+ }
212
+ catch (err) {
213
+ log('detach failed (reaper backstops):', err);
214
+ }
215
+ try {
216
+ session.dispose();
217
+ }
218
+ catch (err) {
219
+ log('dispose failed:', err);
220
+ }
221
+ log('headless Pi clean-exit complete');
222
+ // eslint-disable-next-line no-process-exit
223
+ process.exit(0);
224
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * agent-tempo Pi integration — barrel.
3
+ *
4
+ * Default export is the Pi extension factory (`export default function(pi)`).
5
+ * Named exports expose the testable units and the client-side wrapper for reuse
6
+ * by the headless Pi runtime (Phase 3).
7
+ *
8
+ * Tool registration: the extension renders the shared transport-neutral tool
9
+ * descriptors (src/tools/descriptor.ts) onto Pi via `renderToPi`, deriving
10
+ * TypeBox param schemas from zod through `zod-to-typebox.ts`. There is no
11
+ * Pi-specific re-implementation of any tool.
12
+ *
13
+ * See src/pi/README.md for the Phase 0/2 findings (abrupt-death / MD-A, D12a)
14
+ * and known limitations.
15
+ */
16
+ export { default } from './extension';
17
+ export { PhaseDriver } from './phase-driver';
18
+ export type { PiPhase, WorkflowAction, PhaseDriverResult } from './phase-driver';
19
+ export { PiWorkflowClient } from './workflow-client';
20
+ export type { PiWorkflowClientOptions } from './workflow-client';
21
+ export { CuePump } from './cue-pump';
22
+ export type { CueSource, SessionResolver, CuePumpOptions } from './cue-pump';
23
+ export { renderToPi, toPiResult } from './render-tools';
24
+ export { createLazyProxy } from './lazy-proxy';
25
+ export { zodShapeToTypeBox, UnsupportedZodFeatureError } from './zod-to-typebox';
26
+ export { probePi, PI_PACKAGE, PI_AI_PACKAGE, TESTED_PI_VERSION, PI_NODE_FLOOR } from './probe';
27
+ export type { PiProbeResult } from './probe';
28
+ export type { ExtensionAPI, PiExtension, PiAgentSession, PiEventPayload, PiToolDefinition, PiToolResult, PiLifecycleEvent, } from './pi-types';
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PI_NODE_FLOOR = exports.TESTED_PI_VERSION = exports.PI_AI_PACKAGE = exports.PI_PACKAGE = exports.probePi = exports.UnsupportedZodFeatureError = exports.zodShapeToTypeBox = exports.createLazyProxy = exports.toPiResult = exports.renderToPi = exports.CuePump = exports.PiWorkflowClient = exports.PhaseDriver = exports.default = void 0;
7
+ /**
8
+ * agent-tempo Pi integration — barrel.
9
+ *
10
+ * Default export is the Pi extension factory (`export default function(pi)`).
11
+ * Named exports expose the testable units and the client-side wrapper for reuse
12
+ * by the headless Pi runtime (Phase 3).
13
+ *
14
+ * Tool registration: the extension renders the shared transport-neutral tool
15
+ * descriptors (src/tools/descriptor.ts) onto Pi via `renderToPi`, deriving
16
+ * TypeBox param schemas from zod through `zod-to-typebox.ts`. There is no
17
+ * Pi-specific re-implementation of any tool.
18
+ *
19
+ * See src/pi/README.md for the Phase 0/2 findings (abrupt-death / MD-A, D12a)
20
+ * and known limitations.
21
+ */
22
+ var extension_1 = require("./extension");
23
+ Object.defineProperty(exports, "default", { enumerable: true, get: function () { return __importDefault(extension_1).default; } });
24
+ var phase_driver_1 = require("./phase-driver");
25
+ Object.defineProperty(exports, "PhaseDriver", { enumerable: true, get: function () { return phase_driver_1.PhaseDriver; } });
26
+ var workflow_client_1 = require("./workflow-client");
27
+ Object.defineProperty(exports, "PiWorkflowClient", { enumerable: true, get: function () { return workflow_client_1.PiWorkflowClient; } });
28
+ var cue_pump_1 = require("./cue-pump");
29
+ Object.defineProperty(exports, "CuePump", { enumerable: true, get: function () { return cue_pump_1.CuePump; } });
30
+ var render_tools_1 = require("./render-tools");
31
+ Object.defineProperty(exports, "renderToPi", { enumerable: true, get: function () { return render_tools_1.renderToPi; } });
32
+ Object.defineProperty(exports, "toPiResult", { enumerable: true, get: function () { return render_tools_1.toPiResult; } });
33
+ var lazy_proxy_1 = require("./lazy-proxy");
34
+ Object.defineProperty(exports, "createLazyProxy", { enumerable: true, get: function () { return lazy_proxy_1.createLazyProxy; } });
35
+ var zod_to_typebox_1 = require("./zod-to-typebox");
36
+ Object.defineProperty(exports, "zodShapeToTypeBox", { enumerable: true, get: function () { return zod_to_typebox_1.zodShapeToTypeBox; } });
37
+ Object.defineProperty(exports, "UnsupportedZodFeatureError", { enumerable: true, get: function () { return zod_to_typebox_1.UnsupportedZodFeatureError; } });
38
+ var probe_1 = require("./probe");
39
+ Object.defineProperty(exports, "probePi", { enumerable: true, get: function () { return probe_1.probePi; } });
40
+ Object.defineProperty(exports, "PI_PACKAGE", { enumerable: true, get: function () { return probe_1.PI_PACKAGE; } });
41
+ Object.defineProperty(exports, "PI_AI_PACKAGE", { enumerable: true, get: function () { return probe_1.PI_AI_PACKAGE; } });
42
+ Object.defineProperty(exports, "TESTED_PI_VERSION", { enumerable: true, get: function () { return probe_1.TESTED_PI_VERSION; } });
43
+ Object.defineProperty(exports, "PI_NODE_FLOOR", { enumerable: true, get: function () { return probe_1.PI_NODE_FLOOR; } });
@@ -0,0 +1,67 @@
1
+ import type { InnerFrame, InnerLoopRegistry } from './inner-loop-publisher';
2
+ /** Env var carrying the per-player ingest token (threaded in at spawn). */
3
+ export declare const INGEST_TOKEN_ENV = "AGENT_TEMPO_INGEST_TOKEN";
4
+ /** DOS backstop — frames over this are dropped (summaries are already ~2KB-truncated). */
5
+ export declare const MAX_FRAME_BYTES: number;
6
+ /** Minimal `fetch` shape this client needs — injectable for tests. */
7
+ export type InnerLoopFetch = (url: string, init: {
8
+ method: string;
9
+ headers: Record<string, string>;
10
+ body?: string;
11
+ }) => Promise<{
12
+ status: number;
13
+ json(): Promise<unknown>;
14
+ }>;
15
+ export interface InnerLoopHttpClientOptions {
16
+ /** The player's ensemble (URL path segment). */
17
+ ensemble: string;
18
+ /** The player's id (URL path segment). */
19
+ playerId: string;
20
+ /** Ingest token. Defaults to `process.env[INGEST_TOKEN_ENV]`. */
21
+ ingestToken?: string;
22
+ /** Daemon port discovery. Defaults to {@link readPortFile}. */
23
+ readPort?: () => number | null;
24
+ /** HTTP transport. Defaults to global `fetch` (no-op if absent). */
25
+ fetchFn?: InnerLoopFetch;
26
+ /** Min interval between presence GETs (default 1000ms). */
27
+ presencePollMs?: number;
28
+ /** Injected clock (tests). Defaults to `Date.now`. */
29
+ now?: () => number;
30
+ }
31
+ /**
32
+ * Loopback-HTTP {@link InnerLoopRegistry}. Construct one per headless player with
33
+ * its ensemble + playerId; the publisher calls `publish` / `subscriberCount`.
34
+ * The `workflowId` argument is the player's own and is unused — the URL is built
35
+ * from the ctor ensemble + playerId.
36
+ */
37
+ export declare class InnerLoopHttpClient implements InnerLoopRegistry {
38
+ private readonly ensemble;
39
+ private readonly playerId;
40
+ private readonly ingestToken;
41
+ private readonly readPort;
42
+ private readonly fetchFn;
43
+ private readonly presencePollMs;
44
+ private readonly now;
45
+ /** Last presence count from the daemon. 0 until the first GET resolves / on failure. */
46
+ private cachedSubscribers;
47
+ /** 3d — last gateArmed flag from the daemon (folded into the presence response). */
48
+ private cachedGateArmed;
49
+ private lastPresenceRefresh;
50
+ constructor(opts: InnerLoopHttpClientOptions);
51
+ /** Whether the client can talk to the daemon at all (token + transport present). */
52
+ private get enabled();
53
+ private baseUrl;
54
+ /** Fire-and-forget POST of one frame. Drops (never throws/blocks) on any failure. */
55
+ publish(_workflowId: string, frame: InnerFrame): void;
56
+ /** Synchronous cached presence (stale-while-revalidate). Default 0 / fail-safe 0. */
57
+ subscriberCount(_workflowId: string): number;
58
+ /**
59
+ * 3d — synchronous cached `gateArmed` (same stale-while-revalidate presence GET
60
+ * that feeds subscriberCount; short-poll keeps it within ~1s). Default false /
61
+ * fail-safe false (a missed/failed presence read never spuriously engages the
62
+ * gate). The engagement check reads this together with subscriberCount.
63
+ */
64
+ gateArmed(_workflowId: string): boolean;
65
+ /** Fire a rate-limited background presence GET that updates the cache. */
66
+ private maybeRefreshPresence;
67
+ }