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.
- package/CLAUDE.md +253 -219
- package/LICENSE +21 -21
- package/README.md +293 -289
- package/assets/icon-dark.svg +9 -9
- package/assets/icon.svg +9 -9
- package/assets/logo-dark.svg +11 -11
- package/assets/logo-light.svg +11 -11
- package/dashboard/README.md +91 -91
- package/dashboard/dist/assets/{index-D6Xyje_n.js → index-jmYe6rmS.js} +2 -2
- package/dashboard/dist/assets/index-jmYe6rmS.js.map +1 -0
- package/dashboard/dist/index.html +20 -20
- package/dashboard/package.json +47 -47
- package/dist/activities/outbox.d.ts +30 -1
- package/dist/activities/outbox.js +96 -3
- package/dist/adapters/base.js +5 -0
- package/dist/adapters/copilot/adapter.js +12 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.js +7 -0
- package/dist/adapters/pi/adapter.d.ts +2 -0
- package/dist/adapters/pi/adapter.js +43 -0
- package/dist/adapters/pi/index.d.ts +16 -0
- package/dist/adapters/pi/index.js +10 -0
- package/dist/cli/global-wrapper.d.ts +19 -0
- package/dist/cli/global-wrapper.js +169 -0
- package/dist/cli/help-text.js +97 -97
- package/dist/cli/startup.js +11 -0
- package/dist/cli/upgrade-command.js +81 -81
- package/dist/cli.js +12 -0
- package/dist/client/core.js +9 -2
- package/dist/client/interface.d.ts +6 -0
- package/dist/config.d.ts +79 -0
- package/dist/config.js +74 -0
- package/dist/daemon.js +37 -1
- package/dist/http/aggregate.d.ts +22 -1
- package/dist/http/aggregate.js +41 -0
- package/dist/http/auth.d.ts +94 -8
- package/dist/http/auth.js +93 -9
- package/dist/http/body.d.ts +4 -1
- package/dist/http/body.js +6 -3
- package/dist/http/event-bus.js +1 -0
- package/dist/http/event-types.d.ts +34 -2
- package/dist/http/event-types.js +1 -0
- package/dist/http/gate-audit.d.ts +12 -0
- package/dist/http/gate-audit.js +95 -0
- package/dist/http/gate-registry.d.ts +167 -0
- package/dist/http/gate-registry.js +163 -0
- package/dist/http/gate-routes.d.ts +48 -0
- package/dist/http/gate-routes.js +102 -0
- package/dist/http/ingest-registry.d.ts +30 -0
- package/dist/http/ingest-registry.js +108 -0
- package/dist/http/inner-loop-routes.d.ts +66 -0
- package/dist/http/inner-loop-routes.js +182 -0
- package/dist/http/inner-loop.d.ts +92 -0
- package/dist/http/inner-loop.js +155 -0
- package/dist/http/server.d.ts +38 -3
- package/dist/http/server.js +211 -6
- package/dist/http/snapshot.d.ts +6 -0
- package/dist/http/snapshot.js +6 -0
- package/dist/pi/cue-pump.d.ts +61 -0
- package/dist/pi/cue-pump.js +95 -0
- package/dist/pi/extension.d.ts +45 -0
- package/dist/pi/extension.js +407 -0
- package/dist/pi/gate-client.d.ts +54 -0
- package/dist/pi/gate-client.js +136 -0
- package/dist/pi/headless.d.ts +85 -0
- package/dist/pi/headless.js +224 -0
- package/dist/pi/index.d.ts +28 -0
- package/dist/pi/index.js +43 -0
- package/dist/pi/inner-loop-client.d.ts +67 -0
- package/dist/pi/inner-loop-client.js +164 -0
- package/dist/pi/inner-loop-publisher.d.ts +187 -0
- package/dist/pi/inner-loop-publisher.js +236 -0
- package/dist/pi/lazy-proxy.d.ts +37 -0
- package/dist/pi/lazy-proxy.js +55 -0
- package/dist/pi/mission-control/actions.d.ts +48 -0
- package/dist/pi/mission-control/actions.js +98 -0
- package/dist/pi/mission-control/board.d.ts +53 -0
- package/dist/pi/mission-control/board.js +104 -0
- package/dist/pi/mission-control/extension.d.ts +44 -0
- package/dist/pi/mission-control/extension.js +251 -0
- package/dist/pi/mission-control/index.d.ts +15 -0
- package/dist/pi/mission-control/index.js +32 -0
- package/dist/pi/mission-control/inner-tail.d.ts +48 -0
- package/dist/pi/mission-control/inner-tail.js +76 -0
- package/dist/pi/mission-control/pi-ui.d.ts +43 -0
- package/dist/pi/mission-control/pi-ui.js +10 -0
- package/dist/pi/mission-control/render.d.ts +6 -0
- package/dist/pi/mission-control/render.js +95 -0
- package/dist/pi/phase-driver.d.ts +74 -0
- package/dist/pi/phase-driver.js +122 -0
- package/dist/pi/pi-types.d.ts +208 -0
- package/dist/pi/pi-types.js +21 -0
- package/dist/pi/probe.d.ts +80 -0
- package/dist/pi/probe.js +154 -0
- package/dist/pi/render-tools.d.ts +17 -0
- package/dist/pi/render-tools.js +51 -0
- package/dist/pi/reset-pump.d.ts +47 -0
- package/dist/pi/reset-pump.js +85 -0
- package/dist/pi/tool-capability.d.ts +60 -0
- package/dist/pi/tool-capability.js +156 -0
- package/dist/pi/workflow-client.d.ts +158 -0
- package/dist/pi/workflow-client.js +289 -0
- package/dist/pi/zod-to-typebox.d.ts +74 -0
- package/dist/pi/zod-to-typebox.js +191 -0
- package/dist/scripts/verify-daemon-isolation-guard.js +24 -24
- package/dist/server-tools.d.ts +2 -0
- package/dist/server-tools.js +50 -46
- package/dist/server.js +4 -0
- package/dist/spawn.d.ts +55 -0
- package/dist/spawn.js +84 -12
- package/dist/tools/agent-types.d.ts +2 -2
- package/dist/tools/agent-types.js +22 -17
- package/dist/tools/attachment-info.d.ts +2 -2
- package/dist/tools/attachment-info.js +38 -33
- package/dist/tools/broadcast.d.ts +2 -2
- package/dist/tools/broadcast.js +69 -64
- package/dist/tools/cancel-stage.d.ts +2 -2
- package/dist/tools/cancel-stage.js +20 -15
- package/dist/tools/clear-state.d.ts +2 -2
- package/dist/tools/clear-state.js +25 -20
- package/dist/tools/coat-check-evict.d.ts +2 -2
- package/dist/tools/coat-check-evict.js +30 -25
- package/dist/tools/coat-check-get.d.ts +2 -2
- package/dist/tools/coat-check-get.js +39 -34
- package/dist/tools/coat-check-list.d.ts +2 -2
- package/dist/tools/coat-check-list.js +48 -43
- package/dist/tools/coat-check-put.d.ts +2 -2
- package/dist/tools/coat-check-put.js +41 -36
- package/dist/tools/cue.d.ts +2 -2
- package/dist/tools/cue.js +57 -52
- package/dist/tools/descriptor.d.ts +72 -0
- package/dist/tools/descriptor.js +39 -0
- package/dist/tools/destroy.d.ts +2 -2
- package/dist/tools/destroy.js +153 -148
- package/dist/tools/ensemble.d.ts +2 -2
- package/dist/tools/ensemble.js +71 -66
- package/dist/tools/evaluate-gate.d.ts +2 -2
- package/dist/tools/evaluate-gate.js +33 -27
- package/dist/tools/fetch-state.d.ts +2 -2
- package/dist/tools/fetch-state.js +43 -38
- package/dist/tools/gates.d.ts +2 -2
- package/dist/tools/gates.js +39 -34
- package/dist/tools/hosts.d.ts +2 -2
- package/dist/tools/hosts.js +25 -20
- package/dist/tools/listen.d.ts +2 -2
- package/dist/tools/listen.js +23 -18
- package/dist/tools/load-lineup.d.ts +2 -2
- package/dist/tools/load-lineup.js +324 -319
- package/dist/tools/migrate.d.ts +2 -2
- package/dist/tools/migrate.js +45 -40
- package/dist/tools/pause.d.ts +2 -2
- package/dist/tools/pause.js +34 -29
- package/dist/tools/play.d.ts +2 -2
- package/dist/tools/play.js +53 -48
- package/dist/tools/quality-gate.d.ts +2 -2
- package/dist/tools/quality-gate.js +26 -21
- package/dist/tools/recall.d.ts +2 -2
- package/dist/tools/recall.js +32 -27
- package/dist/tools/recruit.d.ts +2 -2
- package/dist/tools/recruit.js +325 -256
- package/dist/tools/release.d.ts +2 -2
- package/dist/tools/release.js +85 -80
- package/dist/tools/report.d.ts +2 -2
- package/dist/tools/report.js +28 -23
- package/dist/tools/reset.d.ts +3 -0
- package/dist/tools/reset.js +51 -0
- package/dist/tools/restart.d.ts +2 -2
- package/dist/tools/restart.js +51 -46
- package/dist/tools/restore.d.ts +2 -2
- package/dist/tools/restore.js +76 -71
- package/dist/tools/save-lineup.d.ts +2 -2
- package/dist/tools/save-lineup.js +32 -27
- package/dist/tools/save-state.d.ts +2 -2
- package/dist/tools/save-state.js +43 -38
- package/dist/tools/schedule.d.ts +2 -2
- package/dist/tools/schedule.js +133 -128
- package/dist/tools/schedules.d.ts +2 -2
- package/dist/tools/schedules.js +41 -36
- package/dist/tools/set-ensemble-description.d.ts +2 -2
- package/dist/tools/set-ensemble-description.js +26 -21
- package/dist/tools/set-name.d.ts +2 -2
- package/dist/tools/set-name.js +38 -33
- package/dist/tools/set-part.d.ts +2 -2
- package/dist/tools/set-part.js +20 -15
- package/dist/tools/shutdown.d.ts +2 -2
- package/dist/tools/shutdown.js +39 -34
- package/dist/tools/stage.d.ts +2 -2
- package/dist/tools/stage.js +28 -23
- package/dist/tools/stages.d.ts +2 -2
- package/dist/tools/stages.js +36 -31
- package/dist/tools/unschedule.d.ts +2 -2
- package/dist/tools/unschedule.js +30 -25
- package/dist/tools/who-am-i.d.ts +2 -2
- package/dist/tools/who-am-i.js +36 -31
- package/dist/tools/worktree.d.ts +2 -2
- package/dist/tools/worktree.js +134 -129
- package/dist/tui/index.js +6 -6
- package/dist/types.d.ts +47 -2
- package/dist/types.js +1 -1
- package/dist/utils/default-part.js +1 -0
- package/dist/utils/grpc-shutdown-guard.d.ts +52 -0
- package/dist/utils/grpc-shutdown-guard.js +88 -0
- package/dist/utils/sdk-probe.d.ts +23 -0
- package/dist/utils/sdk-probe.js +46 -7
- package/dist/worker.d.ts +3 -1
- package/dist/worker.js +6 -2
- package/dist/workflows/session.js +70 -2
- package/dist/workflows/signals.d.ts +32 -2
- package/dist/workflows/signals.js +25 -2
- package/examples/agents/tempo-composer.md +56 -56
- package/examples/agents/tempo-conductor.md +117 -117
- package/examples/agents/tempo-critic.md +73 -73
- package/examples/agents/tempo-improv.md +74 -74
- package/examples/agents/tempo-liner.md +75 -75
- package/examples/agents/tempo-roadie.md +61 -61
- package/examples/agents/tempo-soloist.md +71 -71
- package/examples/agents/tempo-tuner.md +94 -94
- package/examples/ensembles/tempo-big-band.yaml +146 -146
- package/examples/ensembles/tempo-dev-team.yaml +58 -58
- package/examples/ensembles/tempo-headless-jam.yaml +77 -77
- package/examples/ensembles/tempo-jam-session.yaml +41 -41
- package/examples/ensembles/tempo-mock-jam.yaml +79 -79
- package/examples/ensembles/tempo-review-squad.yaml +32 -32
- package/package.json +176 -173
- package/packaging/launchd/com.agent.tempo.plist +46 -46
- package/packaging/systemd/agent-tempo.service +32 -32
- package/packaging/windows/install-task.ps1 +71 -71
- package/scenarios/conductor-recruit-mock.yaml +33 -33
- package/scenarios/echo-roundtrip.yaml +15 -15
- package/scenarios/multi-player-handoff.yaml +38 -38
- package/scenarios/recruit-cascade.yaml +38 -38
- package/scenarios/two-player-conversation.yaml +33 -33
- package/workflow-bundle.js +97 -6
- package/dashboard/dist/assets/index-D6Xyje_n.js.map +0 -1
- package/dist/activities/claude-stop.d.ts +0 -21
- package/dist/activities/claude-stop.js +0 -94
- package/dist/channel.d.ts +0 -3
- package/dist/channel.js +0 -48
- package/dist/copilot-bridge.d.ts +0 -22
- package/dist/copilot-bridge.js +0 -565
- package/dist/scripts/258-spotcheck.js +0 -303
- package/dist/tools/detach.d.ts +0 -4
- package/dist/tools/detach.js +0 -45
- package/dist/tools/encore.d.ts +0 -4
- package/dist/tools/encore.js +0 -31
- package/dist/tools/helpers.d.ts +0 -21
- package/dist/tools/helpers.js +0 -25
- package/dist/tools/pause-ensemble.d.ts +0 -4
- package/dist/tools/pause-ensemble.js +0 -58
- package/dist/tools/resume-ensemble.d.ts +0 -4
- package/dist/tools/resume-ensemble.js +0 -79
- package/dist/tools/stop.d.ts +0 -4
- package/dist/tools/stop.js +0 -29
- package/dist/tui/client.d.ts +0 -6
- package/dist/tui/client.js +0 -9
- package/dist/tui/components/ActivityLog.d.ts +0 -16
- package/dist/tui/components/ActivityLog.js +0 -36
- package/dist/tui/components/CommandOverlay.d.ts +0 -15
- package/dist/tui/components/CommandOverlay.js +0 -34
- package/dist/tui/components/ConductorChat.d.ts +0 -16
- package/dist/tui/components/ConductorChat.js +0 -32
- package/dist/tui/components/EnsembleListView.d.ts +0 -14
- package/dist/tui/components/EnsembleListView.js +0 -32
- package/dist/tui/components/EnsemblePanel.d.ts +0 -12
- package/dist/tui/components/EnsemblePanel.js +0 -40
- package/dist/tui/components/InputBar.d.ts +0 -13
- package/dist/tui/components/InputBar.js +0 -58
- package/dist/tui/components/ScheduleOverlay.d.ts +0 -13
- package/dist/tui/components/ScheduleOverlay.js +0 -113
- package/dist/tui/components/TopBar.d.ts +0 -12
- package/dist/tui/components/TopBar.js +0 -15
- package/dist/tui/core-api.d.ts +0 -26
- package/dist/tui/core-api.js +0 -67
- package/dist/tui/hooks/useEnsembleDiscovery.d.ts +0 -3
- package/dist/tui/hooks/useEnsembleDiscovery.js +0 -30
- package/dist/tui/hooks/useMaestroPoller.d.ts +0 -3
- package/dist/tui/hooks/useMaestroPoller.js +0 -36
- package/dist/tui/hooks/useSendCommand.d.ts +0 -7
- package/dist/tui/hooks/useSendCommand.js +0 -29
- package/dist/utils/bg-preflight.d.ts +0 -25
- package/dist/utils/bg-preflight.js +0 -154
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CuePump = void 0;
|
|
4
|
+
const DEFAULT_POLL_MS = 1_000;
|
|
5
|
+
const log = (...args) => {
|
|
6
|
+
// eslint-disable-next-line no-console
|
|
7
|
+
console.error('[agent-tempo:pi]', ...args);
|
|
8
|
+
};
|
|
9
|
+
class CuePump {
|
|
10
|
+
source;
|
|
11
|
+
resolveSession;
|
|
12
|
+
intervalMs;
|
|
13
|
+
timer = null;
|
|
14
|
+
draining = false;
|
|
15
|
+
constructor(opts) {
|
|
16
|
+
this.source = opts.source;
|
|
17
|
+
this.resolveSession = opts.resolveSession;
|
|
18
|
+
this.intervalMs = opts.intervalMs ?? DEFAULT_POLL_MS;
|
|
19
|
+
}
|
|
20
|
+
start() {
|
|
21
|
+
if (this.timer)
|
|
22
|
+
return;
|
|
23
|
+
this.timer = setInterval(() => {
|
|
24
|
+
this.tick().catch((err) => log('cue-pump tick failed:', err));
|
|
25
|
+
}, this.intervalMs);
|
|
26
|
+
if (typeof this.timer.unref === 'function')
|
|
27
|
+
this.timer.unref();
|
|
28
|
+
}
|
|
29
|
+
stop() {
|
|
30
|
+
if (this.timer) {
|
|
31
|
+
clearInterval(this.timer);
|
|
32
|
+
this.timer = null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* One poll cycle: fetch pending cues, inject each into the live session, ack
|
|
37
|
+
* the ones successfully injected. Re-entrancy guarded so a slow tick never
|
|
38
|
+
* overlaps the next interval.
|
|
39
|
+
*/
|
|
40
|
+
async tick() {
|
|
41
|
+
if (this.draining)
|
|
42
|
+
return;
|
|
43
|
+
this.draining = true;
|
|
44
|
+
try {
|
|
45
|
+
const pending = await this.source.fetchPending();
|
|
46
|
+
if (pending.length === 0)
|
|
47
|
+
return;
|
|
48
|
+
const session = this.resolveSession();
|
|
49
|
+
if (!session) {
|
|
50
|
+
// No live session yet — leave cues queued; next tick retries.
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const delivered = [];
|
|
54
|
+
for (const msg of pending) {
|
|
55
|
+
try {
|
|
56
|
+
await this.injectCue(session, msg);
|
|
57
|
+
delivered.push(msg.id);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
log(`failed to inject cue ${msg.id}:`, err);
|
|
61
|
+
// Stop on first failure — preserve ordering; retry next tick.
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
await this.source.ackDelivered(delivered);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
this.draining = false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Inject one cue into the live session (D10 — see file header). Operator cues
|
|
73
|
+
* `steer` (same-turn priority); peer cues `followUp` (queue). `triggerTurn` is
|
|
74
|
+
* always set: a no-op mid-turn, the required cold-idle wake otherwise.
|
|
75
|
+
*/
|
|
76
|
+
async injectCue(session, msg) {
|
|
77
|
+
const content = msg.from ? `[cue from ${msg.from}] ${msg.text}` : msg.text;
|
|
78
|
+
// LOAD-BEARING Pi-runtime invariant (D10) — confirmed sound through Pi 0.78.x
|
|
79
|
+
// (researcher-cited; a D6 "behaviors-to-revalidate-on-bump" item):
|
|
80
|
+
// peer cue = { deliverAs: 'followUp', triggerTurn: true } → QUEUES; drains
|
|
81
|
+
// when the agent goes idle, NEVER preempts a running turn. triggerTurn only
|
|
82
|
+
// wakes a cold-idle session (followUp alone won't start one); it is a no-op
|
|
83
|
+
// while a turn is in flight.
|
|
84
|
+
// operator cue = { deliverAs: 'steer', triggerTurn: true } → same-turn PRIORITY:
|
|
85
|
+
// injected after the current tool batch, before the next LLM call. NOT a hard
|
|
86
|
+
// mid-tool abort (only RPC abort / AbortSignal hard-interrupts a running tool).
|
|
87
|
+
// The guarantee this comment protects: a future Pi version MUST keep followUp
|
|
88
|
+
// non-interrupting AND triggerTurn a no-op-while-busy. If that regresses, peer
|
|
89
|
+
// cues silently become preemptions, defeating operator-vs-peer. Not unit-testable
|
|
90
|
+
// here (the session is mocked) — locked by researcher confirmation + the D6 Pi
|
|
91
|
+
// version floor (≥ #2860 + #5115) + a real-Pi mid-turn integration smoke.
|
|
92
|
+
await session.sendCustomMessage({ customType: 'cue', content, display: true }, { deliverAs: msg.isMaestro ? 'steer' : 'followUp', triggerTurn: true });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.CuePump = CuePump;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Client } from '@temporalio/client';
|
|
2
|
+
import { type Config } from '../config';
|
|
3
|
+
import type { ExtensionAPI, PiAgentSession } from './pi-types';
|
|
4
|
+
/** Runtime mode. Headless = recruited unsupervised player (MD-C gate active). */
|
|
5
|
+
export type PiExtensionMode = 'interactive' | 'headless';
|
|
6
|
+
export type PiToolAccess = 'restricted' | 'standard' | 'full';
|
|
7
|
+
export interface PiExtensionOptions {
|
|
8
|
+
/** Default `'interactive'`. Headless installs the MD-C tool_call gate. */
|
|
9
|
+
mode?: PiExtensionMode;
|
|
10
|
+
/** MD-C tool-class policy (headless only). Default `'restricted'`. */
|
|
11
|
+
toolAccess?: PiToolAccess;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Build the Pi extension factory. `mode='headless'` installs the MD-C tool_call
|
|
15
|
+
* gate; `mode='interactive'` (default) does not (the human owns their machine).
|
|
16
|
+
*/
|
|
17
|
+
export declare function createPiExtension(options?: PiExtensionOptions): (pi: ExtensionAPI) => void;
|
|
18
|
+
/**
|
|
19
|
+
* RELIABLE detach for the headless exit sequence (Phase 3a). Headless owns its
|
|
20
|
+
* exit loop, so — unlike interactive's best-effort `quit` path — it can AWAIT a
|
|
21
|
+
* clean detach before disposing the SDK session. Ordering (architect ruling):
|
|
22
|
+
* stopHeartbeat → requestDetach → adapterExited (all inside `wf.detach`) → unmap.
|
|
23
|
+
* The caller then calls `session.dispose()`; the dispose-fired `session_shutdown`
|
|
24
|
+
* finds no mapped runtime → no-op (avoids double-detach). Detaches every runtime
|
|
25
|
+
* in the process (headless = one player per process).
|
|
26
|
+
*/
|
|
27
|
+
export declare function detachAllPiRuntimesForExit(): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Headless-only: wire the live Pi SDK session onto a runtime so the cue pump can
|
|
30
|
+
* inject into it. The interactive CLI's `session_start` payload carries
|
|
31
|
+
* `session`, but the headless SDK's DEFAULT session_start payload does NOT (it's
|
|
32
|
+
* `{ type, reason }`) — so `attachOrRebind` sets `rt.session = null` and the cue
|
|
33
|
+
* pump's `resolveSession` returns null (every cue is dropped). The headless entry
|
|
34
|
+
* HOLDS the session from `createAgentSession`, so it calls this after
|
|
35
|
+
* `bindExtensions` (by which point the runtime exists + has claimed) to set it.
|
|
36
|
+
* (3a live smoke — devops.)
|
|
37
|
+
*/
|
|
38
|
+
export declare function setRuntimeSession(workflowId: string, session: PiAgentSession): void;
|
|
39
|
+
/** Override the Temporal connection factory (inject a fake Client). */
|
|
40
|
+
export declare function __setPiClientFactoryForTests(factory: (config: Config) => Promise<Client>): void;
|
|
41
|
+
/** Stop timers, clear the per-player runtime map + shared-client singletons + factory. */
|
|
42
|
+
export declare function __resetPiRuntimesForTests(): void;
|
|
43
|
+
/** Default export — interactive-mode extension (the human `pi` CLI entry). */
|
|
44
|
+
declare const piExtension: (pi: ExtensionAPI) => void;
|
|
45
|
+
export default piExtension;
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.createPiExtension = createPiExtension;
|
|
37
|
+
exports.detachAllPiRuntimesForExit = detachAllPiRuntimesForExit;
|
|
38
|
+
exports.setRuntimeSession = setRuntimeSession;
|
|
39
|
+
exports.__setPiClientFactoryForTests = __setPiClientFactoryForTests;
|
|
40
|
+
exports.__resetPiRuntimesForTests = __resetPiRuntimesForTests;
|
|
41
|
+
/**
|
|
42
|
+
* agent-tempo Pi extension — interactive (Phase 2) + headless (Phase 3a) runtime.
|
|
43
|
+
*
|
|
44
|
+
* createPiExtension({ mode, toolAccess }) → (pi: ExtensionAPI) => void
|
|
45
|
+
* export default = createPiExtension() (interactive)
|
|
46
|
+
*
|
|
47
|
+
* Registers the FULL agent-tempo tool surface natively on Pi (shared
|
|
48
|
+
* transport-neutral descriptors + `renderToPi`), drives the attachment phase
|
|
49
|
+
* from Pi lifecycle events, holds an attachment lease + heartbeat, and pumps
|
|
50
|
+
* cues into the live session. The SAME extension runs interactive (behind a
|
|
51
|
+
* human `pi` CLI) and headless (injected into `createAgentSession` by the daemon
|
|
52
|
+
* — Phase 3a); `mode` is the only behavioural discriminator (it gates the MD-C
|
|
53
|
+
* tool_call enforcement, which applies to unsupervised headless players only).
|
|
54
|
+
*
|
|
55
|
+
* ── Module-scope singleton (CRITICAL — researcher finding) ──
|
|
56
|
+
* Pi REBUILDS the extension instance on every SessionManager switch, so
|
|
57
|
+
* per-INSTANCE state does NOT survive. Everything that must survive — the
|
|
58
|
+
* Temporal `Client`, the fixed `workflowId`, the pinned handle, the heartbeat
|
|
59
|
+
* timer, the cue pump, the current-session pointer — lives in a MODULE-SCOPE
|
|
60
|
+
* singleton (`runtimes`, keyed by workflowId; one entry interactive, N for the
|
|
61
|
+
* headless daemon — D12a). The rebuilt instance RE-BINDS; it never recreates.
|
|
62
|
+
*
|
|
63
|
+
* ── Teardown (Option C — reason-discriminated) ──
|
|
64
|
+
* `session_shutdown` carries `reason` {quit|reload|new|resume|fork}. We detach
|
|
65
|
+
* ONLY on a clean `quit`; switch/unknown reasons → rebind (no detach). The
|
|
66
|
+
* `quit` detach is best-effort; the MD-A lease reaper is the permanent floor.
|
|
67
|
+
* Headless owns its exit sequence, so it uses {@link detachAllPiRuntimesForExit}
|
|
68
|
+
* for RELIABLE detach (await adapterExited) before disposing the SDK session.
|
|
69
|
+
*
|
|
70
|
+
* Determinism boundary: this module (and all of src/pi/) is CLIENT-SIDE only.
|
|
71
|
+
*/
|
|
72
|
+
const os = __importStar(require("os"));
|
|
73
|
+
const crypto = __importStar(require("crypto"));
|
|
74
|
+
const config_1 = require("../config");
|
|
75
|
+
const server_tools_1 = require("../server-tools");
|
|
76
|
+
const phase_driver_1 = require("./phase-driver");
|
|
77
|
+
const workflow_client_1 = require("./workflow-client");
|
|
78
|
+
const cue_pump_1 = require("./cue-pump");
|
|
79
|
+
const reset_pump_1 = require("./reset-pump");
|
|
80
|
+
const render_tools_1 = require("./render-tools");
|
|
81
|
+
const lazy_proxy_1 = require("./lazy-proxy");
|
|
82
|
+
const probe_1 = require("./probe");
|
|
83
|
+
const inner_loop_publisher_1 = require("./inner-loop-publisher");
|
|
84
|
+
const inner_loop_client_1 = require("./inner-loop-client");
|
|
85
|
+
const gate_client_1 = require("./gate-client");
|
|
86
|
+
const tool_capability_1 = require("./tool-capability");
|
|
87
|
+
const gate_registry_1 = require("../http/gate-registry");
|
|
88
|
+
const log = (...args) => {
|
|
89
|
+
// eslint-disable-next-line no-console
|
|
90
|
+
console.error('[agent-tempo:pi]', ...args);
|
|
91
|
+
};
|
|
92
|
+
const nowIso = () => new Date().toISOString();
|
|
93
|
+
const PI_AGENT_TYPE = 'claude'; // Pi is not yet a first-class AgentType.
|
|
94
|
+
// MD-C shell/exec tool-class membership is owned by `tool-capability.ts`
|
|
95
|
+
// (`classify(name) === 'exec'`, content signed off by tempo-security). F1
|
|
96
|
+
// import-refactor (3d): this REPLACES the former local `SHELL_TOOL_NAMES` set —
|
|
97
|
+
// the canonical EXEC_TOOLS set is a SUPERSET that also blocks
|
|
98
|
+
// powershell/pwsh/cmd/run, closing the gap the local list left open. Single
|
|
99
|
+
// source of truth: never re-declare a shell denylist here.
|
|
100
|
+
// ── Module-scope Temporal Client singleton (D12a: one Client per OS process) ──
|
|
101
|
+
let sharedClientPromise = null;
|
|
102
|
+
let connectedClient = null;
|
|
103
|
+
function getSharedClient(config) {
|
|
104
|
+
if (!sharedClientPromise) {
|
|
105
|
+
sharedClientPromise = workflow_client_1.PiWorkflowClient.connect(config)
|
|
106
|
+
.then((c) => { connectedClient = c; return c; })
|
|
107
|
+
.catch((err) => { sharedClientPromise = null; log('Temporal connect failed:', err); throw err; });
|
|
108
|
+
}
|
|
109
|
+
return sharedClientPromise;
|
|
110
|
+
}
|
|
111
|
+
/** Connection factory; overridable via `__setPiClientFactoryForTests`. */
|
|
112
|
+
let clientFactory = getSharedClient;
|
|
113
|
+
/** One runtime per player, keyed by fixed workflowId. Survives instance rebuilds. */
|
|
114
|
+
const runtimes = new Map();
|
|
115
|
+
/**
|
|
116
|
+
* Build the Pi extension factory. `mode='headless'` installs the MD-C tool_call
|
|
117
|
+
* gate; `mode='interactive'` (default) does not (the human owns their machine).
|
|
118
|
+
*/
|
|
119
|
+
function createPiExtension(options = {}) {
|
|
120
|
+
const mode = options.mode ?? 'interactive';
|
|
121
|
+
const toolAccess = options.toolAccess ?? 'restricted';
|
|
122
|
+
return function piExtension(pi) {
|
|
123
|
+
const probe = (0, probe_1.probePi)();
|
|
124
|
+
if (!probe.available)
|
|
125
|
+
log('WARNING:', probe.reason);
|
|
126
|
+
const config = (0, config_1.getConfig)();
|
|
127
|
+
const isConductor = process.env[config_1.ENV.CONDUCTOR] === '1' || process.env[config_1.ENV.CONDUCTOR] === 'true';
|
|
128
|
+
// Identity — FIXED workflowId for the process lifetime. `currentPlayerId` is
|
|
129
|
+
// the mutable DISPLAY id (set_name updates it); the workflowId never repoints.
|
|
130
|
+
let currentPlayerId = process.env[config_1.ENV.PLAYER_NAME] || `pi-${process.pid}`;
|
|
131
|
+
const workflowId = (0, config_1.sessionWorkflowId)(config.ensemble, currentPlayerId);
|
|
132
|
+
// 3c — the inner-loop URL + ingest token are keyed to the player's FIXED
|
|
133
|
+
// identity (the daemon minted the token for sessionWorkflowId(ensemble,
|
|
134
|
+
// <recruit name>)). `currentPlayerId` is mutable (set_name), so capture the
|
|
135
|
+
// original here — the publisher's HTTP client URL must match the workflowId.
|
|
136
|
+
const fixedPlayerId = currentPlayerId;
|
|
137
|
+
// Kick off (or reuse) the module-scope shared connection.
|
|
138
|
+
void clientFactory(config);
|
|
139
|
+
// ── D11 lazy proxies: resolve MODULE-SCOPE state per call (instance-independent) ──
|
|
140
|
+
const clientProxy = (0, lazy_proxy_1.createLazyProxy)(() => connectedClient, 'Temporal client');
|
|
141
|
+
const handleProxy = (0, lazy_proxy_1.createLazyProxy)(() => runtimes.get(workflowId)?.wf.handle ?? null, 'workflow handle');
|
|
142
|
+
// ── Register the FULL tool surface on THIS instance's `pi` ──
|
|
143
|
+
const toolOpts = {
|
|
144
|
+
client: clientProxy,
|
|
145
|
+
config,
|
|
146
|
+
getPlayerId: () => currentPlayerId,
|
|
147
|
+
setPlayerId: (id) => { currentPlayerId = id; },
|
|
148
|
+
handle: handleProxy,
|
|
149
|
+
workflowId,
|
|
150
|
+
ownAgentType: PI_AGENT_TYPE,
|
|
151
|
+
isConductor,
|
|
152
|
+
};
|
|
153
|
+
(0, render_tools_1.renderToPi)(pi, (0, server_tools_1.buildAllTempoTools)(toolOpts));
|
|
154
|
+
log(`registered tools (player=${currentPlayerId}, conductor=${isConductor}, mode=${mode})`);
|
|
155
|
+
// ── MD-C tool-access gate (HEADLESS ONLY) ──
|
|
156
|
+
// Interactive Pi = a human owns their machine → no gate. Headless = recruited
|
|
157
|
+
// unsupervised → MD-C governs tool access. TOOL-CLASS CHECK FIRST: shell/exec
|
|
158
|
+
// tools are HARD-BLOCKED at toolAccess='restricted' (the safe unsupervised
|
|
159
|
+
// default) regardless of any later gate logic. The supervised gate (3d) slots
|
|
160
|
+
// in AFTER this MD-C floor — for now, anything MD-C permits is allowed.
|
|
161
|
+
if (mode === 'headless') {
|
|
162
|
+
// 3d MD-G — the operator gate's two loopback clients, keyed to the FIXED
|
|
163
|
+
// player identity (matches the ingest token + workflowId). `gateInner`
|
|
164
|
+
// emits the gate_pending frame (the daemon's ingest side-effect registers
|
|
165
|
+
// the pending) and reports presence {subscribers, gateArmed}; `gateClient`
|
|
166
|
+
// polls the daemon for the operator's decision. Both no-op without the
|
|
167
|
+
// ingest token (so this is inert for a manually-launched headless Pi).
|
|
168
|
+
const gateInner = new inner_loop_client_1.InnerLoopHttpClient({ ensemble: config.ensemble, playerId: fixedPlayerId });
|
|
169
|
+
const gateClient = new gate_client_1.GateClient({ ensemble: config.ensemble, playerId: fixedPlayerId });
|
|
170
|
+
pi.on('tool_call', async (event, ctx) => {
|
|
171
|
+
const cls = (0, tool_capability_1.classify)(event.toolName);
|
|
172
|
+
// 1) MD-C tool-class FLOOR (fires FIRST). classify()==='exec' is the
|
|
173
|
+
// canonical EXEC set (F1) — a SUPERSET that hard-blocks
|
|
174
|
+
// powershell/pwsh/cmd/run at restricted. HARD-block, never gated.
|
|
175
|
+
if (cls === 'exec' && toolAccess === 'restricted') {
|
|
176
|
+
log(`MD-C: blocked '${event.toolName}' (toolAccess=restricted)`);
|
|
177
|
+
return {
|
|
178
|
+
block: true,
|
|
179
|
+
reason: `toolAccess=restricted: shell/exec tools are disabled for this headless Pi player`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
// 2) MD-G OPERATOR GATE — engage for any non-low-risk tool WHEN an
|
|
183
|
+
// operator is armed AND present (both read from the short-poll
|
|
184
|
+
// cached presence). low-risk bypasses; unknown→high-blast is gated-
|
|
185
|
+
// when-armed (R2). The await resolves on the operator's decision,
|
|
186
|
+
// the daemon's 45s auto-allow, ctx.signal cancel, or the bounded
|
|
187
|
+
// poll deadline — all FAIL-OPEN except an explicit deny.
|
|
188
|
+
if (cls !== 'low-risk' && gateInner.gateArmed(workflowId) && gateInner.subscriberCount(workflowId) > 0) {
|
|
189
|
+
const requestId = crypto.randomUUID();
|
|
190
|
+
gateInner.publish(workflowId, {
|
|
191
|
+
type: 'inner.gate_pending',
|
|
192
|
+
requestId,
|
|
193
|
+
tool: event.toolName,
|
|
194
|
+
argsSummary: (0, inner_loop_publisher_1.truncateSummary)(event.input, 2048),
|
|
195
|
+
classification: cls, // low-risk already returned above
|
|
196
|
+
timeoutMs: gate_registry_1.GATE_AUTO_ALLOW_MS,
|
|
197
|
+
ts: Date.now(),
|
|
198
|
+
});
|
|
199
|
+
const effect = await gateClient.awaitDecision(requestId, { signal: ctx?.signal });
|
|
200
|
+
if (effect === 'deny') {
|
|
201
|
+
log(`MD-G: operator DENIED '${event.toolName}' (req ${requestId})`);
|
|
202
|
+
return { block: true, reason: `operator denied ${event.toolName}` };
|
|
203
|
+
}
|
|
204
|
+
log(`MD-G: '${event.toolName}' permitted (req ${requestId})`);
|
|
205
|
+
return {};
|
|
206
|
+
}
|
|
207
|
+
// 3) not gated → permit.
|
|
208
|
+
return {};
|
|
209
|
+
});
|
|
210
|
+
log(`MD-C+MD-G tool gate active (mode=headless, toolAccess=${toolAccess})`);
|
|
211
|
+
}
|
|
212
|
+
/** Build session metadata from the (current) identity + host. */
|
|
213
|
+
function buildMetadata() {
|
|
214
|
+
return {
|
|
215
|
+
playerId: currentPlayerId,
|
|
216
|
+
ensemble: config.ensemble,
|
|
217
|
+
hostname: os.hostname(),
|
|
218
|
+
workDir: process.cwd(),
|
|
219
|
+
isConductor,
|
|
220
|
+
agentType: PI_AGENT_TYPE,
|
|
221
|
+
adapterId: 'pi',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/** Persist the active Pi conversation id to metadata.sessionId IF it changed (P2-5). */
|
|
225
|
+
async function refreshSessionId(rt, sessionId) {
|
|
226
|
+
if (!sessionId || sessionId === rt.lastSessionId)
|
|
227
|
+
return;
|
|
228
|
+
await rt.wf.updateSessionId(sessionId);
|
|
229
|
+
rt.lastSessionId = sessionId;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Get-or-create the runtime for this player. FIRST attach claims the lease +
|
|
233
|
+
* starts the heartbeat + cue pump. A subsequent `session_start` (instance
|
|
234
|
+
* rebuild) RE-BINDS the surviving runtime — session pointer only, no re-claim.
|
|
235
|
+
*/
|
|
236
|
+
async function attachOrRebind(payload) {
|
|
237
|
+
const existing = runtimes.get(workflowId);
|
|
238
|
+
if (existing) {
|
|
239
|
+
existing.session = payload.session ?? existing.session;
|
|
240
|
+
log(`re-bound ${currentPlayerId} (Pi instance rebuilt; lease intact)`);
|
|
241
|
+
return existing;
|
|
242
|
+
}
|
|
243
|
+
const client = await clientFactory(config);
|
|
244
|
+
const wf = new workflow_client_1.PiWorkflowClient({
|
|
245
|
+
client,
|
|
246
|
+
config,
|
|
247
|
+
metadata: buildMetadata(),
|
|
248
|
+
expectedAttachmentId: process.env[config_1.ENV.ATTACHMENT_ID] || undefined,
|
|
249
|
+
});
|
|
250
|
+
const driver = new phase_driver_1.PhaseDriver();
|
|
251
|
+
const pump = new cue_pump_1.CuePump({
|
|
252
|
+
source: wf,
|
|
253
|
+
resolveSession: () => runtimes.get(workflowId)?.session ?? null,
|
|
254
|
+
});
|
|
255
|
+
// 3c — inner-loop publisher + its loopback-HTTP sink. The client no-ops
|
|
256
|
+
// unless AGENT_TEMPO_INGEST_TOKEN is present (daemon-spawned headless
|
|
257
|
+
// players only), so interactive Pi gets Tier-1 coarse for free and zero
|
|
258
|
+
// Tier-2 forwarding. URL keyed to the FIXED playerId (matches workflowId).
|
|
259
|
+
const registry = new inner_loop_client_1.InnerLoopHttpClient({ ensemble: config.ensemble, playerId: fixedPlayerId });
|
|
260
|
+
const pub = new inner_loop_publisher_1.InnerLoopPublisher({ workflowId, registry });
|
|
261
|
+
// 3d D14 — reset poll-tick (sibling to the cue pump): polls pendingReset →
|
|
262
|
+
// session.newSession() clean-wipe + ack. resolveSession re-acquired each
|
|
263
|
+
// tick so a session switch never wipes a stale session.
|
|
264
|
+
const reset = new reset_pump_1.ResetPump({
|
|
265
|
+
source: wf,
|
|
266
|
+
resolveSession: () => runtimes.get(workflowId)?.session ?? null,
|
|
267
|
+
});
|
|
268
|
+
const rt = { workflowId, wf, driver, pump, pub, reset, session: payload.session ?? null };
|
|
269
|
+
runtimes.set(workflowId, rt);
|
|
270
|
+
await wf.ensureSessionWorkflow();
|
|
271
|
+
const result = driver.handle('session_start', payload, nowIso());
|
|
272
|
+
await wf.performAction(result.action); // claim → attached, starts heartbeat
|
|
273
|
+
pump.start();
|
|
274
|
+
// Start the publisher AFTER the claim (heartbeat is live → coarse samples
|
|
275
|
+
// have a delivery path) and wire its coarse state into the heartbeat. The
|
|
276
|
+
// bound method is wrapped so `this` survives the provider call.
|
|
277
|
+
pub.start(pi);
|
|
278
|
+
wf.setCoarseProvider(() => pub.getCoarseState());
|
|
279
|
+
reset.start(); // 3d D14 — begin polling for pending resets
|
|
280
|
+
log(`attached ${currentPlayerId} (wf ${workflowId})`);
|
|
281
|
+
return rt;
|
|
282
|
+
}
|
|
283
|
+
// ── Lifecycle: session_start → first attach OR re-bind ──
|
|
284
|
+
pi.on('session_start', async (payload) => {
|
|
285
|
+
try {
|
|
286
|
+
const rt = await attachOrRebind(payload);
|
|
287
|
+
await refreshSessionId(rt, rt.session?.id);
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
log('session_start wiring failed:', err);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
// ── Lifecycle: phase-affecting events ──
|
|
294
|
+
for (const event of ['agent_start', 'agent_end']) {
|
|
295
|
+
pi.on(event, async (payload) => {
|
|
296
|
+
const rt = runtimes.get(workflowId);
|
|
297
|
+
if (!rt)
|
|
298
|
+
return;
|
|
299
|
+
if (payload.session)
|
|
300
|
+
rt.session = payload.session;
|
|
301
|
+
const result = rt.driver.handle(event, payload, nowIso());
|
|
302
|
+
try {
|
|
303
|
+
await rt.wf.performAction(result.action);
|
|
304
|
+
if (event === 'agent_start')
|
|
305
|
+
await refreshSessionId(rt, rt.session?.id);
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
log(`${event} → ${result.action.kind} failed:`, err);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
// ── Lifecycle: activity-only events (NEVER drive phase) ──
|
|
313
|
+
for (const event of [
|
|
314
|
+
'turn_start', 'turn_end', 'tool_execution_start', 'tool_execution_end',
|
|
315
|
+
]) {
|
|
316
|
+
pi.on(event, (payload) => {
|
|
317
|
+
const rt = runtimes.get(workflowId);
|
|
318
|
+
if (!rt)
|
|
319
|
+
return;
|
|
320
|
+
if (payload.session)
|
|
321
|
+
rt.session = payload.session;
|
|
322
|
+
rt.driver.handle(event, payload, nowIso());
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
// ── Lifecycle: session_shutdown → Option C (reason-discriminated teardown) ──
|
|
326
|
+
pi.on('session_shutdown', async (payload) => {
|
|
327
|
+
const rt = runtimes.get(workflowId);
|
|
328
|
+
if (!rt)
|
|
329
|
+
return;
|
|
330
|
+
rt.session = null; // switch gap: cue pump stops injecting (dodges Pi #2860)
|
|
331
|
+
if (payload.reason === 'quit') {
|
|
332
|
+
rt.pub.stop(); // 3c — stop observing + flush the trailing coalesce buffer
|
|
333
|
+
rt.reset.stop(); // 3d — stop the reset poll
|
|
334
|
+
try {
|
|
335
|
+
await rt.wf.detach('agent-exited'); // requestDetach + adapterExited + stopHeartbeat
|
|
336
|
+
runtimes.delete(workflowId);
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
log('quit detach (best-effort) failed — reaper will backstop:', err);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* RELIABLE detach for the headless exit sequence (Phase 3a). Headless owns its
|
|
347
|
+
* exit loop, so — unlike interactive's best-effort `quit` path — it can AWAIT a
|
|
348
|
+
* clean detach before disposing the SDK session. Ordering (architect ruling):
|
|
349
|
+
* stopHeartbeat → requestDetach → adapterExited (all inside `wf.detach`) → unmap.
|
|
350
|
+
* The caller then calls `session.dispose()`; the dispose-fired `session_shutdown`
|
|
351
|
+
* finds no mapped runtime → no-op (avoids double-detach). Detaches every runtime
|
|
352
|
+
* in the process (headless = one player per process).
|
|
353
|
+
*/
|
|
354
|
+
async function detachAllPiRuntimesForExit() {
|
|
355
|
+
for (const rt of runtimes.values()) {
|
|
356
|
+
rt.pub.stop(); // 3c — stop the inner-loop publisher before detaching
|
|
357
|
+
rt.reset.stop(); // 3d — stop the reset poll before detaching
|
|
358
|
+
try {
|
|
359
|
+
await rt.wf.detach('agent-exited');
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
log('headless detach failed (reaper will backstop):', err);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
runtimes.clear();
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Headless-only: wire the live Pi SDK session onto a runtime so the cue pump can
|
|
369
|
+
* inject into it. The interactive CLI's `session_start` payload carries
|
|
370
|
+
* `session`, but the headless SDK's DEFAULT session_start payload does NOT (it's
|
|
371
|
+
* `{ type, reason }`) — so `attachOrRebind` sets `rt.session = null` and the cue
|
|
372
|
+
* pump's `resolveSession` returns null (every cue is dropped). The headless entry
|
|
373
|
+
* HOLDS the session from `createAgentSession`, so it calls this after
|
|
374
|
+
* `bindExtensions` (by which point the runtime exists + has claimed) to set it.
|
|
375
|
+
* (3a live smoke — devops.)
|
|
376
|
+
*/
|
|
377
|
+
function setRuntimeSession(workflowId, session) {
|
|
378
|
+
const rt = runtimes.get(workflowId);
|
|
379
|
+
if (rt) {
|
|
380
|
+
rt.session = session;
|
|
381
|
+
log(`headless session wired to runtime (wf ${workflowId})`);
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
log(`setRuntimeSession: no runtime for ${workflowId} yet (session_start may not have fired)`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// ── Test-only hooks (ADR 0006 `__<verb><Noun>ForTests` convention) ──
|
|
388
|
+
/** Override the Temporal connection factory (inject a fake Client). */
|
|
389
|
+
function __setPiClientFactoryForTests(factory) {
|
|
390
|
+
clientFactory = factory;
|
|
391
|
+
}
|
|
392
|
+
/** Stop timers, clear the per-player runtime map + shared-client singletons + factory. */
|
|
393
|
+
function __resetPiRuntimesForTests() {
|
|
394
|
+
for (const rt of runtimes.values()) {
|
|
395
|
+
rt.pub.stop();
|
|
396
|
+
rt.reset.stop();
|
|
397
|
+
rt.pump.stop();
|
|
398
|
+
rt.wf.stopHeartbeat();
|
|
399
|
+
}
|
|
400
|
+
runtimes.clear();
|
|
401
|
+
sharedClientPromise = null;
|
|
402
|
+
connectedClient = null;
|
|
403
|
+
clientFactory = getSharedClient;
|
|
404
|
+
}
|
|
405
|
+
/** Default export — interactive-mode extension (the human `pi` CLI entry). */
|
|
406
|
+
const piExtension = createPiExtension();
|
|
407
|
+
exports.default = piExtension;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Env var carrying the per-player ingest token (threaded in at spawn, shared w/ inner-loop). */
|
|
2
|
+
export declare const INGEST_TOKEN_ENV = "AGENT_TEMPO_INGEST_TOKEN";
|
|
3
|
+
/** What the handler does with the result. */
|
|
4
|
+
export type GateEffect = 'allow' | 'deny';
|
|
5
|
+
/** Minimal `fetch` shape (injectable for tests) — same contract as the inner-loop client. */
|
|
6
|
+
export type GateFetch = (url: string, init: {
|
|
7
|
+
method: string;
|
|
8
|
+
headers: Record<string, string>;
|
|
9
|
+
}) => Promise<{
|
|
10
|
+
status: number;
|
|
11
|
+
json(): Promise<unknown>;
|
|
12
|
+
}>;
|
|
13
|
+
export interface GateClientOptions {
|
|
14
|
+
ensemble: string;
|
|
15
|
+
playerId: string;
|
|
16
|
+
ingestToken?: string;
|
|
17
|
+
readPort?: () => number | null;
|
|
18
|
+
fetchFn?: GateFetch;
|
|
19
|
+
pollIntervalMs?: number;
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
now?: () => number;
|
|
22
|
+
/** Cancellable wait (tests inject a synchronous/controllable one). */
|
|
23
|
+
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Loopback poll-bridge to the daemon gate. Construct one per headless player.
|
|
27
|
+
* The `awaitDecision` poll is the only public surface used by the engagement path.
|
|
28
|
+
*/
|
|
29
|
+
export declare class GateClient {
|
|
30
|
+
private readonly ensemble;
|
|
31
|
+
private readonly playerId;
|
|
32
|
+
private readonly ingestToken;
|
|
33
|
+
private readonly readPort;
|
|
34
|
+
private readonly fetchFn;
|
|
35
|
+
private readonly pollIntervalMs;
|
|
36
|
+
private readonly timeoutMs;
|
|
37
|
+
private readonly now;
|
|
38
|
+
private readonly sleep;
|
|
39
|
+
constructor(opts: GateClientOptions);
|
|
40
|
+
private get enabled();
|
|
41
|
+
private resolutionUrl;
|
|
42
|
+
/** One poll. Returns the effect on a resolved answer, or null to keep polling. */
|
|
43
|
+
private pollOnce;
|
|
44
|
+
/**
|
|
45
|
+
* Poll the daemon for the operator's decision on `requestId`, blocking until
|
|
46
|
+
* resolved / timeout / abort. FAIL-OPEN: `allow` unless the operator explicitly
|
|
47
|
+
* denied. Without a token/transport (e.g. interactive Pi, daemon HTTP off) →
|
|
48
|
+
* immediate `allow` (the gate is a daemon-mediated feature).
|
|
49
|
+
*/
|
|
50
|
+
awaitDecision(requestId: string, opts?: {
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
timeoutMs?: number;
|
|
53
|
+
}): Promise<GateEffect>;
|
|
54
|
+
}
|