agent-relay 13.2.0 → 13.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/README.md +60 -0
- package/dist/cli/agent-relay-mcp.d.ts.map +1 -1
- package/dist/cli/agent-relay-mcp.js +131 -10
- package/dist/cli/agent-relay-mcp.js.map +1 -1
- package/dist/cli/commands/agent.d.ts.map +1 -1
- package/dist/cli/commands/agent.js +24 -19
- package/dist/cli/commands/agent.js.map +1 -1
- package/dist/cli/commands/fleet.d.ts.map +1 -1
- package/dist/cli/commands/fleet.js +183 -39
- package/dist/cli/commands/fleet.js.map +1 -1
- package/dist/cli/commands/integration.d.ts.map +1 -1
- package/dist/cli/commands/integration.js +81 -7
- package/dist/cli/commands/integration.js.map +1 -1
- package/dist/cli/commands/local-agent.d.ts.map +1 -1
- package/dist/cli/commands/local-agent.js +22 -3
- package/dist/cli/commands/local-agent.js.map +1 -1
- package/dist/cli/commands/message.d.ts.map +1 -1
- package/dist/cli/commands/message.js +111 -29
- package/dist/cli/commands/message.js.map +1 -1
- package/dist/cli/lib/agent-registration.d.ts +2 -0
- package/dist/cli/lib/agent-registration.d.ts.map +1 -1
- package/dist/cli/lib/agent-registration.js +2 -2
- package/dist/cli/lib/agent-registration.js.map +1 -1
- package/dist/cli/lib/agent-removal.d.ts +30 -0
- package/dist/cli/lib/agent-removal.d.ts.map +1 -0
- package/dist/cli/lib/agent-removal.js +73 -0
- package/dist/cli/lib/agent-removal.js.map +1 -0
- package/dist/cli/lib/attachments.d.ts +31 -0
- package/dist/cli/lib/attachments.d.ts.map +1 -0
- package/dist/cli/lib/attachments.js +239 -0
- package/dist/cli/lib/attachments.js.map +1 -0
- package/dist/cli/lib/fleet-live-agents.d.ts +17 -0
- package/dist/cli/lib/fleet-live-agents.d.ts.map +1 -1
- package/dist/cli/lib/fleet-live-agents.js +23 -0
- package/dist/cli/lib/fleet-live-agents.js.map +1 -1
- package/dist/cli/lib/sdk-command.d.ts.map +1 -1
- package/dist/cli/lib/sdk-command.js +8 -2
- package/dist/cli/lib/sdk-command.js.map +1 -1
- package/dist/cli/lib/spawn-liveness.d.ts +50 -0
- package/dist/cli/lib/spawn-liveness.d.ts.map +1 -0
- package/dist/cli/lib/spawn-liveness.js +184 -0
- package/dist/cli/lib/spawn-liveness.js.map +1 -0
- package/dist/cli/lib/task-input.d.ts +26 -0
- package/dist/cli/lib/task-input.d.ts.map +1 -0
- package/dist/cli/lib/task-input.js +102 -0
- package/dist/cli/lib/task-input.js.map +1 -0
- package/dist/cli/mcp/messaging-tools.d.ts.map +1 -1
- package/dist/cli/mcp/messaging-tools.js +136 -34
- package/dist/cli/mcp/messaging-tools.js.map +1 -1
- package/dist/cli/mcp/request-replay.d.ts +21 -9
- package/dist/cli/mcp/request-replay.d.ts.map +1 -1
- package/dist/cli/mcp/request-replay.js +58 -9
- package/dist/cli/mcp/request-replay.js.map +1 -1
- package/dist/index.cjs +200 -6
- package/package.json +10 -10
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { RelayPlacementError, safeRelayErrorMessage } from '@agent-relay/sdk';
|
|
2
|
+
import { shellQuote, withDeadline } from './agent-registration.js';
|
|
3
|
+
import { isNotFoundError } from './agent-removal.js';
|
|
4
|
+
import { heartbeatAgeMs, isAvailableFleetNode, MAX_LIVE_HEARTBEAT_AGE_MS, readRemoteLiveAgents, } from './fleet-live-agents.js';
|
|
5
|
+
export class FleetSpawnError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
liveness;
|
|
8
|
+
diagnostic;
|
|
9
|
+
state;
|
|
10
|
+
exitCode;
|
|
11
|
+
invocationId;
|
|
12
|
+
node;
|
|
13
|
+
dispatchState;
|
|
14
|
+
receipt;
|
|
15
|
+
constructor(
|
|
16
|
+
// `spawn_unconfirmed` is the established public code for an accepted spawn
|
|
17
|
+
// whose outcome is unknown (relay#1563); keep it so callers keyed on it keep
|
|
18
|
+
// working, and carry the pending outcome in `state` and the exit code.
|
|
19
|
+
code, message, context, liveness, diagnostic) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.code = code;
|
|
22
|
+
this.liveness = liveness;
|
|
23
|
+
this.diagnostic = diagnostic;
|
|
24
|
+
this.state = code === 'spawn_unconfirmed' ? 'pending' : 'failed';
|
|
25
|
+
this.exitCode = code === 'spawn_unconfirmed' ? 8 : 1;
|
|
26
|
+
this.invocationId = context.invocationId;
|
|
27
|
+
this.node = context.node;
|
|
28
|
+
this.dispatchState = context.dispatchState;
|
|
29
|
+
this.receipt = context.receipt;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function mayStillBeRunning(error) {
|
|
33
|
+
return ((error instanceof RelayPlacementError && error.state === 'unconfirmed_may_be_running') ||
|
|
34
|
+
(error instanceof FleetSpawnError && error.code === 'spawn_unconfirmed'));
|
|
35
|
+
}
|
|
36
|
+
export function pendingSpawnError(name, context, liveness, diagnostic) {
|
|
37
|
+
// A worker found elsewhere is invisible to a node-scoped check.
|
|
38
|
+
const nodeOption = context.node && liveness.evidence !== 'live_elsewhere' ? ` --node ${shellQuote(context.node)}` : '';
|
|
39
|
+
// Heartbeats carry worker names, not invocation IDs, so presence cannot be
|
|
40
|
+
// attributed to this spawn: it may be a worker that was already running.
|
|
41
|
+
const attribution = liveness.evidence === 'live' || liveness.evidence === 'live_elsewhere' || liveness.evidence === 'stale'
|
|
42
|
+
? `A worker with this name is listed by a node heartbeat, but heartbeats do not identify the invocation that started it, so it may be an earlier worker rather than this spawn. `
|
|
43
|
+
: '';
|
|
44
|
+
return new FleetSpawnError('spawn_unconfirmed', `Spawn of ${JSON.stringify(name)} was accepted; its outcome is pending. ` +
|
|
45
|
+
`Evidence: ${liveness.evidence}${liveness.node ? ` on ${JSON.stringify(liveness.node)}` : ''}. ` +
|
|
46
|
+
attribution +
|
|
47
|
+
`Invocation: ${context.invocationId ?? 'unavailable'}; dispatch: ${context.dispatchState ?? 'unknown'}. ` +
|
|
48
|
+
`Do not retry blindly: check \`agent-relay fleet agent list${nodeOption}\` first. ` +
|
|
49
|
+
`Only if no worker is running and you intend to reclaim the name, run \`agent-relay agent remove ${shellQuote(name)} --wait\` and wait for clearance before respawning.`, context, liveness, diagnostic);
|
|
50
|
+
}
|
|
51
|
+
export function classifySpawnFailure(message) {
|
|
52
|
+
// Anchored to the broker formatter; unrelated "already exists" failures are not registration collisions.
|
|
53
|
+
return /(?:^|:\s)failed to pre-register worker '[^\n]+': agent '[^\n]+' already exists and registration is create-only(?:;|$)/i.test(message)
|
|
54
|
+
? 'spawn_name_taken'
|
|
55
|
+
: undefined;
|
|
56
|
+
}
|
|
57
|
+
export function nameTakenSpawnError(name, error) {
|
|
58
|
+
return new FleetSpawnError('spawn_name_taken', `${safeRelayErrorMessage(error)} A removal or cleanup from a previous unsuccessful spawn may still be in flight. Re-check \`agent-relay agent list\`; if the name is present with no worker running, run \`agent-relay agent remove ${shellQuote(name)} --wait\`, wait for clearance, then retry.`, error);
|
|
59
|
+
}
|
|
60
|
+
/** Reads are bounded and client construction is fallible (agent-token-only callers are supported). */
|
|
61
|
+
export async function probeSpawnLiveness(options) {
|
|
62
|
+
let readError;
|
|
63
|
+
let rosterUnread = false;
|
|
64
|
+
let rosterIncomplete = false;
|
|
65
|
+
const read = (fn) => withDeadline(fn, () => new Error('Spawn evidence read timed out.'), 2_000);
|
|
66
|
+
if (options.getInvocation) {
|
|
67
|
+
try {
|
|
68
|
+
const value = await read(options.getInvocation);
|
|
69
|
+
if (value && typeof value === 'object') {
|
|
70
|
+
const invocation = value;
|
|
71
|
+
if (/^(completed|succeeded|success|failed|error|cancelled|canceled|denied)$/i.test(String(invocation.status))) {
|
|
72
|
+
return { evidence: 'invocation_terminal', invocation };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
readError = safeRelayErrorMessage(error);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const client = options.createClient();
|
|
82
|
+
try {
|
|
83
|
+
// Read the clock when a heartbeat is judged, after the reads: sampling it
|
|
84
|
+
// first would let read latency make an old heartbeat look fresh.
|
|
85
|
+
const clock = () => options.now?.() ?? Date.now();
|
|
86
|
+
const target = options.targetNode ? await read(() => client.nodes.get(options.targetNode)) : undefined;
|
|
87
|
+
const nodes = options.targetNode && target ? [target] : await read(() => client.nodes.list());
|
|
88
|
+
// Prefer the targeted read; consult the fleet unless the target is an
|
|
89
|
+
// available node whose fresh heartbeat claims this name.
|
|
90
|
+
const targetAge = target ? heartbeatAgeMs(target, clock()) : null;
|
|
91
|
+
if (options.targetNode &&
|
|
92
|
+
target &&
|
|
93
|
+
!(isAvailableFleetNode(target) &&
|
|
94
|
+
targetAge !== null &&
|
|
95
|
+
targetAge <= MAX_LIVE_HEARTBEAT_AGE_MS &&
|
|
96
|
+
readRemoteLiveAgents(target).agents.some((a) => a.name === options.name))) {
|
|
97
|
+
nodes.push(...(await read(() => client.nodes.list())));
|
|
98
|
+
}
|
|
99
|
+
let weaker;
|
|
100
|
+
const now = clock();
|
|
101
|
+
for (const node of nodes) {
|
|
102
|
+
if (!isAvailableFleetNode(node))
|
|
103
|
+
continue;
|
|
104
|
+
const live = readRemoteLiveAgents(node);
|
|
105
|
+
// A node whose heartbeat roster is missing or undecodable may still be
|
|
106
|
+
// running the worker, so a later missing registration cannot prove absence.
|
|
107
|
+
if (!live.complete)
|
|
108
|
+
rosterIncomplete = true;
|
|
109
|
+
if (!live.agents.some((agent) => agent.name === options.name))
|
|
110
|
+
continue;
|
|
111
|
+
const age = heartbeatAgeMs(node, now);
|
|
112
|
+
const evidence = age === null || age > MAX_LIVE_HEARTBEAT_AGE_MS
|
|
113
|
+
? 'stale'
|
|
114
|
+
: options.targetNode && node.name !== options.targetNode
|
|
115
|
+
? 'live_elsewhere'
|
|
116
|
+
: 'live';
|
|
117
|
+
// Keep a failed invocation read visible next to roster evidence.
|
|
118
|
+
const result = {
|
|
119
|
+
evidence,
|
|
120
|
+
node: node.name,
|
|
121
|
+
heartbeatAgeMs: age,
|
|
122
|
+
...(readError ? { readError } : {}),
|
|
123
|
+
};
|
|
124
|
+
if (evidence === 'live')
|
|
125
|
+
return result;
|
|
126
|
+
// Fresh evidence elsewhere outranks a stale entry.
|
|
127
|
+
if (!weaker || (weaker.evidence === 'stale' && evidence === 'live_elsewhere'))
|
|
128
|
+
weaker = result;
|
|
129
|
+
}
|
|
130
|
+
if (weaker)
|
|
131
|
+
return weaker;
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
readError = safeRelayErrorMessage(error);
|
|
135
|
+
rosterUnread = true;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
await read(() => client.agents.get(options.name));
|
|
139
|
+
return { evidence: 'registered', ...(readError ? { readError } : {}) };
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
// `absent` claims no worker was observed. A missing registration only
|
|
143
|
+
// proves that when the node roster was actually read.
|
|
144
|
+
if (isNotFoundError(error) && !rosterUnread && !rosterIncomplete)
|
|
145
|
+
return { evidence: 'absent', ...(readError ? { readError } : {}) };
|
|
146
|
+
if (!isNotFoundError(error))
|
|
147
|
+
readError = safeRelayErrorMessage(error);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
readError = safeRelayErrorMessage(error);
|
|
152
|
+
}
|
|
153
|
+
return { evidence: 'unknown', readError };
|
|
154
|
+
}
|
|
155
|
+
/** Preserve the original request's launch/readiness contract on the final invocation read. */
|
|
156
|
+
export function terminalSpawnOutcome(invocation, context, requireReady) {
|
|
157
|
+
const output = invocation.output;
|
|
158
|
+
if (!/^(completed|succeeded|success)$/i.test(String(invocation.status)) ||
|
|
159
|
+
output?.spawned !== true ||
|
|
160
|
+
typeof output.ready !== 'boolean' ||
|
|
161
|
+
(requireReady && output.ready !== true)) {
|
|
162
|
+
throw new RelayPlacementError('spawn_failed', typeof invocation.error === 'string'
|
|
163
|
+
? invocation.error
|
|
164
|
+
: 'Spawn completed without the requested launch/readiness proof.', {
|
|
165
|
+
capability: 'spawn',
|
|
166
|
+
attempts: 1,
|
|
167
|
+
state: 'failed',
|
|
168
|
+
invocationId: context.invocationId,
|
|
169
|
+
node: context.node,
|
|
170
|
+
dispatchState: context.dispatchState,
|
|
171
|
+
receipt: invocation,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
...invocation,
|
|
176
|
+
placement: {
|
|
177
|
+
state: output.ready ? 'ready' : 'accepted',
|
|
178
|
+
confirmed: true,
|
|
179
|
+
invocationId: context.invocationId,
|
|
180
|
+
dispatchState: context.dispatchState,
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=spawn-liveness.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spawn-liveness.js","sourceRoot":"","sources":["../../../src/cli/lib/spawn-liveness.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Broker-wide PTY ceiling for the formatted envelope; a harness may reject a lower effective wire limit. */
|
|
2
|
+
export declare const MAX_INJECTION_BODY_BYTES: number;
|
|
3
|
+
/** Room the broker keeps for attribution and its MCP reminder around a body. */
|
|
4
|
+
export declare const ENVELOPE_RESERVE_BYTES: number;
|
|
5
|
+
/** Largest body that still fits {@link MAX_INJECTION_BODY_BYTES} once formatted. */
|
|
6
|
+
export declare const MAX_TASK_BODY_BYTES: number;
|
|
7
|
+
/**
|
|
8
|
+
* The broker's portable ceiling for a prompt passed as one process argument
|
|
9
|
+
* (Muse's startup task, a headless provider's message).
|
|
10
|
+
*/
|
|
11
|
+
export declare const MAX_ARGV_TASK_BYTES: number;
|
|
12
|
+
/**
|
|
13
|
+
* A native runtime receives its task as a delivery frame, so no wire limit
|
|
14
|
+
* applies; this only bounds how much of a `--task-file` the CLI will read.
|
|
15
|
+
*/
|
|
16
|
+
export declare const MAX_NATIVE_TASK_BYTES: number;
|
|
17
|
+
/** Who receives a task: the CLI, and the runtime once it is resolved. */
|
|
18
|
+
export interface TaskTarget {
|
|
19
|
+
cli: string;
|
|
20
|
+
/** `native` delivers the task as a frame, without the PTY envelope; omitted means PTY. */
|
|
21
|
+
runtime?: 'native' | 'pty';
|
|
22
|
+
}
|
|
23
|
+
export declare function readTaskInput(task: unknown, taskFile: unknown, required?: boolean, target?: TaskTarget): Promise<string | undefined>;
|
|
24
|
+
/** Validate a task as its target receives it; recheck after anything is appended to it. */
|
|
25
|
+
export declare function validateTaskSize(text: string, target?: TaskTarget): void;
|
|
26
|
+
//# sourceMappingURL=task-input.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-input.d.ts","sourceRoot":"","sources":["../../../src/cli/lib/task-input.ts"],"names":[],"mappings":"AAEA,6GAA6G;AAC7G,eAAO,MAAM,wBAAwB,QAAY,CAAC;AAClD,gFAAgF;AAChF,eAAO,MAAM,sBAAsB,QAAW,CAAC;AAC/C,oFAAoF;AACpF,eAAO,MAAM,mBAAmB,QAAoD,CAAC;AACrF;;;GAGG;AACH,eAAO,MAAM,mBAAmB,QAAY,CAAC;AAE7C;;;GAGG;AACH,eAAO,MAAM,qBAAqB,QAAc,CAAC;AAEjD,yEAAyE;AACzE,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,0FAA0F;IAC1F,OAAO,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAC;CAC5B;AAiBD,wBAAsB,aAAa,CACjC,IAAI,EAAE,OAAO,EACb,QAAQ,EAAE,OAAO,EACjB,QAAQ,UAAQ,EAChB,MAAM,CAAC,EAAE,UAAU,GAClB,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAY7B;AAOD,2FAA2F;AAC3F,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,UAAU,GAAG,IAAI,CAExE"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { open } from 'node:fs/promises';
|
|
2
|
+
/** Broker-wide PTY ceiling for the formatted envelope; a harness may reject a lower effective wire limit. */
|
|
3
|
+
export const MAX_INJECTION_BODY_BYTES = 16 * 1024;
|
|
4
|
+
/** Room the broker keeps for attribution and its MCP reminder around a body. */
|
|
5
|
+
export const ENVELOPE_RESERVE_BYTES = 2 * 1024;
|
|
6
|
+
/** Largest body that still fits {@link MAX_INJECTION_BODY_BYTES} once formatted. */
|
|
7
|
+
export const MAX_TASK_BODY_BYTES = MAX_INJECTION_BODY_BYTES - ENVELOPE_RESERVE_BYTES;
|
|
8
|
+
/**
|
|
9
|
+
* The broker's portable ceiling for a prompt passed as one process argument
|
|
10
|
+
* (Muse's startup task, a headless provider's message).
|
|
11
|
+
*/
|
|
12
|
+
export const MAX_ARGV_TASK_BYTES = 16 * 1024;
|
|
13
|
+
/**
|
|
14
|
+
* A native runtime receives its task as a delivery frame, so no wire limit
|
|
15
|
+
* applies; this only bounds how much of a `--task-file` the CLI will read.
|
|
16
|
+
*/
|
|
17
|
+
export const MAX_NATIVE_TASK_BYTES = 1024 * 1024;
|
|
18
|
+
/** Mirrors the broker's `is_muse_executable`: the basename, case-insensitive, minus a Windows launcher suffix. */
|
|
19
|
+
function isMuseExecutable(cli) {
|
|
20
|
+
const basename = cli.split(/[\\/]/).pop() || cli;
|
|
21
|
+
return basename.toLowerCase().replace(/\.(exe|cmd|bat)$/, '') === 'muse';
|
|
22
|
+
}
|
|
23
|
+
/** Muse takes its task as one argument; a native runtime as a frame; everything else through the PTY envelope. */
|
|
24
|
+
function taskLimitKind(target) {
|
|
25
|
+
if (target?.runtime === 'native')
|
|
26
|
+
return 'native';
|
|
27
|
+
if (target && isMuseExecutable(target.cli))
|
|
28
|
+
return 'argv';
|
|
29
|
+
return 'pty';
|
|
30
|
+
}
|
|
31
|
+
export async function readTaskInput(task, taskFile, required = false, target) {
|
|
32
|
+
if (task !== undefined && taskFile !== undefined)
|
|
33
|
+
throw new Error('Specify exactly one of --task or --task-file');
|
|
34
|
+
if (task === undefined && taskFile === undefined) {
|
|
35
|
+
if (required)
|
|
36
|
+
throw new Error('Specify exactly one of --task or --task-file');
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const text = taskFile === undefined ? task : await readTaskFile(String(taskFile), taskByteLimit(target), target);
|
|
40
|
+
if (typeof text !== 'string' || !text.trim())
|
|
41
|
+
throw new Error('Task must not be empty');
|
|
42
|
+
validateTaskSize(text, target);
|
|
43
|
+
return text;
|
|
44
|
+
}
|
|
45
|
+
function taskByteLimit(target) {
|
|
46
|
+
const kind = taskLimitKind(target);
|
|
47
|
+
return kind === 'pty' ? MAX_TASK_BODY_BYTES : kind === 'argv' ? MAX_ARGV_TASK_BYTES : MAX_NATIVE_TASK_BYTES;
|
|
48
|
+
}
|
|
49
|
+
/** Validate a task as its target receives it; recheck after anything is appended to it. */
|
|
50
|
+
export function validateTaskSize(text, target) {
|
|
51
|
+
validateTaskBytes(Buffer.byteLength(text, 'utf8'), target);
|
|
52
|
+
}
|
|
53
|
+
function validateTaskBytes(bytes, target) {
|
|
54
|
+
const kind = taskLimitKind(target);
|
|
55
|
+
if (kind === 'pty' && bytes > MAX_TASK_BODY_BYTES)
|
|
56
|
+
throwInjectionTooLarge();
|
|
57
|
+
if (kind === 'argv')
|
|
58
|
+
validateArgvTaskSize(bytes);
|
|
59
|
+
if (kind === 'native' && bytes > MAX_NATIVE_TASK_BYTES) {
|
|
60
|
+
throw new Error(`Task exceeds ${MAX_NATIVE_TASK_BYTES} UTF-8 bytes; use a brief file on the node and send a short pointer`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Read a UTF-8 task file, never more than one byte past `limit`: that is enough
|
|
65
|
+
* to reject an oversized file without materializing all of it. Size is checked
|
|
66
|
+
* before decoding, since the cut can split a character, and malformed UTF-8 is
|
|
67
|
+
* rejected rather than replaced, so the agent never gets different text.
|
|
68
|
+
*/
|
|
69
|
+
async function readTaskFile(path, limit, target) {
|
|
70
|
+
const handle = await open(path, 'r');
|
|
71
|
+
try {
|
|
72
|
+
const maxBytes = limit + 1;
|
|
73
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
74
|
+
let length = 0;
|
|
75
|
+
while (length < maxBytes) {
|
|
76
|
+
const { bytesRead } = await handle.read(buffer, length, maxBytes - length, null);
|
|
77
|
+
if (bytesRead === 0)
|
|
78
|
+
break;
|
|
79
|
+
length += bytesRead;
|
|
80
|
+
}
|
|
81
|
+
if (length > limit)
|
|
82
|
+
validateTaskBytes(length, target);
|
|
83
|
+
try {
|
|
84
|
+
return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(buffer.subarray(0, length));
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
throw new Error(`Task file ${path} is not valid UTF-8`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
await handle.close();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function validateArgvTaskSize(bytes) {
|
|
95
|
+
if (bytes > MAX_ARGV_TASK_BYTES) {
|
|
96
|
+
throw new Error(`Task exceeds the ${MAX_ARGV_TASK_BYTES}-byte portable argv limit for a single process argument; use a brief file on the node and send a short pointer`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function throwInjectionTooLarge() {
|
|
100
|
+
throw new Error(`Task exceeds ${MAX_TASK_BODY_BYTES} UTF-8 bytes (the ${MAX_INJECTION_BODY_BYTES}-byte PTY limit less room for the message envelope); use a brief file on the node and send a short pointer`);
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=task-input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-input.js","sourceRoot":"","sources":["../../../src/cli/lib/task-input.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"messaging-tools.d.ts","sourceRoot":"","sources":["../../../src/cli/mcp/messaging-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"messaging-tools.d.ts","sourceRoot":"","sources":["../../../src/cli/mcp/messaging-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAyBpE,OAAO,EAAE,gBAAgB,EAAiB,MAAM,qBAAqB,CAAC;AACtE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAsElD;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,SAAS,EACjB,cAAc,EAAE,CAAC,UAAU,CAAC,EAAE,MAAM,KAAK,eAAe,EACxD,gCAAgC,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,EAAE,GAAG,SAAS,CAAC,EACvE,aAAa,CAAC,EAAE,gBAAgB,GAC/B,IAAI,CAylBN"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { replayMessageMetadata } from '@agent-relay/sdk';
|
|
1
|
+
import { downloadRelayFile, replayMessageMetadata, uploadRelayFile, } from '@agent-relay/sdk';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { compactDirectMessageReceipt, directMessageDeliveryFailure, directMessageReceipt, messageReadersReceipt, resolveExactAgentName, } from '../lib/message-delivery-receipts.js';
|
|
4
|
+
import { MAX_ATTACHMENT_BYTES, contentTypeFor, decodeBase64Strict, readAttachment, saveAttachment, } from '../lib/attachments.js';
|
|
4
5
|
import { jsonContent, jsonResult, textContent } from './tool-results.js';
|
|
5
6
|
import { identityOverrideInputShape, messageResult } from './tool-shapes.js';
|
|
6
|
-
import { McpRequestReplay } from './request-replay.js';
|
|
7
|
+
import { McpRequestReplay, replayScopeOf } from './request-replay.js';
|
|
7
8
|
const directMessageResult = z.looseObject({
|
|
8
9
|
id: z.string().optional().describe('Message ID, for "get_message_readers"'),
|
|
9
10
|
conversationId: z.string().optional().describe('DM conversation ID, addressing relay://dm/{id}'),
|
|
@@ -49,6 +50,22 @@ function resolveEmoji(input) {
|
|
|
49
50
|
};
|
|
50
51
|
return aliases[normalized] ?? input;
|
|
51
52
|
}
|
|
53
|
+
const idempotencyKeyInput = z
|
|
54
|
+
.string()
|
|
55
|
+
// Relaycast trims this key upstream, so trim before both the local replay
|
|
56
|
+
// cache and the forwarded call to keep one logical send on one key; a
|
|
57
|
+
// whitespace-only key would otherwise become an unkeyed send.
|
|
58
|
+
.trim()
|
|
59
|
+
.min(1)
|
|
60
|
+
.max(255)
|
|
61
|
+
.optional()
|
|
62
|
+
.describe('Stable key for retrying this same message after a lost response; use a new key for a new message. Surrounding whitespace is trimmed and a whitespace-only key is rejected.');
|
|
63
|
+
function requireFiles(client) {
|
|
64
|
+
if (!client.files) {
|
|
65
|
+
throw new Error('This Relay client cannot store files.');
|
|
66
|
+
}
|
|
67
|
+
return client.files;
|
|
68
|
+
}
|
|
52
69
|
/**
|
|
53
70
|
* Register the channel, message, thread, DM, reaction, search, and inbox MCP
|
|
54
71
|
* tools. These all act through a single agent client resolved per-call from the
|
|
@@ -165,11 +182,12 @@ export function registerMessagingTools(server, getAgentClient, listAgentsForReci
|
|
|
165
182
|
inputSchema: {
|
|
166
183
|
channel: z.string().describe('Channel name'),
|
|
167
184
|
text: z.string().describe('Message text'),
|
|
168
|
-
attachments: z.array(z.string()).optional().describe('File
|
|
185
|
+
attachments: z.array(z.string()).optional().describe('File IDs from "upload_file"'),
|
|
169
186
|
mode: z
|
|
170
187
|
.enum(['wait', 'steer'])
|
|
171
188
|
.optional()
|
|
172
189
|
.describe('wait (default): queue delivery until each recipient reaches a safe idle boundary; steer: request immediate injection, which may interrupt active work.'),
|
|
190
|
+
idempotency_key: idempotencyKeyInput,
|
|
173
191
|
...identityOverrideInputShape,
|
|
174
192
|
},
|
|
175
193
|
outputSchema: jsonResult,
|
|
@@ -179,11 +197,17 @@ export function registerMessagingTools(server, getAgentClient, listAgentsForReci
|
|
|
179
197
|
idempotentHint: false,
|
|
180
198
|
openWorldHint: true,
|
|
181
199
|
},
|
|
182
|
-
}, async ({ channel, text, attachments, mode, as }) =>
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
200
|
+
}, async ({ channel, text, attachments, mode, idempotency_key, as }, extra) => {
|
|
201
|
+
// Bind the acting identity now: a register_agent that moves the session
|
|
202
|
+
// default must not reroute a write that is already in flight.
|
|
203
|
+
const client = getAgentClient(as);
|
|
204
|
+
return replay.run('post_message', extra, idempotency_key, async () => jsonContent(await client.send(channel, text, {
|
|
205
|
+
attachments,
|
|
206
|
+
data: replayMessageMetadata(),
|
|
207
|
+
mode,
|
|
208
|
+
idempotencyKey: idempotency_key,
|
|
209
|
+
})), replayScopeOf(client));
|
|
210
|
+
});
|
|
187
211
|
server.registerTool('list_messages', {
|
|
188
212
|
title: 'Get Messages',
|
|
189
213
|
description: 'Retrieve message history from a channel. ' +
|
|
@@ -210,6 +234,7 @@ export function registerMessagingTools(server, getAgentClient, listAgentsForReci
|
|
|
210
234
|
inputSchema: {
|
|
211
235
|
message_id: z.string().describe('Parent message ID'),
|
|
212
236
|
text: z.string().describe('Reply text'),
|
|
237
|
+
idempotency_key: idempotencyKeyInput,
|
|
213
238
|
...identityOverrideInputShape,
|
|
214
239
|
},
|
|
215
240
|
outputSchema: jsonResult,
|
|
@@ -219,7 +244,13 @@ export function registerMessagingTools(server, getAgentClient, listAgentsForReci
|
|
|
219
244
|
idempotentHint: false,
|
|
220
245
|
openWorldHint: true,
|
|
221
246
|
},
|
|
222
|
-
}, async ({ message_id, text, as }) =>
|
|
247
|
+
}, async ({ message_id, text, idempotency_key, as }, extra) => {
|
|
248
|
+
const client = getAgentClient(as);
|
|
249
|
+
return replay.run('reply_to_thread', extra, idempotency_key, async () => jsonContent(await client.reply(message_id, text, {
|
|
250
|
+
data: replayMessageMetadata(),
|
|
251
|
+
idempotencyKey: idempotency_key,
|
|
252
|
+
})), replayScopeOf(client));
|
|
253
|
+
});
|
|
223
254
|
server.registerTool('get_message_thread', {
|
|
224
255
|
title: 'Get Thread',
|
|
225
256
|
description: 'Retrieve a message thread. ' +
|
|
@@ -249,17 +280,8 @@ export function registerMessagingTools(server, getAgentClient, listAgentsForReci
|
|
|
249
280
|
.enum(['wait', 'steer'])
|
|
250
281
|
.optional()
|
|
251
282
|
.describe('wait (default): queue until the recipient reaches a safe idle boundary; steer: request immediate injection, which may interrupt active work. Both modes return before reading is confirmed.'),
|
|
252
|
-
attachments: z.array(z.string()).optional().describe('File
|
|
253
|
-
idempotency_key:
|
|
254
|
-
.string()
|
|
255
|
-
// Relaycast trims this key upstream, so trim before both the local
|
|
256
|
-
// replay cache and the forwarded call to keep one logical send on one
|
|
257
|
-
// key; a whitespace-only key would otherwise become an unkeyed send.
|
|
258
|
-
.trim()
|
|
259
|
-
.min(1)
|
|
260
|
-
.max(255)
|
|
261
|
-
.optional()
|
|
262
|
-
.describe('Stable key for retrying this same message after a lost response; use a new key for a new message. Surrounding whitespace is trimmed and a whitespace-only key is rejected.'),
|
|
283
|
+
attachments: z.array(z.string()).optional().describe('File IDs from "upload_file"'),
|
|
284
|
+
idempotency_key: idempotencyKeyInput,
|
|
263
285
|
...identityOverrideInputShape,
|
|
264
286
|
},
|
|
265
287
|
outputSchema: directMessageResult,
|
|
@@ -269,19 +291,22 @@ export function registerMessagingTools(server, getAgentClient, listAgentsForReci
|
|
|
269
291
|
idempotentHint: false,
|
|
270
292
|
openWorldHint: true,
|
|
271
293
|
},
|
|
272
|
-
}, async ({ to, text, mode, attachments, idempotency_key, as }, extra) =>
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
294
|
+
}, async ({ to, text, mode, attachments, idempotency_key, as }, extra) => {
|
|
295
|
+
const client = getAgentClient(as);
|
|
296
|
+
return replay.run('send_dm', extra, idempotency_key, async () => {
|
|
297
|
+
const agents = await listAgentsForRecipientResolution?.();
|
|
298
|
+
const resolvedRecipient = agents ? resolveExactAgentName(agents, to) : undefined;
|
|
299
|
+
const message = await client.dm(to, text, {
|
|
300
|
+
idempotencyKey: idempotency_key,
|
|
301
|
+
mode,
|
|
302
|
+
attachments,
|
|
303
|
+
data: replayMessageMetadata(),
|
|
304
|
+
});
|
|
305
|
+
const receipt = compactDirectMessageReceipt(directMessageReceipt(message, to, mode, resolvedRecipient));
|
|
306
|
+
const result = jsonContent(receipt);
|
|
307
|
+
return directMessageDeliveryFailure(receipt) ? { ...result, isError: true } : result;
|
|
308
|
+
}, replayScopeOf(client));
|
|
309
|
+
});
|
|
285
310
|
server.registerTool('list_dms', {
|
|
286
311
|
title: 'List DM Conversations',
|
|
287
312
|
description: 'List direct message conversations for the current agent. ' +
|
|
@@ -302,6 +327,7 @@ export function registerMessagingTools(server, getAgentClient, listAgentsForReci
|
|
|
302
327
|
participants: z.array(z.string()).describe('Participant agent names'),
|
|
303
328
|
name: z.string().optional().describe('Optional group name'),
|
|
304
329
|
text: z.string().describe('Initial message'),
|
|
330
|
+
attachments: z.array(z.string()).optional().describe('File IDs from "upload_file"'),
|
|
305
331
|
...identityOverrideInputShape,
|
|
306
332
|
},
|
|
307
333
|
outputSchema: jsonResult,
|
|
@@ -311,14 +337,90 @@ export function registerMessagingTools(server, getAgentClient, listAgentsForReci
|
|
|
311
337
|
idempotentHint: false,
|
|
312
338
|
openWorldHint: true,
|
|
313
339
|
},
|
|
314
|
-
}, async ({ participants, name, text, as }) => {
|
|
340
|
+
}, async ({ participants, name, text, attachments, as }) => {
|
|
315
341
|
const client = getAgentClient(as);
|
|
316
342
|
const conversation = await client.dms.createGroup({ participants, name });
|
|
317
343
|
const message = await client.dms.sendMessage(conversation.id, text, {
|
|
344
|
+
attachments,
|
|
318
345
|
data: replayMessageMetadata(),
|
|
319
346
|
});
|
|
320
347
|
return jsonContent({ conversation, message });
|
|
321
348
|
});
|
|
349
|
+
server.registerTool('upload_file', {
|
|
350
|
+
title: 'Upload File',
|
|
351
|
+
description: 'Upload a file (for example a screenshot) so it can be attached to a message. ' +
|
|
352
|
+
'Pass a local `path`, or `content_base64` with `filename`. The bytes are stored before this returns, so the returned `id` is ready to pass in `attachments` to "send_dm", "post_message" or "send_group_dm". ' +
|
|
353
|
+
`Files are limited to ${MAX_ATTACHMENT_BYTES / (1024 * 1024)} MiB.`,
|
|
354
|
+
inputSchema: {
|
|
355
|
+
path: z.string().optional().describe('Local file path to upload'),
|
|
356
|
+
content_base64: z.string().optional().describe('File bytes, base64-encoded (instead of path)'),
|
|
357
|
+
filename: z.string().optional().describe('File name; defaults to the basename of path'),
|
|
358
|
+
content_type: z.string().optional().describe('MIME type; guessed from the file name when omitted'),
|
|
359
|
+
...identityOverrideInputShape,
|
|
360
|
+
},
|
|
361
|
+
outputSchema: jsonResult,
|
|
362
|
+
annotations: {
|
|
363
|
+
readOnlyHint: false,
|
|
364
|
+
destructiveHint: false,
|
|
365
|
+
idempotentHint: false,
|
|
366
|
+
openWorldHint: true,
|
|
367
|
+
},
|
|
368
|
+
}, async ({ path: filePath, content_base64, filename, content_type, as }) => {
|
|
369
|
+
if (filePath && content_base64 !== undefined) {
|
|
370
|
+
throw new Error('Pass either a local `path` or `content_base64`, not both.');
|
|
371
|
+
}
|
|
372
|
+
let file;
|
|
373
|
+
if (filePath) {
|
|
374
|
+
const read = await readAttachment(filePath);
|
|
375
|
+
file = {
|
|
376
|
+
...read,
|
|
377
|
+
filename: filename ?? read.filename,
|
|
378
|
+
contentType: content_type ?? read.contentType,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
else if (content_base64 !== undefined && filename) {
|
|
382
|
+
const data = decodeBase64Strict(content_base64);
|
|
383
|
+
if (data.byteLength > MAX_ATTACHMENT_BYTES) {
|
|
384
|
+
throw new Error(`Cannot attach ${filename}: ${data.byteLength} bytes exceeds the ${MAX_ATTACHMENT_BYTES}-byte limit.`);
|
|
385
|
+
}
|
|
386
|
+
file = { filename, contentType: content_type ?? contentTypeFor(filename), data };
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
throw new Error('Pass a local `path`, or `content_base64` with `filename`.');
|
|
390
|
+
}
|
|
391
|
+
const uploaded = await uploadRelayFile(requireFiles(getAgentClient(as)), file);
|
|
392
|
+
// Signed download URLs are bearer credentials. MCP callers need only
|
|
393
|
+
// the stable metadata and id used to attach the completed upload.
|
|
394
|
+
return jsonContent({
|
|
395
|
+
id: uploaded.id,
|
|
396
|
+
filename: uploaded.filename,
|
|
397
|
+
contentType: uploaded.contentType,
|
|
398
|
+
sizeBytes: uploaded.sizeBytes,
|
|
399
|
+
status: uploaded.status,
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
server.registerTool('download_file', {
|
|
403
|
+
title: 'Download File',
|
|
404
|
+
description: 'Download a message attachment to a local file and return its `path`, so you can open it (for example, read an attached screenshot). ' +
|
|
405
|
+
"Use the `file_id` (or `id`) from a message's attachments. By default the file is saved under .agent-relay/attachments/<file_id>/ in the working directory.",
|
|
406
|
+
inputSchema: {
|
|
407
|
+
file_id: z.string().describe('Attachment file ID'),
|
|
408
|
+
path: z.string().optional().describe('Output file or existing directory'),
|
|
409
|
+
...identityOverrideInputShape,
|
|
410
|
+
},
|
|
411
|
+
outputSchema: jsonResult,
|
|
412
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
413
|
+
}, async ({ file_id, path: out, as }) => {
|
|
414
|
+
const { file, data } = await downloadRelayFile(requireFiles(getAgentClient(as)), file_id);
|
|
415
|
+
const saved = await saveAttachment(file_id, file.filename, data, out);
|
|
416
|
+
return jsonContent({
|
|
417
|
+
id: file.id,
|
|
418
|
+
filename: file.filename,
|
|
419
|
+
contentType: file.contentType,
|
|
420
|
+
sizeBytes: data.byteLength,
|
|
421
|
+
path: saved,
|
|
422
|
+
});
|
|
423
|
+
});
|
|
322
424
|
server.registerTool('add_reaction', {
|
|
323
425
|
title: 'Add Reaction',
|
|
324
426
|
description: 'Add an emoji reaction to a message. ' +
|