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.
- package/CLAUDE.md +39 -5
- package/README.md +6 -2
- 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 +1 -1
- package/dashboard/package.json +1 -1
- 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/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/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 +32 -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 +250 -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 +88 -0
- package/dist/pi/mission-control/board.js +141 -0
- package/dist/pi/mission-control/extension.d.ts +51 -0
- package/dist/pi/mission-control/extension.js +330 -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 +98 -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 +222 -0
- package/dist/pi/pi-types.js +21 -0
- package/dist/pi/probe.d.ts +99 -0
- package/dist/pi/probe.js +179 -0
- package/dist/pi/render-tools.d.ts +17 -0
- package/dist/pi/render-tools.js +56 -0
- package/dist/pi/reset-pump.d.ts +47 -0
- package/dist/pi/reset-pump.js +85 -0
- package/dist/pi/session-seed.d.ts +74 -0
- package/dist/pi/session-seed.js +103 -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/server-tools.d.ts +2 -0
- package/dist/server-tools.js +50 -46
- package/dist/spawn.d.ts +55 -0
- package/dist/spawn.js +72 -0
- 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 +29 -24
- package/dist/tools/coat-check-get.d.ts +2 -2
- package/dist/tools/coat-check-get.js +38 -33
- 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 +38 -33
- 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 +42 -37
- 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 +340 -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 +31 -26
- 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/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/package.json +4 -1
- package/workflow-bundle.js +97 -6
- package/dashboard/dist/assets/index-D6Xyje_n.js.map +0 -1
- package/dist/tools/helpers.d.ts +0 -21
- package/dist/tools/helpers.js +0 -25
package/dist/tools/cue.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.formatDetachedDeliveryError = formatDetachedDeliveryError;
|
|
4
|
-
exports.
|
|
4
|
+
exports.buildCueTool = buildCueTool;
|
|
5
5
|
exports.formatUnknownPlayerError = formatUnknownPlayerError;
|
|
6
6
|
exports.findClosestPlayers = findClosestPlayers;
|
|
7
7
|
exports.levenshtein = levenshtein;
|
|
@@ -10,7 +10,7 @@ const resolve_1 = require("./resolve");
|
|
|
10
10
|
const resolve_2 = require("../activities/resolve");
|
|
11
11
|
const signals_1 = require("../workflows/signals");
|
|
12
12
|
const query_timeout_1 = require("../utils/query-timeout");
|
|
13
|
-
const
|
|
13
|
+
const descriptor_1 = require("./descriptor");
|
|
14
14
|
const validation_1 = require("../utils/validation");
|
|
15
15
|
/**
|
|
16
16
|
* Max Levenshtein distance for a fuzzy-match candidate to surface in the
|
|
@@ -55,60 +55,65 @@ function formatDetachedDeliveryError(playerId, phase) {
|
|
|
55
55
|
`(2) 'restart' to re-attach the session and retry the cue, ` +
|
|
56
56
|
`(3) the workflow inbox queues the signal and auto-delivers on re-attach.`);
|
|
57
57
|
}
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
// actionable error — active-player list + fuzzy-match suggestion
|
|
73
|
-
// + Agent-tool surface hint. We pay the extra `scanEnsembleSessions`
|
|
74
|
-
// round-trip ONLY on the error path, so the success path is
|
|
75
|
-
// unchanged.
|
|
76
|
-
const sessions = await (0, resolve_2.scanEnsembleSessions)(client, config.ensemble);
|
|
77
|
-
const activePlayers = sessions.map((s) => s.playerId).sort();
|
|
78
|
-
return (0, helpers_1.fail)(formatUnknownPlayerError(playerId, activePlayers));
|
|
79
|
-
}
|
|
80
|
-
// #562: phase pre-flight. Resolves the gap where `cue` to a
|
|
81
|
-
// detached/gone player returns "Message sent" — wire-truthful (the
|
|
82
|
-
// signal IS delivered to the workflow inbox), but operator-
|
|
83
|
-
// misleading because no live adapter surfaces the message.
|
|
84
|
-
let phase;
|
|
58
|
+
function buildCueTool(client, config, getPlayerId, handle) {
|
|
59
|
+
return {
|
|
60
|
+
name: 'cue',
|
|
61
|
+
description: 'Send a message to another Claude Code session by player name. Delivered instantly via Temporal signal. For content larger than ~100 KB, use `coat_check_put` to stash the body and pass the returned ticket via `attachmentTicket` — the cue body itself should carry a short summary the recipient can act on without fetching.',
|
|
62
|
+
params: {
|
|
63
|
+
playerId: zod_1.z.string().max(validation_1.PLAYER_NAME_MAX).describe('The player name of the target session'),
|
|
64
|
+
message: zod_1.z.string().max(validation_1.MESSAGE_MAX).describe('The message to send'),
|
|
65
|
+
attachmentTicket: zod_1.z.string().regex(validation_1.COAT_CHECK_TICKET_REGEX).max(validation_1.COAT_CHECK_TICKET_MAX).optional().describe('Optional coat-check ticket (#318). Reference content stashed via `coat_check_put`; the receiver sees the ticket on their `recall` message and can pull the body via `coat_check_get`. Backward-compatible — omit for normal cues.'),
|
|
66
|
+
},
|
|
67
|
+
handler: async (args) => {
|
|
68
|
+
const { playerId, message, attachmentTicket } = args;
|
|
69
|
+
const nameError = (0, validation_1.validatePlayerName)(playerId);
|
|
70
|
+
if (nameError)
|
|
71
|
+
return (0, descriptor_1.fail)(nameError);
|
|
85
72
|
try {
|
|
86
|
-
const
|
|
87
|
-
|
|
73
|
+
const resolved = await (0, resolve_1.resolveSession)(client, config.ensemble, playerId);
|
|
74
|
+
if (!resolved) {
|
|
75
|
+
// #560: replace the generic "no active session found" with an
|
|
76
|
+
// actionable error — active-player list + fuzzy-match suggestion
|
|
77
|
+
// + Agent-tool surface hint. We pay the extra `scanEnsembleSessions`
|
|
78
|
+
// round-trip ONLY on the error path, so the success path is
|
|
79
|
+
// unchanged.
|
|
80
|
+
const sessions = await (0, resolve_2.scanEnsembleSessions)(client, config.ensemble);
|
|
81
|
+
const activePlayers = sessions.map((s) => s.playerId).sort();
|
|
82
|
+
return (0, descriptor_1.fail)(formatUnknownPlayerError(playerId, activePlayers));
|
|
83
|
+
}
|
|
84
|
+
// #562: phase pre-flight. Resolves the gap where `cue` to a
|
|
85
|
+
// detached/gone player returns "Message sent" — wire-truthful (the
|
|
86
|
+
// signal IS delivered to the workflow inbox), but operator-
|
|
87
|
+
// misleading because no live adapter surfaces the message.
|
|
88
|
+
let phase;
|
|
89
|
+
try {
|
|
90
|
+
const info = await (0, query_timeout_1.queryHandleWithTimeout)(resolved, signals_1.attachmentInfoQuery, { timeoutMs: CUE_PHASE_PREFLIGHT_TIMEOUT_MS });
|
|
91
|
+
phase = info.phase;
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
// Query timed out or threw — workflow may be wedged. Don't
|
|
95
|
+
// penalize the operator: fall through to best-effort submit.
|
|
96
|
+
// Auto-redelivery on re-attach still applies. Stderr log keeps
|
|
97
|
+
// the observability trail without surfacing noise to the user.
|
|
98
|
+
console.error(`[agent-tempo:cue] phase pre-flight failed for "${playerId}" — proceeding best-effort:`, err instanceof Error ? err.message : err);
|
|
99
|
+
}
|
|
100
|
+
if (phase && UNDELIVERABLE_PHASES.has(phase)) {
|
|
101
|
+
return (0, descriptor_1.fail)(formatDetachedDeliveryError(playerId, phase));
|
|
102
|
+
}
|
|
103
|
+
const entry = {
|
|
104
|
+
type: 'cue',
|
|
105
|
+
targetPlayerId: playerId,
|
|
106
|
+
message,
|
|
107
|
+
...(attachmentTicket !== undefined ? { attachmentTicket } : {}),
|
|
108
|
+
};
|
|
109
|
+
const entryId = await handle.executeUpdate(signals_1.submitOutboxUpdate, { args: [entry] });
|
|
110
|
+
return (0, descriptor_1.ok)(`Message sent to ${playerId}. (outbox: ${entryId})`);
|
|
88
111
|
}
|
|
89
112
|
catch (err) {
|
|
90
|
-
|
|
91
|
-
// penalize the operator: fall through to best-effort submit.
|
|
92
|
-
// Auto-redelivery on re-attach still applies. Stderr log keeps
|
|
93
|
-
// the observability trail without surfacing noise to the user.
|
|
94
|
-
console.error(`[agent-tempo:cue] phase pre-flight failed for "${playerId}" — proceeding best-effort:`, err instanceof Error ? err.message : err);
|
|
95
|
-
}
|
|
96
|
-
if (phase && UNDELIVERABLE_PHASES.has(phase)) {
|
|
97
|
-
return (0, helpers_1.fail)(formatDetachedDeliveryError(playerId, phase));
|
|
113
|
+
return (0, descriptor_1.fail)(`Failed to send message to ${playerId}: ${(0, descriptor_1.formatError)(err)}`);
|
|
98
114
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
targetPlayerId: playerId,
|
|
102
|
-
message,
|
|
103
|
-
...(attachmentTicket !== undefined ? { attachmentTicket } : {}),
|
|
104
|
-
};
|
|
105
|
-
const entryId = await handle.executeUpdate(signals_1.submitOutboxUpdate, { args: [entry] });
|
|
106
|
-
return (0, helpers_1.ok)(`Message sent to ${playerId}. (outbox: ${entryId})`);
|
|
107
|
-
}
|
|
108
|
-
catch (err) {
|
|
109
|
-
return (0, helpers_1.fail)(`Failed to send message to ${playerId}: ${(0, helpers_1.formatError)(err)}`);
|
|
110
|
-
}
|
|
111
|
-
});
|
|
115
|
+
},
|
|
116
|
+
};
|
|
112
117
|
}
|
|
113
118
|
/**
|
|
114
119
|
* Format the cue tool's "unknown player" error with actionable suggestions.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport-neutral tool descriptor (MD-B seam, Phase 1).
|
|
3
|
+
*
|
|
4
|
+
* agent-tempo tools used to be defined directly against an `McpServer`
|
|
5
|
+
* (`defineTool` → `server.tool(...)`). MD-B splits that into:
|
|
6
|
+
*
|
|
7
|
+
* 1. A neutral **descriptor** — `{ name, description, params, handler }` —
|
|
8
|
+
* with NO MCP or Pi coupling. zod is the single source of truth for
|
|
9
|
+
* params; the handler returns neutral `{ text, isError? }`.
|
|
10
|
+
* 2. Per-front-end **renderers** — `renderToMcp` (here) and `renderToPi`
|
|
11
|
+
* (`src/pi/render-tools.ts`). MCP consumes the zod shape raw; Pi derives
|
|
12
|
+
* a TypeBox schema from it. No dual-define, no drift.
|
|
13
|
+
*
|
|
14
|
+
* Backward-compat is load-bearing: `renderToMcp` reproduces the EXACT output
|
|
15
|
+
* of the old `defineTool` so `src/server.ts` and the claude-api in-process MCP
|
|
16
|
+
* bridge keep byte-identical behavior. `registerAllTempoTools` stays a thin
|
|
17
|
+
* wrapper over `renderToMcp(server, buildAllTempoTools(opts))` — its callers
|
|
18
|
+
* never change.
|
|
19
|
+
*
|
|
20
|
+
* Determinism note: this module is client-side (src/tools). `src/pi/` imports
|
|
21
|
+
* the descriptor type FROM here; this module never imports `src/pi/`.
|
|
22
|
+
*
|
|
23
|
+
* Design reference: architect's MD-B implementation spec (Phase 1).
|
|
24
|
+
*/
|
|
25
|
+
import type { z } from 'zod';
|
|
26
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
27
|
+
/**
|
|
28
|
+
* Neutral tool result. Every current tool returns a single text block via
|
|
29
|
+
* `ok()` / `fail()`. Richer multi-block / structured output is an ADDITIVE
|
|
30
|
+
* future extension (e.g. an optional `data?`) — intentionally not modeled now.
|
|
31
|
+
*/
|
|
32
|
+
export interface TempoToolResult {
|
|
33
|
+
text: string;
|
|
34
|
+
isError?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A transport-neutral tool definition. Each tool module exports a
|
|
38
|
+
* `build<X>Tool(...deps): TempoToolDescriptor` factory that closes over its
|
|
39
|
+
* existing dependencies (client, config, handle, getPlayerId, …) exactly as
|
|
40
|
+
* the old `register<X>Tool` did.
|
|
41
|
+
*/
|
|
42
|
+
export interface TempoToolDescriptor {
|
|
43
|
+
name: string;
|
|
44
|
+
description: string;
|
|
45
|
+
/**
|
|
46
|
+
* zod is the SINGLE SOURCE OF TRUTH for parameters. The MCP renderer passes
|
|
47
|
+
* this shape straight to `server.tool`; the Pi renderer derives a TypeBox
|
|
48
|
+
* schema from it via the zod→TypeBox converter. `z.ZodRawShape` is
|
|
49
|
+
* `Record<string, z.ZodTypeAny>` — the same shape `defineTool` accepted.
|
|
50
|
+
*/
|
|
51
|
+
params: z.ZodRawShape;
|
|
52
|
+
/** Neutral handler. No MCP `extra` param (zero tools use it). */
|
|
53
|
+
handler: (args: Record<string, unknown>) => Promise<TempoToolResult>;
|
|
54
|
+
}
|
|
55
|
+
/** Return a successful tool result. (Neutral replacement for the old MCP `ok`.) */
|
|
56
|
+
export declare function ok(text: string): TempoToolResult;
|
|
57
|
+
/** Return an error tool result. (Neutral replacement for the old MCP `fail`.) */
|
|
58
|
+
export declare function fail(text: string): TempoToolResult;
|
|
59
|
+
/** Extract a human-readable message from an unknown error. */
|
|
60
|
+
export declare function formatError(err: unknown): string;
|
|
61
|
+
/**
|
|
62
|
+
* Render descriptors onto an MCP server — the backward-compatible path.
|
|
63
|
+
*
|
|
64
|
+
* Reproduces the old `defineTool` EXACTLY (see the removed `helpers.ts`):
|
|
65
|
+
* - `(server.tool as Function)(name, description, paramsSchema, handler)` —
|
|
66
|
+
* the `as Function` cast is the TS2589 deep-instantiation workaround for
|
|
67
|
+
* Zod 3.25 + MCP SDK type inference; preserve it.
|
|
68
|
+
* - The handler wraps the neutral `{ text, isError? }` back into MCP's
|
|
69
|
+
* `{ content: [{ type: 'text', text }], isError? }` — byte-identical to
|
|
70
|
+
* what `ok()` / `fail()` produced before MD-B.
|
|
71
|
+
*/
|
|
72
|
+
export declare function renderToMcp(server: McpServer, descriptors: TempoToolDescriptor[]): void;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ok = ok;
|
|
4
|
+
exports.fail = fail;
|
|
5
|
+
exports.formatError = formatError;
|
|
6
|
+
exports.renderToMcp = renderToMcp;
|
|
7
|
+
/** Return a successful tool result. (Neutral replacement for the old MCP `ok`.) */
|
|
8
|
+
function ok(text) {
|
|
9
|
+
return { text };
|
|
10
|
+
}
|
|
11
|
+
/** Return an error tool result. (Neutral replacement for the old MCP `fail`.) */
|
|
12
|
+
function fail(text) {
|
|
13
|
+
return { text, isError: true };
|
|
14
|
+
}
|
|
15
|
+
/** Extract a human-readable message from an unknown error. */
|
|
16
|
+
function formatError(err) {
|
|
17
|
+
return err instanceof Error ? err.message : String(err);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Render descriptors onto an MCP server — the backward-compatible path.
|
|
21
|
+
*
|
|
22
|
+
* Reproduces the old `defineTool` EXACTLY (see the removed `helpers.ts`):
|
|
23
|
+
* - `(server.tool as Function)(name, description, paramsSchema, handler)` —
|
|
24
|
+
* the `as Function` cast is the TS2589 deep-instantiation workaround for
|
|
25
|
+
* Zod 3.25 + MCP SDK type inference; preserve it.
|
|
26
|
+
* - The handler wraps the neutral `{ text, isError? }` back into MCP's
|
|
27
|
+
* `{ content: [{ type: 'text', text }], isError? }` — byte-identical to
|
|
28
|
+
* what `ok()` / `fail()` produced before MD-B.
|
|
29
|
+
*/
|
|
30
|
+
function renderToMcp(server, descriptors) {
|
|
31
|
+
for (const d of descriptors) {
|
|
32
|
+
server.tool(d.name, d.description, d.params, async (args) => {
|
|
33
|
+
const r = await d.handler(args);
|
|
34
|
+
return r.isError
|
|
35
|
+
? { content: [{ type: 'text', text: r.text }], isError: true }
|
|
36
|
+
: { content: [{ type: 'text', text: r.text }] };
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
package/dist/tools/destroy.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
1
|
import { Client, WorkflowHandle } from '@temporalio/client';
|
|
3
2
|
import { Config } from '../config';
|
|
4
|
-
|
|
3
|
+
import { type TempoToolDescriptor } from './descriptor';
|
|
4
|
+
export declare function buildDestroyTool(client: Client, config: Config, getPlayerId: () => string, handle: WorkflowHandle): TempoToolDescriptor;
|
package/dist/tools/destroy.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.buildDestroyTool = buildDestroyTool;
|
|
4
4
|
/**
|
|
5
5
|
* `destroy` — terminal end of either a single session or the whole ensemble.
|
|
6
6
|
*
|
|
@@ -24,165 +24,170 @@ const zod_1 = require("zod");
|
|
|
24
24
|
const config_1 = require("../config");
|
|
25
25
|
const signals_1 = require("../workflows/signals");
|
|
26
26
|
const resolve_1 = require("../activities/resolve");
|
|
27
|
-
const
|
|
27
|
+
const descriptor_1 = require("./descriptor");
|
|
28
28
|
const validation_1 = require("../utils/validation");
|
|
29
29
|
const log = (...args) => console.error('[agent-tempo:destroy]', ...args);
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (nameError)
|
|
52
|
-
return (0, helpers_1.fail)(nameError);
|
|
53
|
-
if (playerId === callerId) {
|
|
54
|
-
return (0, helpers_1.fail)('Cannot destroy your own session.');
|
|
30
|
+
function buildDestroyTool(client, config, getPlayerId, handle) {
|
|
31
|
+
return {
|
|
32
|
+
name: 'destroy',
|
|
33
|
+
description: 'Terminally destroy a session workflow (when `playerId` is given) or the entire ensemble (when omitted): every peer session, the scheduler, the maestro, and the conductor. COMPLETEs workflows and cannot be undone. For graceful reap use `shutdown`; for a clean revive use `restart`.',
|
|
34
|
+
params: {
|
|
35
|
+
// #306: `.min(1)` rejects `{playerId: ""}` at the SDK boundary so a
|
|
36
|
+
// buggy MCP caller can't silently fall through to ensemble-wide
|
|
37
|
+
// destroy mode. The handler also guards programmatic callers that
|
|
38
|
+
// bypass Zod (see explicit `playerId === ''` rejection below).
|
|
39
|
+
playerId: zod_1.z.string().min(1).max(validation_1.PLAYER_NAME_MAX).optional().describe('Target player name. Omit to destroy the entire ensemble.'),
|
|
40
|
+
reason: zod_1.z.string().max(500).optional().describe('Optional reason recorded in the workflow\'s audit event'),
|
|
41
|
+
},
|
|
42
|
+
handler: async (args) => {
|
|
43
|
+
const { playerId, reason } = args;
|
|
44
|
+
const callerId = getPlayerId();
|
|
45
|
+
// #306: defense-in-depth for callers that bypass Zod (test harnesses,
|
|
46
|
+
// direct handler invocation). Zod's `.min(1)` already covers normal
|
|
47
|
+
// MCP traffic; this guard ensures empty-string never falls through to
|
|
48
|
+
// ensemble-wide destroy mode regardless of how the handler is reached.
|
|
49
|
+
if (playerId === '') {
|
|
50
|
+
return (0, descriptor_1.fail)('`playerId` cannot be an empty string. Omit it to destroy the entire ensemble.');
|
|
55
51
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const entryId = await handle.executeUpdate(signals_1.submitOutboxUpdate, { args: [entry] });
|
|
64
|
-
return (0, helpers_1.ok)(`Destroy queued for **${playerId}**${reason ? ` (reason: ${reason})` : ''}. (outbox: ${entryId})`);
|
|
65
|
-
}
|
|
66
|
-
catch (err) {
|
|
67
|
-
return (0, helpers_1.fail)(`Failed to destroy: ${(0, helpers_1.formatError)(err)}`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
// ── Ensemble-scope mode (#287) ──────────────────────────────────────
|
|
71
|
-
// Order: peer sessions (parallel) → scheduler + maestro (parallel) →
|
|
72
|
-
// conductor last. The conductor-last step is the invariant the
|
|
73
|
-
// architect's spec relies on so the conductor sees every peer
|
|
74
|
-
// teardown before its own destroy.
|
|
75
|
-
try {
|
|
76
|
-
const destroyReason = reason ?? `ensemble destroy via ${callerId}`;
|
|
77
|
-
const sessions = await (0, resolve_1.scanEnsembleSessions)(client, config.ensemble);
|
|
78
|
-
const conductorWfId = (0, config_1.conductorWorkflowId)(config.ensemble);
|
|
79
|
-
const peers = [];
|
|
80
|
-
let conductorPresent = false;
|
|
81
|
-
for (const s of sessions) {
|
|
82
|
-
if (s.workflowId === conductorWfId) {
|
|
83
|
-
conductorPresent = true;
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
peers.push(s);
|
|
52
|
+
// ── Single-player mode (existing behaviour) ─────────────────────────
|
|
53
|
+
if (playerId !== undefined) {
|
|
54
|
+
const nameError = (0, validation_1.validatePlayerName)(playerId);
|
|
55
|
+
if (nameError)
|
|
56
|
+
return (0, descriptor_1.fail)(nameError);
|
|
57
|
+
if (playerId === callerId) {
|
|
58
|
+
return (0, descriptor_1.fail)('Cannot destroy your own session.');
|
|
87
59
|
}
|
|
88
|
-
}
|
|
89
|
-
const details = [];
|
|
90
|
-
let destroyed = 0;
|
|
91
|
-
let terminated = 0;
|
|
92
|
-
let failed = 0;
|
|
93
|
-
// Phase 1: destroy every peer in parallel (conductor excluded). Skip
|
|
94
|
-
// the caller's own session — self-destroy is a no-op guard.
|
|
95
|
-
const peerResults = await Promise.allSettled(peers.map(async (s) => {
|
|
96
|
-
if (s.playerId === callerId)
|
|
97
|
-
return { session: s, outcome: 'skipped-self' };
|
|
98
60
|
try {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
61
|
+
const entry = {
|
|
62
|
+
type: 'destroy',
|
|
63
|
+
targetPlayerId: playerId,
|
|
64
|
+
...(reason !== undefined ? { reason } : {}),
|
|
65
|
+
notifyConductor: true,
|
|
66
|
+
};
|
|
67
|
+
const entryId = await handle.executeUpdate(signals_1.submitOutboxUpdate, { args: [entry] });
|
|
68
|
+
return (0, descriptor_1.ok)(`Destroy queued for **${playerId}**${reason ? ` (reason: ${reason})` : ''}. (outbox: ${entryId})`);
|
|
103
69
|
}
|
|
104
70
|
catch (err) {
|
|
105
|
-
return
|
|
71
|
+
return (0, descriptor_1.fail)(`Failed to destroy: ${(0, descriptor_1.formatError)(err)}`);
|
|
106
72
|
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
73
|
+
}
|
|
74
|
+
// ── Ensemble-scope mode (#287) ──────────────────────────────────────
|
|
75
|
+
// Order: peer sessions (parallel) → scheduler + maestro (parallel) →
|
|
76
|
+
// conductor last. The conductor-last step is the invariant the
|
|
77
|
+
// architect's spec relies on so the conductor sees every peer
|
|
78
|
+
// teardown before its own destroy.
|
|
79
|
+
try {
|
|
80
|
+
const destroyReason = reason ?? `ensemble destroy via ${callerId}`;
|
|
81
|
+
const sessions = await (0, resolve_1.scanEnsembleSessions)(client, config.ensemble);
|
|
82
|
+
const conductorWfId = (0, config_1.conductorWorkflowId)(config.ensemble);
|
|
83
|
+
const peers = [];
|
|
84
|
+
let conductorPresent = false;
|
|
85
|
+
for (const s of sessions) {
|
|
86
|
+
if (s.workflowId === conductorWfId) {
|
|
87
|
+
conductorPresent = true;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
peers.push(s);
|
|
91
|
+
}
|
|
115
92
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
93
|
+
const details = [];
|
|
94
|
+
let destroyed = 0;
|
|
95
|
+
let terminated = 0;
|
|
96
|
+
let failed = 0;
|
|
97
|
+
// Phase 1: destroy every peer in parallel (conductor excluded). Skip
|
|
98
|
+
// the caller's own session — self-destroy is a no-op guard.
|
|
99
|
+
const peerResults = await Promise.allSettled(peers.map(async (s) => {
|
|
100
|
+
if (s.playerId === callerId)
|
|
101
|
+
return { session: s, outcome: 'skipped-self' };
|
|
102
|
+
try {
|
|
103
|
+
await client.workflow.getHandle(s.workflowId).executeUpdate(signals_1.destroyUpdate, {
|
|
104
|
+
args: [{ reason: destroyReason, terminatedBy: callerId }],
|
|
105
|
+
});
|
|
106
|
+
return { session: s, outcome: 'destroyed' };
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
return { session: s, outcome: 'failed', error: (0, descriptor_1.formatError)(err) };
|
|
110
|
+
}
|
|
111
|
+
}));
|
|
112
|
+
for (const r of peerResults) {
|
|
113
|
+
if (r.status !== 'fulfilled')
|
|
114
|
+
continue;
|
|
115
|
+
const v = r.value;
|
|
116
|
+
if (v.outcome === 'destroyed') {
|
|
117
|
+
details.push({ target: v.session.playerId, outcome: 'destroyed' });
|
|
118
|
+
destroyed++;
|
|
119
|
+
}
|
|
120
|
+
else if (v.outcome === 'failed') {
|
|
121
|
+
details.push({ target: v.session.playerId, outcome: 'failed', error: v.error });
|
|
122
|
+
failed++;
|
|
123
|
+
}
|
|
124
|
+
// #299: `'skipped-self'` is an internal control-flow tag for the
|
|
125
|
+
// caller's own session — intentionally NOT surfaced in `details`
|
|
126
|
+
// because `EnsembleDestroyDetail` is consumed publicly via
|
|
127
|
+
// `EnsembleDestroySummary` (TempoClient.destroy), which has no
|
|
128
|
+
// caller-self concept. The skip is a bookkeeping no-op here.
|
|
119
129
|
}
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
client.workflow.getHandle((0, config_1.schedulerWorkflowId)(config.ensemble)).terminate(destroyReason),
|
|
131
|
-
client.workflow.getHandle((0, config_1.maestroWorkflowId)(config.ensemble)).terminate(destroyReason),
|
|
132
|
-
]);
|
|
133
|
-
if (schedRes.status === 'fulfilled') {
|
|
134
|
-
details.push({ target: 'scheduler', outcome: 'terminated' });
|
|
135
|
-
terminated++;
|
|
136
|
-
}
|
|
137
|
-
if (maestroRes.status === 'fulfilled') {
|
|
138
|
-
details.push({ target: 'maestro', outcome: 'terminated' });
|
|
139
|
-
terminated++;
|
|
140
|
-
}
|
|
141
|
-
// Phase 3: conductor last, so it observes peer teardown. Skipped if
|
|
142
|
-
// the caller IS the conductor (same self-destroy guard). #299: the
|
|
143
|
-
// skip is a control-flow no-op — no `details` entry, mirroring the
|
|
144
|
-
// peer-self skip. `EnsembleDestroyDetail.outcome` no longer carries
|
|
145
|
-
// a self-skip member.
|
|
146
|
-
if (callerId === 'conductor') {
|
|
147
|
-
// self-skip; no recording
|
|
148
|
-
}
|
|
149
|
-
else if (conductorPresent) {
|
|
150
|
-
try {
|
|
151
|
-
await client.workflow.getHandle(conductorWfId).executeUpdate(signals_1.destroyUpdate, {
|
|
152
|
-
args: [{ reason: destroyReason, terminatedBy: callerId }],
|
|
153
|
-
});
|
|
154
|
-
details.push({ target: 'conductor', outcome: 'destroyed' });
|
|
155
|
-
destroyed++;
|
|
130
|
+
// Phase 2: scheduler + maestro terminate in parallel (non-session
|
|
131
|
+
// workflows — no destroy handler). `terminate` rejects when the
|
|
132
|
+
// workflow isn't running; treat as "not present" instead of failure.
|
|
133
|
+
const [schedRes, maestroRes] = await Promise.allSettled([
|
|
134
|
+
client.workflow.getHandle((0, config_1.schedulerWorkflowId)(config.ensemble)).terminate(destroyReason),
|
|
135
|
+
client.workflow.getHandle((0, config_1.maestroWorkflowId)(config.ensemble)).terminate(destroyReason),
|
|
136
|
+
]);
|
|
137
|
+
if (schedRes.status === 'fulfilled') {
|
|
138
|
+
details.push({ target: 'scheduler', outcome: 'terminated' });
|
|
139
|
+
terminated++;
|
|
156
140
|
}
|
|
157
|
-
|
|
158
|
-
details.push({ target: '
|
|
159
|
-
|
|
141
|
+
if (maestroRes.status === 'fulfilled') {
|
|
142
|
+
details.push({ target: 'maestro', outcome: 'terminated' });
|
|
143
|
+
terminated++;
|
|
160
144
|
}
|
|
145
|
+
// Phase 3: conductor last, so it observes peer teardown. Skipped if
|
|
146
|
+
// the caller IS the conductor (same self-destroy guard). #299: the
|
|
147
|
+
// skip is a control-flow no-op — no `details` entry, mirroring the
|
|
148
|
+
// peer-self skip. `EnsembleDestroyDetail.outcome` no longer carries
|
|
149
|
+
// a self-skip member.
|
|
150
|
+
if (callerId === 'conductor') {
|
|
151
|
+
// self-skip; no recording
|
|
152
|
+
}
|
|
153
|
+
else if (conductorPresent) {
|
|
154
|
+
try {
|
|
155
|
+
await client.workflow.getHandle(conductorWfId).executeUpdate(signals_1.destroyUpdate, {
|
|
156
|
+
args: [{ reason: destroyReason, terminatedBy: callerId }],
|
|
157
|
+
});
|
|
158
|
+
details.push({ target: 'conductor', outcome: 'destroyed' });
|
|
159
|
+
destroyed++;
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
details.push({ target: 'conductor', outcome: 'failed', error: (0, descriptor_1.formatError)(err) });
|
|
163
|
+
failed++;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const summaryLine = `${destroyed} destroyed, ${terminated} terminated, ${failed} failed`;
|
|
167
|
+
const headline = failed > 0
|
|
168
|
+
? `Ensemble **${config.ensemble}** partially destroyed.`
|
|
169
|
+
: `Ensemble **${config.ensemble}** destroyed.`;
|
|
170
|
+
const lines = [headline, summaryLine];
|
|
171
|
+
const failures = details.filter((d) => d.outcome === 'failed');
|
|
172
|
+
if (failures.length > 0) {
|
|
173
|
+
lines.push(`Errors:\n${failures.map((d) => ` - ${d.target}: ${d.error}`).join('\n')}`);
|
|
174
|
+
// #306 follow-up: surface the indeterminate-state hint from my own
|
|
175
|
+
// PR-#306 holistic review (regression risk #3). `Promise.allSettled`
|
|
176
|
+
// returned `failed` outcomes for these peers — the workflows may
|
|
177
|
+
// be in any state from "still running" to "destroyed but RPC
|
|
178
|
+
// timed out". Re-running `destroy` is safe (idempotent on the
|
|
179
|
+
// workflow side: `destroyUpdate` on a `gone` workflow is a no-op
|
|
180
|
+
// via the `isDestroyedQuery` guard) and the cleanest recovery.
|
|
181
|
+
const noun = failed === 1 ? 'peer' : 'peers';
|
|
182
|
+
lines.push(`⚠ ${failed} ${noun} in indeterminate state — ` +
|
|
183
|
+
`run \`/destroy ${config.ensemble}\` again to clean up.`);
|
|
184
|
+
}
|
|
185
|
+
log(`Ensemble destroy by ${callerId}: ${summaryLine}`);
|
|
186
|
+
return (0, descriptor_1.ok)(lines.join('\n'));
|
|
161
187
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
? `Ensemble **${config.ensemble}** partially destroyed.`
|
|
165
|
-
: `Ensemble **${config.ensemble}** destroyed.`;
|
|
166
|
-
const lines = [headline, summaryLine];
|
|
167
|
-
const failures = details.filter((d) => d.outcome === 'failed');
|
|
168
|
-
if (failures.length > 0) {
|
|
169
|
-
lines.push(`Errors:\n${failures.map((d) => ` - ${d.target}: ${d.error}`).join('\n')}`);
|
|
170
|
-
// #306 follow-up: surface the indeterminate-state hint from my own
|
|
171
|
-
// PR-#306 holistic review (regression risk #3). `Promise.allSettled`
|
|
172
|
-
// returned `failed` outcomes for these peers — the workflows may
|
|
173
|
-
// be in any state from "still running" to "destroyed but RPC
|
|
174
|
-
// timed out". Re-running `destroy` is safe (idempotent on the
|
|
175
|
-
// workflow side: `destroyUpdate` on a `gone` workflow is a no-op
|
|
176
|
-
// via the `isDestroyedQuery` guard) and the cleanest recovery.
|
|
177
|
-
const noun = failed === 1 ? 'peer' : 'peers';
|
|
178
|
-
lines.push(`⚠ ${failed} ${noun} in indeterminate state — ` +
|
|
179
|
-
`run \`/destroy ${config.ensemble}\` again to clean up.`);
|
|
188
|
+
catch (err) {
|
|
189
|
+
return (0, descriptor_1.fail)(`Failed to destroy ensemble: ${(0, descriptor_1.formatError)(err)}`);
|
|
180
190
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
catch (err) {
|
|
185
|
-
return (0, helpers_1.fail)(`Failed to destroy ensemble: ${(0, helpers_1.formatError)(err)}`);
|
|
186
|
-
}
|
|
187
|
-
});
|
|
191
|
+
},
|
|
192
|
+
};
|
|
188
193
|
}
|
package/dist/tools/ensemble.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
1
|
import { Client } from '@temporalio/client';
|
|
3
2
|
import { Config } from '../config';
|
|
4
3
|
import { type EnsembleSessionInfo } from '../activities/resolve';
|
|
4
|
+
import { type TempoToolDescriptor } from './descriptor';
|
|
5
5
|
/**
|
|
6
6
|
* Default dormancy threshold (1 hour). Per #563: a `detached` player whose
|
|
7
7
|
* last activity is older than this is considered dormant. `phase === 'gone'`
|
|
@@ -29,4 +29,4 @@ export type DormantFilter = 'show' | 'hide' | 'show-only';
|
|
|
29
29
|
* Exported for unit testing.
|
|
30
30
|
*/
|
|
31
31
|
export declare function classifyDormancy(session: Pick<EnsembleSessionInfo, 'phase' | 'lastActivityAt'>, now: number, thresholdMs?: number): 'active' | 'dormant';
|
|
32
|
-
export declare function
|
|
32
|
+
export declare function buildEnsembleTool(client: Client, config: Config, getPlayerId: () => string, ownWorkflowId: string): TempoToolDescriptor;
|