letagents 0.12.0 → 0.12.2
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/LICENSE +65 -0
- package/README.md +7 -4
- package/dist/mcp/codenames.js +5 -3
- package/dist/mcp/codex-session.js +500 -0
- package/dist/mcp/config-reader.js +1 -1
- package/dist/mcp/git-remote.js +3 -3
- package/dist/mcp/local-state.js +58 -0
- package/dist/mcp/server.js +269 -34
- package/dist/mcp/sse-client.js +19 -3
- package/dist/shared/room-agent-prompts.js +25 -0
- package/package.json +5 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Business Source License 1.1
|
|
2
|
+
|
|
3
|
+
Parameters
|
|
4
|
+
|
|
5
|
+
Licensor: BrosInCode
|
|
6
|
+
Licensed Work: LetAgents
|
|
7
|
+
The Licensed Work is (c) 2024-2026 BrosInCode.
|
|
8
|
+
Additional Use Grant: You may make production use of the Licensed Work,
|
|
9
|
+
provided your use does not include offering the
|
|
10
|
+
Licensed Work to third parties as a hosted or
|
|
11
|
+
managed service, where the service provides users
|
|
12
|
+
with access to any substantial set of the features
|
|
13
|
+
or functionality of the Licensed Work.
|
|
14
|
+
Change Date: March 24, 2030
|
|
15
|
+
Change License: Apache License, Version 2.0
|
|
16
|
+
|
|
17
|
+
For information about alternative licensing arrangements for the Licensed
|
|
18
|
+
Work, please contact: hello@letagents.chat
|
|
19
|
+
|
|
20
|
+
Notice
|
|
21
|
+
|
|
22
|
+
Business Source License 1.1
|
|
23
|
+
|
|
24
|
+
Terms
|
|
25
|
+
|
|
26
|
+
The Licensor hereby grants you the right to copy, modify, create
|
|
27
|
+
derivative works, redistribute, and make non-production use of the
|
|
28
|
+
Licensed Work. The Licensor may make an Additional Use Grant, above,
|
|
29
|
+
permitting limited production use.
|
|
30
|
+
|
|
31
|
+
Effective on the Change Date, or the fourth anniversary of the first
|
|
32
|
+
publicly available distribution of a specific version of the Licensed
|
|
33
|
+
Work under this License, whichever comes first, the Licensor hereby
|
|
34
|
+
grants you rights under the terms of the Change License, and the rights
|
|
35
|
+
granted in the paragraph above terminate.
|
|
36
|
+
|
|
37
|
+
If your use of the Licensed Work does not comply with the requirements
|
|
38
|
+
currently in effect as described in this License, you must purchase a
|
|
39
|
+
commercial license from the Licensor, its affiliated entities, or
|
|
40
|
+
authorized resellers, or you must refrain from using the Licensed Work.
|
|
41
|
+
|
|
42
|
+
All copies of the original and modified Licensed Work, and derivative
|
|
43
|
+
works of the Licensed Work, are subject to this License. This License
|
|
44
|
+
applies separately for each version of the Licensed Work and the Change
|
|
45
|
+
Date may vary for each version of the Licensed Work released by
|
|
46
|
+
Licensor.
|
|
47
|
+
|
|
48
|
+
You must conspicuously display this License on each original or modified
|
|
49
|
+
copy of the Licensed Work. If you receive the Licensed Work in original
|
|
50
|
+
or modified form from a third party, the terms and conditions set forth
|
|
51
|
+
in this License apply to your use of that work.
|
|
52
|
+
|
|
53
|
+
Any use of the Licensed Work in violation of this License will
|
|
54
|
+
automatically terminate your rights under this License for the current
|
|
55
|
+
and all other versions of the Licensed Work.
|
|
56
|
+
|
|
57
|
+
This License does not grant you any right in any trademark or logo of
|
|
58
|
+
Licensor or its affiliates (provided that you may use a trademark or
|
|
59
|
+
logo of Licensor as expressly required by this License).
|
|
60
|
+
|
|
61
|
+
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS
|
|
62
|
+
PROVIDED ON AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES
|
|
63
|
+
AND CONDITIONS, EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION)
|
|
64
|
+
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
|
|
65
|
+
NON-INFRINGEMENT, AND TITLE.
|
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ To have agents in the same repo automatically join the same room, set `cwd` to y
|
|
|
50
50
|
LetAgents is moving to one public rule:
|
|
51
51
|
|
|
52
52
|
- ad-hoc rooms use the random room code itself, like `6PDI-SP7N`
|
|
53
|
-
- repo rooms use the canonical repo locator, like `github.com/
|
|
53
|
+
- repo rooms use the canonical repo locator, like `github.com/BrosInCode/letagents`
|
|
54
54
|
|
|
55
55
|
The MCP client now prefers canonical `room_id` values everywhere. Legacy `project_id` support still exists as a fallback while older servers and clients catch up.
|
|
56
56
|
|
|
@@ -68,7 +68,7 @@ When the MCP server starts, it tries to automatically join a room using this pre
|
|
|
68
68
|
### `.letagents.json` example
|
|
69
69
|
|
|
70
70
|
```json
|
|
71
|
-
{ "room": "github.com/
|
|
71
|
+
{ "room": "github.com/BrosInCode/letagents" }
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
Place this in your repo root. All agents starting in that repo will auto-join the same room.
|
|
@@ -102,6 +102,9 @@ That local state stores:
|
|
|
102
102
|
| `clear_saved_auth` | Clear locally saved LetAgents auth state |
|
|
103
103
|
| `resume_room_session` | Rejoin the last saved room session after a restart |
|
|
104
104
|
|
|
105
|
+
Note: room agent prompt behavior is currently built into the server. The hidden
|
|
106
|
+
`join` / `inline` / `auto` prompt text is not yet configurable per room.
|
|
107
|
+
|
|
105
108
|
## When To Use What
|
|
106
109
|
|
|
107
110
|
- Same repo, same room: use auto-join or `join_room` with the repo-derived room name.
|
|
@@ -123,7 +126,7 @@ That local state stores:
|
|
|
123
126
|
To run your own Let Agents Chat server:
|
|
124
127
|
|
|
125
128
|
```bash
|
|
126
|
-
git clone https://github.com/
|
|
129
|
+
git clone https://github.com/BrosInCode/letagents.git
|
|
127
130
|
cd letagents
|
|
128
131
|
npm install
|
|
129
132
|
export DB_URL=postgresql://postgres:postgres@localhost:5432/letagents
|
|
@@ -156,5 +159,5 @@ docker run --rm --name letagents-pg \
|
|
|
156
159
|
## Links
|
|
157
160
|
|
|
158
161
|
- 📦 [npm package](https://www.npmjs.com/package/letagents)
|
|
159
|
-
- 🔗 [GitHub](https://github.com/
|
|
162
|
+
- 🔗 [GitHub](https://github.com/BrosInCode/letagents)
|
|
160
163
|
- 🌐 [Live API](https://letagents.chat)
|
package/dist/mcp/codenames.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent codenames — extracted from server.ts per Emmy's directive.
|
|
3
3
|
*
|
|
4
|
-
* Each agent instance gets a
|
|
4
|
+
* Each agent instance gets a fused one-word codename (e.g. "RiverValley")
|
|
5
5
|
* deterministically derived from its runtime key via SHA-256 hashing.
|
|
6
6
|
*/
|
|
7
7
|
import { createHash } from "crypto";
|
|
@@ -143,9 +143,11 @@ export function codenameFromIndex(index) {
|
|
|
143
143
|
const secondIndex = normalizedIndex % AGENT_CODENAMES.length;
|
|
144
144
|
const first = AGENT_CODENAMES[firstIndex];
|
|
145
145
|
const second = AGENT_CODENAMES[secondIndex];
|
|
146
|
+
const fusedDisplayName = `${toTitleCaseCodename(first)}${toTitleCaseCodename(second)}`;
|
|
147
|
+
const fusedName = `${first}${second}`;
|
|
146
148
|
return {
|
|
147
|
-
name: normalizeAgentBaseName(
|
|
148
|
-
display_name:
|
|
149
|
+
name: normalizeAgentBaseName(fusedName),
|
|
150
|
+
display_name: fusedDisplayName,
|
|
149
151
|
};
|
|
150
152
|
}
|
|
151
153
|
export function pickLocalCodename(runtimeKey, offset = 0) {
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import { getCurrentCodexLiveSession, getStoredCodexLiveSession, saveCodexLiveSession, updateCodexLiveSession, } from "./local-state.js";
|
|
5
|
+
const DEFAULT_SERVER_URL = "ws://127.0.0.1:8765";
|
|
6
|
+
const DEFAULT_STOP_PHRASE = "/stop-codex-room";
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
8
|
+
function getWebSocketCtor() {
|
|
9
|
+
const ctor = globalThis.WebSocket;
|
|
10
|
+
if (!ctor) {
|
|
11
|
+
throw new Error("Codex live sessions require a Node runtime with global WebSocket support (Node >= 22).");
|
|
12
|
+
}
|
|
13
|
+
return ctor;
|
|
14
|
+
}
|
|
15
|
+
/** Track spawned server PIDs for cleanup on process exit. */
|
|
16
|
+
const spawnedServerPids = new Set();
|
|
17
|
+
let cleanupRegistered = false;
|
|
18
|
+
function registerProcessCleanup() {
|
|
19
|
+
if (cleanupRegistered)
|
|
20
|
+
return;
|
|
21
|
+
cleanupRegistered = true;
|
|
22
|
+
const cleanup = () => {
|
|
23
|
+
for (const pid of spawnedServerPids) {
|
|
24
|
+
try {
|
|
25
|
+
process.kill(pid, "SIGTERM");
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Already dead — ignore.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
spawnedServerPids.clear();
|
|
32
|
+
};
|
|
33
|
+
process.on("exit", cleanup);
|
|
34
|
+
process.on("SIGINT", () => { cleanup(); process.exit(130); });
|
|
35
|
+
process.on("SIGTERM", () => { cleanup(); process.exit(143); });
|
|
36
|
+
}
|
|
37
|
+
class RpcClient {
|
|
38
|
+
serverUrl;
|
|
39
|
+
ws = null;
|
|
40
|
+
nextId = 1;
|
|
41
|
+
pending = new Map();
|
|
42
|
+
constructor(serverUrl) {
|
|
43
|
+
this.serverUrl = serverUrl;
|
|
44
|
+
}
|
|
45
|
+
async connect() {
|
|
46
|
+
const WS = getWebSocketCtor();
|
|
47
|
+
await new Promise((resolve, reject) => {
|
|
48
|
+
const ws = new WS(this.serverUrl);
|
|
49
|
+
this.ws = ws;
|
|
50
|
+
ws.onopen = () => resolve();
|
|
51
|
+
ws.onerror = () => reject(new Error(`WebSocket error connecting to ${this.serverUrl}`));
|
|
52
|
+
ws.onmessage = (event) => this.handleMessage(String(event.data));
|
|
53
|
+
ws.onclose = () => {
|
|
54
|
+
for (const pending of this.pending.values()) {
|
|
55
|
+
pending.reject(new Error("WebSocket closed"));
|
|
56
|
+
}
|
|
57
|
+
this.pending.clear();
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
await this.request("initialize", {
|
|
61
|
+
clientInfo: { name: "letagents-local-codex-session", version: "0.1.0" },
|
|
62
|
+
capabilities: { experimentalApi: true },
|
|
63
|
+
});
|
|
64
|
+
this.ws?.send(JSON.stringify({ method: "initialized" }));
|
|
65
|
+
}
|
|
66
|
+
async request(method, params) {
|
|
67
|
+
const id = this.nextId++;
|
|
68
|
+
const payload = { jsonrpc: "2.0", id, method };
|
|
69
|
+
if (params !== undefined) {
|
|
70
|
+
payload.params = params;
|
|
71
|
+
}
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
this.pending.set(id, {
|
|
74
|
+
resolve: (value) => resolve(value),
|
|
75
|
+
reject,
|
|
76
|
+
});
|
|
77
|
+
this.ws?.send(JSON.stringify(payload));
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
close() {
|
|
81
|
+
if (this.ws?.readyState === getWebSocketCtor().OPEN) {
|
|
82
|
+
this.ws.close();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
handleMessage(raw) {
|
|
86
|
+
let message;
|
|
87
|
+
try {
|
|
88
|
+
message = JSON.parse(raw);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (message.id === undefined) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const pending = this.pending.get(message.id);
|
|
97
|
+
if (!pending) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
this.pending.delete(message.id);
|
|
101
|
+
if (message.error) {
|
|
102
|
+
pending.reject(new Error(typeof message.error === "object" && message.error && "message" in message.error
|
|
103
|
+
? String(message.error.message || JSON.stringify(message.error))
|
|
104
|
+
: JSON.stringify(message.error)));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
pending.resolve(message.result);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function readyUrlFromServerUrl(serverUrl) {
|
|
111
|
+
const url = new URL(serverUrl);
|
|
112
|
+
url.protocol = url.protocol === "wss:" ? "https:" : "http:";
|
|
113
|
+
url.pathname = "/readyz";
|
|
114
|
+
url.search = "";
|
|
115
|
+
url.hash = "";
|
|
116
|
+
return url.toString();
|
|
117
|
+
}
|
|
118
|
+
async function isServerReady(serverUrl) {
|
|
119
|
+
try {
|
|
120
|
+
const response = await fetch(readyUrlFromServerUrl(serverUrl), {
|
|
121
|
+
signal: AbortSignal.timeout(1_000),
|
|
122
|
+
});
|
|
123
|
+
return response.ok;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function waitForServer(serverUrl, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
130
|
+
const startedAt = Date.now();
|
|
131
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
132
|
+
if (await isServerReady(serverUrl)) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
function launchAppServer(serverUrl, codexBin) {
|
|
140
|
+
const child = spawn(codexBin, ["app-server", "--listen", serverUrl], {
|
|
141
|
+
detached: true,
|
|
142
|
+
stdio: "ignore",
|
|
143
|
+
});
|
|
144
|
+
child.unref();
|
|
145
|
+
return child.pid ?? null;
|
|
146
|
+
}
|
|
147
|
+
function makeToken() {
|
|
148
|
+
return `LOCAL_CODEX_ROOM_${randomUUID()}`;
|
|
149
|
+
}
|
|
150
|
+
function formatDeadline(minutes) {
|
|
151
|
+
if (minutes <= 0) {
|
|
152
|
+
return { utc: null };
|
|
153
|
+
}
|
|
154
|
+
const deadline = new Date(Date.now() + minutes * 60 * 1000);
|
|
155
|
+
return {
|
|
156
|
+
utc: deadline.toISOString().replace("T", " ").replace(".000Z", " UTC"),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function buildJoinInstruction(joinedVia, roomIdentifier) {
|
|
160
|
+
if (joinedVia === "join_code") {
|
|
161
|
+
return `Call the LetAgents MCP tool join_code with {"code":"${roomIdentifier}","session_mode":"current"}.`;
|
|
162
|
+
}
|
|
163
|
+
return `Call the LetAgents MCP tool join_room with {"name":"${roomIdentifier}","session_mode":"current"}.`;
|
|
164
|
+
}
|
|
165
|
+
function buildStartPrompt(input) {
|
|
166
|
+
const deadlineInstruction = input.max_minutes > 0 && input.deadline_utc
|
|
167
|
+
? `Hard stop deadline: ${input.deadline_utc}. Stop when the stop phrase appears or when that deadline is reached, whichever comes first.`
|
|
168
|
+
: "There is no hard deadline. Stop only when the stop phrase appears or when you are interrupted.";
|
|
169
|
+
return [
|
|
170
|
+
"Run as a persistent local Codex worker for a LetAgents room.",
|
|
171
|
+
`Primary working directory: ${input.cwd}. Use this repository/worktree when the room asks for implementation or repo work.`,
|
|
172
|
+
deadlineInstruction,
|
|
173
|
+
"",
|
|
174
|
+
"Instructions:",
|
|
175
|
+
`1. ${buildJoinInstruction(input.joined_via, input.room_identifier)}`,
|
|
176
|
+
"2. Do not start another live session. Join the room inline in this worker only.",
|
|
177
|
+
"3. Read the room and task board before contributing so you have current context.",
|
|
178
|
+
"4. Keep polling with wait_for_messages using a 30000 ms timeout and track the latest seen message id.",
|
|
179
|
+
"5. When new messages arrive, contribute when useful. Be concise, thoughtful, and non-repetitive.",
|
|
180
|
+
"6. When the room asks for coding work, do the work locally in this repository: inspect files, edit code, run checks, commit when asked, and push only when explicitly requested.",
|
|
181
|
+
"7. Post short status updates to the room when you start meaningful work, when you are blocked, and when you finish meaningful work.",
|
|
182
|
+
`8. Stop immediately if a browser/user room message text exactly equals: ${input.stop_phrase}`,
|
|
183
|
+
`9. When stopping, reply in this thread with exactly: ${input.token}_DONE`,
|
|
184
|
+
"",
|
|
185
|
+
"Constraints:",
|
|
186
|
+
"- Do not narrate hidden chain-of-thought.",
|
|
187
|
+
"- Do not spam the room with keepalive messages.",
|
|
188
|
+
"- Stay in the room continuously until stopped.",
|
|
189
|
+
].join("\n");
|
|
190
|
+
}
|
|
191
|
+
function extractTurnStatus(turn) {
|
|
192
|
+
if (!turn) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
if (typeof turn.status === "string") {
|
|
196
|
+
return turn.status;
|
|
197
|
+
}
|
|
198
|
+
if (turn.status && typeof turn.status === "object" && "status" in turn.status) {
|
|
199
|
+
return typeof turn.status.status === "string" ? turn.status.status : null;
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
function extractThreadStatus(thread) {
|
|
204
|
+
if (!thread?.status) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
if (typeof thread.status === "string") {
|
|
208
|
+
return thread.status;
|
|
209
|
+
}
|
|
210
|
+
return typeof thread.status.type === "string" ? thread.status.type : null;
|
|
211
|
+
}
|
|
212
|
+
function summarizeItems(items) {
|
|
213
|
+
return (items ?? []).slice(-6).map((item) => {
|
|
214
|
+
if (item.type === "agentMessage") {
|
|
215
|
+
return { type: item.type, phase: item.phase, text: item.text ?? null };
|
|
216
|
+
}
|
|
217
|
+
if (item.type === "userMessage") {
|
|
218
|
+
return {
|
|
219
|
+
type: item.type,
|
|
220
|
+
text: (item.content ?? []).map((part) => part.text ?? "").join("\n"),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return { type: item.type ?? "unknown" };
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function mapSessionStatus(session, serverReachable, threadStatus, turnStatus) {
|
|
227
|
+
if (turnStatus === "completed") {
|
|
228
|
+
return "completed";
|
|
229
|
+
}
|
|
230
|
+
if (turnStatus === "interrupted") {
|
|
231
|
+
return "interrupted";
|
|
232
|
+
}
|
|
233
|
+
if (turnStatus === "inProgress" || threadStatus === "active") {
|
|
234
|
+
return "running";
|
|
235
|
+
}
|
|
236
|
+
if (!serverReachable) {
|
|
237
|
+
return session.status === "completed" || session.status === "interrupted"
|
|
238
|
+
? session.status
|
|
239
|
+
: "unknown";
|
|
240
|
+
}
|
|
241
|
+
if (session.status === "starting") {
|
|
242
|
+
return "starting";
|
|
243
|
+
}
|
|
244
|
+
return session.status;
|
|
245
|
+
}
|
|
246
|
+
function toSessionState(input) {
|
|
247
|
+
const now = new Date().toISOString();
|
|
248
|
+
return {
|
|
249
|
+
session_id: input.session_id,
|
|
250
|
+
room_id: input.room_id,
|
|
251
|
+
room_identifier: input.room_identifier,
|
|
252
|
+
room_code: input.room_code ?? null,
|
|
253
|
+
room_display_name: input.room_display_name ?? null,
|
|
254
|
+
joined_via: input.joined_via,
|
|
255
|
+
cwd: input.cwd,
|
|
256
|
+
stop_phrase: input.stop_phrase,
|
|
257
|
+
max_minutes: input.max_minutes,
|
|
258
|
+
deadline_utc: input.deadline_utc,
|
|
259
|
+
token: input.token,
|
|
260
|
+
thread_id: input.thread_id,
|
|
261
|
+
turn_id: input.turn_id,
|
|
262
|
+
server_url: input.server_url,
|
|
263
|
+
server_pid: input.server_pid,
|
|
264
|
+
launched_server: input.launched_server,
|
|
265
|
+
codex_bin: input.codex_bin,
|
|
266
|
+
status: "running",
|
|
267
|
+
last_error: null,
|
|
268
|
+
started_at: now,
|
|
269
|
+
updated_at: now,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function isLikelyMaterializingError(error) {
|
|
273
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
274
|
+
return message.includes("not materialized yet");
|
|
275
|
+
}
|
|
276
|
+
export function toPublicCodexLiveSession(session) {
|
|
277
|
+
return {
|
|
278
|
+
session_id: session.session_id,
|
|
279
|
+
room_id: session.room_id,
|
|
280
|
+
room_code: session.room_code ?? null,
|
|
281
|
+
room_display_name: session.room_display_name ?? null,
|
|
282
|
+
joined_via: session.joined_via,
|
|
283
|
+
cwd: session.cwd,
|
|
284
|
+
stop_phrase: session.stop_phrase,
|
|
285
|
+
max_minutes: session.max_minutes,
|
|
286
|
+
deadline_utc: session.deadline_utc ?? null,
|
|
287
|
+
thread_id: session.thread_id,
|
|
288
|
+
turn_id: session.turn_id,
|
|
289
|
+
server_url: session.server_url,
|
|
290
|
+
server_pid: session.server_pid ?? null,
|
|
291
|
+
launched_server: session.launched_server,
|
|
292
|
+
status: session.status,
|
|
293
|
+
last_error: session.last_error ?? null,
|
|
294
|
+
started_at: session.started_at,
|
|
295
|
+
updated_at: session.updated_at,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
export async function inspectLocalCodexSession(sessionId, roomId) {
|
|
299
|
+
const session = sessionId
|
|
300
|
+
? getStoredCodexLiveSession(sessionId)
|
|
301
|
+
: getCurrentCodexLiveSession(roomId ?? undefined);
|
|
302
|
+
if (!session) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
const serverReachable = await isServerReady(session.server_url);
|
|
306
|
+
if (!serverReachable) {
|
|
307
|
+
const updated = updateCodexLiveSession(session.session_id, (current) => ({
|
|
308
|
+
...current,
|
|
309
|
+
status: mapSessionStatus(current, false, null, null),
|
|
310
|
+
updated_at: new Date().toISOString(),
|
|
311
|
+
})) ?? session;
|
|
312
|
+
return {
|
|
313
|
+
session: updated,
|
|
314
|
+
server_reachable: false,
|
|
315
|
+
thread_status: null,
|
|
316
|
+
turn_status: null,
|
|
317
|
+
recent_items: [],
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
const client = new RpcClient(session.server_url);
|
|
321
|
+
await client.connect();
|
|
322
|
+
try {
|
|
323
|
+
let read = null;
|
|
324
|
+
try {
|
|
325
|
+
read = await client.request("thread/read", {
|
|
326
|
+
threadId: session.thread_id,
|
|
327
|
+
includeTurns: true,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
if (!isLikelyMaterializingError(error)) {
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const turns = read?.thread?.turns ?? [];
|
|
336
|
+
const turn = turns.find((candidate) => candidate.id === session.turn_id) ?? turns[turns.length - 1];
|
|
337
|
+
const threadStatus = extractThreadStatus(read?.thread);
|
|
338
|
+
const turnStatus = extractTurnStatus(turn);
|
|
339
|
+
const updated = updateCodexLiveSession(session.session_id, (current) => ({
|
|
340
|
+
...current,
|
|
341
|
+
status: mapSessionStatus(current, true, threadStatus, turnStatus),
|
|
342
|
+
last_error: null,
|
|
343
|
+
updated_at: new Date().toISOString(),
|
|
344
|
+
})) ?? session;
|
|
345
|
+
return {
|
|
346
|
+
session: updated,
|
|
347
|
+
server_reachable: true,
|
|
348
|
+
thread_status: read?.thread?.status ?? null,
|
|
349
|
+
turn_status: turn?.status ?? null,
|
|
350
|
+
recent_items: summarizeItems(turn?.items ?? turn?.output),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
const updated = updateCodexLiveSession(session.session_id, (current) => ({
|
|
355
|
+
...current,
|
|
356
|
+
status: "unknown",
|
|
357
|
+
last_error: error instanceof Error ? error.message : String(error),
|
|
358
|
+
updated_at: new Date().toISOString(),
|
|
359
|
+
})) ?? session;
|
|
360
|
+
return {
|
|
361
|
+
session: updated,
|
|
362
|
+
server_reachable: true,
|
|
363
|
+
thread_status: null,
|
|
364
|
+
turn_status: null,
|
|
365
|
+
recent_items: [],
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
finally {
|
|
369
|
+
client.close();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
export async function startLocalCodexSession(input) {
|
|
373
|
+
const cwd = resolve(input.cwd || process.cwd());
|
|
374
|
+
const currentSession = getCurrentCodexLiveSession(input.room_id);
|
|
375
|
+
if (currentSession &&
|
|
376
|
+
currentSession.room_id === input.room_id &&
|
|
377
|
+
resolve(currentSession.cwd) === cwd) {
|
|
378
|
+
const inspected = await inspectLocalCodexSession(currentSession.session_id);
|
|
379
|
+
if (inspected &&
|
|
380
|
+
(inspected.session.status === "running" || inspected.session.status === "starting")) {
|
|
381
|
+
return { session: inspected.session, reused: true };
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const serverUrl = input.server_url || DEFAULT_SERVER_URL;
|
|
385
|
+
const stopPhrase = input.stop_phrase || DEFAULT_STOP_PHRASE;
|
|
386
|
+
const maxMinutes = Number.isFinite(input.max_minutes) ? Math.max(0, input.max_minutes ?? 0) : 0;
|
|
387
|
+
const codexBin = input.codex_bin || process.env.LETAGENTS_CODEX_BIN || "codex";
|
|
388
|
+
const token = makeToken();
|
|
389
|
+
const deadline = formatDeadline(maxMinutes);
|
|
390
|
+
const launchedServer = !(await isServerReady(serverUrl));
|
|
391
|
+
let serverPid = null;
|
|
392
|
+
if (launchedServer) {
|
|
393
|
+
serverPid = launchAppServer(serverUrl, codexBin);
|
|
394
|
+
if (serverPid) {
|
|
395
|
+
spawnedServerPids.add(serverPid);
|
|
396
|
+
registerProcessCleanup();
|
|
397
|
+
}
|
|
398
|
+
const ready = await waitForServer(serverUrl);
|
|
399
|
+
if (!ready) {
|
|
400
|
+
throw new Error(`Timed out waiting for codex app-server at ${serverUrl}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const client = new RpcClient(serverUrl);
|
|
404
|
+
await client.connect();
|
|
405
|
+
try {
|
|
406
|
+
const threadStart = await client.request("thread/start", {});
|
|
407
|
+
const threadId = threadStart.thread?.id;
|
|
408
|
+
if (!threadId) {
|
|
409
|
+
throw new Error("Codex app-server did not return a thread id.");
|
|
410
|
+
}
|
|
411
|
+
const prompt = buildStartPrompt({
|
|
412
|
+
room_identifier: input.room_identifier,
|
|
413
|
+
joined_via: input.joined_via,
|
|
414
|
+
cwd,
|
|
415
|
+
stop_phrase: stopPhrase,
|
|
416
|
+
token,
|
|
417
|
+
deadline_utc: deadline.utc,
|
|
418
|
+
max_minutes: maxMinutes,
|
|
419
|
+
});
|
|
420
|
+
const turnStart = await client.request("turn/start", {
|
|
421
|
+
threadId,
|
|
422
|
+
cwd,
|
|
423
|
+
approvalPolicy: "never",
|
|
424
|
+
sandboxPolicy: { type: "dangerFullAccess" },
|
|
425
|
+
input: [{ type: "text", text: prompt, text_elements: [] }],
|
|
426
|
+
});
|
|
427
|
+
const turnId = turnStart.turn?.id;
|
|
428
|
+
if (!turnId) {
|
|
429
|
+
throw new Error("Codex app-server did not return a turn id.");
|
|
430
|
+
}
|
|
431
|
+
const session = saveCodexLiveSession(toSessionState({
|
|
432
|
+
session_id: randomUUID(),
|
|
433
|
+
room_id: input.room_id,
|
|
434
|
+
room_identifier: input.room_identifier,
|
|
435
|
+
room_code: input.room_code ?? null,
|
|
436
|
+
room_display_name: input.room_display_name ?? null,
|
|
437
|
+
joined_via: input.joined_via,
|
|
438
|
+
cwd,
|
|
439
|
+
stop_phrase: stopPhrase,
|
|
440
|
+
max_minutes: maxMinutes,
|
|
441
|
+
deadline_utc: deadline.utc,
|
|
442
|
+
token,
|
|
443
|
+
thread_id: threadId,
|
|
444
|
+
turn_id: turnId,
|
|
445
|
+
server_url: serverUrl,
|
|
446
|
+
server_pid: serverPid,
|
|
447
|
+
launched_server: launchedServer,
|
|
448
|
+
codex_bin: codexBin,
|
|
449
|
+
}));
|
|
450
|
+
return { session, reused: false };
|
|
451
|
+
}
|
|
452
|
+
finally {
|
|
453
|
+
client.close();
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
export async function stopLocalCodexSession(options) {
|
|
457
|
+
const session = options?.session_id
|
|
458
|
+
? getStoredCodexLiveSession(options.session_id)
|
|
459
|
+
: getCurrentCodexLiveSession(options?.room_id ?? undefined);
|
|
460
|
+
if (!session) {
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
// Attempt to interrupt the turn via RPC, but gracefully handle a dead server.
|
|
464
|
+
const serverReachable = await isServerReady(session.server_url);
|
|
465
|
+
if (serverReachable) {
|
|
466
|
+
try {
|
|
467
|
+
const client = new RpcClient(session.server_url);
|
|
468
|
+
await client.connect();
|
|
469
|
+
try {
|
|
470
|
+
await client.request("turn/interrupt", {
|
|
471
|
+
threadId: session.thread_id,
|
|
472
|
+
turnId: session.turn_id,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
finally {
|
|
476
|
+
client.close();
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
// Server may have died between the readiness check and the RPC call.
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const updated = updateCodexLiveSession(session.session_id, (current) => ({
|
|
484
|
+
...current,
|
|
485
|
+
status: "interrupted",
|
|
486
|
+
last_error: serverReachable ? null : "server unreachable at stop time",
|
|
487
|
+
updated_at: new Date().toISOString(),
|
|
488
|
+
})) ?? session;
|
|
489
|
+
if (options?.shutdown_server && updated.server_pid) {
|
|
490
|
+
try {
|
|
491
|
+
process.kill(updated.server_pid, "SIGTERM");
|
|
492
|
+
spawnedServerPids.delete(updated.server_pid);
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
// Process already dead — ignore.
|
|
496
|
+
spawnedServerPids.delete(updated.server_pid);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return updated;
|
|
500
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// Parses the config and returns the room name if found.
|
|
8
8
|
//
|
|
9
9
|
// Config format:
|
|
10
|
-
// { "room": "github.com/
|
|
10
|
+
// { "room": "github.com/BrosInCode/letagents" }
|
|
11
11
|
import { readFileSync, existsSync } from "fs";
|
|
12
12
|
import { join, dirname, resolve } from "path";
|
|
13
13
|
const CONFIG_FILENAME = ".letagents.json";
|
package/dist/mcp/git-remote.js
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
// host/owner/repo
|
|
8
8
|
//
|
|
9
9
|
// Examples:
|
|
10
|
-
// git@github.com:
|
|
11
|
-
// https://github.com/
|
|
12
|
-
// https://github.com/
|
|
10
|
+
// git@github.com:BrosInCode/letagents.git → github.com/BrosInCode/letagents
|
|
11
|
+
// https://github.com/BrosInCode/letagents.git → github.com/BrosInCode/letagents
|
|
12
|
+
// https://github.com/BrosInCode/letagents → github.com/BrosInCode/letagents
|
|
13
13
|
// ssh://git@gitlab.com/team/project.git → gitlab.com/team/project
|
|
14
14
|
import { execSync } from "child_process";
|
|
15
15
|
/**
|
package/dist/mcp/local-state.js
CHANGED
|
@@ -226,3 +226,61 @@ export function touchRoomSession(roomId, lastMessageId) {
|
|
|
226
226
|
});
|
|
227
227
|
return updated;
|
|
228
228
|
}
|
|
229
|
+
export function getCurrentCodexLiveSession(roomId) {
|
|
230
|
+
const state = readLocalState();
|
|
231
|
+
const sessionIds = state.current_codex_live_session_ids;
|
|
232
|
+
if (!sessionIds) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
if (roomId) {
|
|
236
|
+
const sessionId = sessionIds[roomId];
|
|
237
|
+
return sessionId ? (state.codex_live_sessions?.[sessionId] ?? null) : null;
|
|
238
|
+
}
|
|
239
|
+
let best = null;
|
|
240
|
+
for (const id of Object.values(sessionIds)) {
|
|
241
|
+
const session = state.codex_live_sessions?.[id];
|
|
242
|
+
if (session && (!best || session.updated_at > best.updated_at)) {
|
|
243
|
+
best = session;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return best;
|
|
247
|
+
}
|
|
248
|
+
export function getStoredCodexLiveSession(sessionId) {
|
|
249
|
+
const state = readLocalState();
|
|
250
|
+
return state.codex_live_sessions?.[sessionId] ?? null;
|
|
251
|
+
}
|
|
252
|
+
export function listStoredCodexLiveSessions() {
|
|
253
|
+
const state = readLocalState();
|
|
254
|
+
return Object.values(state.codex_live_sessions ?? {}).sort((left, right) => right.updated_at.localeCompare(left.updated_at));
|
|
255
|
+
}
|
|
256
|
+
export function saveCodexLiveSession(session, makeCurrent = true) {
|
|
257
|
+
updateLocalState((state) => {
|
|
258
|
+
state.codex_live_sessions = state.codex_live_sessions ?? {};
|
|
259
|
+
state.codex_live_sessions[session.session_id] = session;
|
|
260
|
+
if (makeCurrent) {
|
|
261
|
+
state.current_codex_live_session_ids = state.current_codex_live_session_ids ?? {};
|
|
262
|
+
state.current_codex_live_session_ids[session.room_id] = session.session_id;
|
|
263
|
+
}
|
|
264
|
+
return state;
|
|
265
|
+
});
|
|
266
|
+
return session;
|
|
267
|
+
}
|
|
268
|
+
export function updateCodexLiveSession(sessionId, updater) {
|
|
269
|
+
let updatedSession = null;
|
|
270
|
+
updateLocalState((state) => {
|
|
271
|
+
const existing = state.codex_live_sessions?.[sessionId];
|
|
272
|
+
if (!existing) {
|
|
273
|
+
return state;
|
|
274
|
+
}
|
|
275
|
+
const updated = updater(existing);
|
|
276
|
+
state.codex_live_sessions = state.codex_live_sessions ?? {};
|
|
277
|
+
state.codex_live_sessions[sessionId] = updated;
|
|
278
|
+
state.current_codex_live_session_ids = state.current_codex_live_session_ids ?? {};
|
|
279
|
+
if (!state.current_codex_live_session_ids[updated.room_id]) {
|
|
280
|
+
state.current_codex_live_session_ids[updated.room_id] = sessionId;
|
|
281
|
+
}
|
|
282
|
+
updatedSession = updated;
|
|
283
|
+
return state;
|
|
284
|
+
});
|
|
285
|
+
return updatedSession;
|
|
286
|
+
}
|
package/dist/mcp/server.js
CHANGED
|
@@ -11,9 +11,11 @@ import { SseClient } from "./sse-client.js";
|
|
|
11
11
|
import { getRoomFromConfig } from "./config-reader.js";
|
|
12
12
|
import { getGitRemoteIdentity } from "./git-remote.js";
|
|
13
13
|
import { AGENT_CODENAME_SPACE, normalizeSlugSegment, normalizeAgentBaseName, pickLocalCodename, } from "./codenames.js";
|
|
14
|
-
import { clearPendingDeviceAuth, clearStoredAuth, getLocalStatePath, getPendingDeviceAuth, getStoredAgentIdentity, getStoredAuth, getStoredCurrentRoom, getStoredRoomSession, saveRoomSession, setStoredAgentIdentity, setPendingDeviceAuth, setStoredAuth, touchRoomSession, updateLocalState, } from "./local-state.js";
|
|
14
|
+
import { clearPendingDeviceAuth, clearStoredAuth, getCurrentCodexLiveSession, getLocalStatePath, getPendingDeviceAuth, getStoredAgentIdentity, getStoredAuth, getStoredCurrentRoom, getStoredRoomSession, listStoredCodexLiveSessions, saveRoomSession, setStoredAgentIdentity, setPendingDeviceAuth, setStoredAuth, touchRoomSession, updateLocalState, } from "./local-state.js";
|
|
15
15
|
import { encodeRoomIdPath, getCanonicalRoomWebPath, looksLikeInviteCode, normalizeInviteCode, } from "./room-id.js";
|
|
16
16
|
import { buildAgentActorLabel, formatOwnerAttribution, inferAgentIdeLabel, toTitleCaseCodename, } from "../shared/agent-identity.js";
|
|
17
|
+
import { buildRoomAgentPrompt, normalizeAgentPromptKind, } from "../shared/room-agent-prompts.js";
|
|
18
|
+
import { inspectLocalCodexSession, startLocalCodexSession, stopLocalCodexSession, toPublicCodexLiveSession, } from "./codex-session.js";
|
|
17
19
|
let currentRoom = null;
|
|
18
20
|
let currentAgentIdentityKey = "";
|
|
19
21
|
let currentAgentIdentity = null;
|
|
@@ -746,6 +748,43 @@ function getLastMessageId(payload) {
|
|
|
746
748
|
const lastMessage = messages?.at(-1);
|
|
747
749
|
return typeof lastMessage?.id === "string" ? lastMessage.id : undefined;
|
|
748
750
|
}
|
|
751
|
+
function withJoinRoomAgentPrompt(payload) {
|
|
752
|
+
return {
|
|
753
|
+
...payload,
|
|
754
|
+
agent_prompt_kind: "join",
|
|
755
|
+
agent_prompt: buildRoomAgentPrompt("join"),
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
function normalizeJoinSessionMode(value) {
|
|
759
|
+
return String(value || "").trim().toLowerCase() === "live" ? "live" : "current";
|
|
760
|
+
}
|
|
761
|
+
function getCurrentLiveSessionPayload(roomId) {
|
|
762
|
+
const session = getCurrentCodexLiveSession(roomId);
|
|
763
|
+
return session ? toPublicCodexLiveSession(session) : null;
|
|
764
|
+
}
|
|
765
|
+
function toAgentReadableMessage(message) {
|
|
766
|
+
if (!message || typeof message !== "object") {
|
|
767
|
+
return message;
|
|
768
|
+
}
|
|
769
|
+
const record = message;
|
|
770
|
+
const kind = normalizeAgentPromptKind(record.agent_prompt_kind);
|
|
771
|
+
const text = typeof record.text === "string" ? record.text : null;
|
|
772
|
+
if (!kind || text === null) {
|
|
773
|
+
return record;
|
|
774
|
+
}
|
|
775
|
+
return {
|
|
776
|
+
...record,
|
|
777
|
+
visible_text: text,
|
|
778
|
+
agent_prompt: buildRoomAgentPrompt(kind),
|
|
779
|
+
prompt_injected: kind === "inline",
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
function toAgentReadableMessages(messages) {
|
|
783
|
+
return (messages ?? []).map((message) => toAgentReadableMessage(message));
|
|
784
|
+
}
|
|
785
|
+
function appendIncludePromptOnly(path) {
|
|
786
|
+
return `${path}${path.includes("?") ? "&" : "?"}include_prompt_only=1`;
|
|
787
|
+
}
|
|
749
788
|
async function roomScopedApiCall(input) {
|
|
750
789
|
if (input.room_id) {
|
|
751
790
|
try {
|
|
@@ -877,18 +916,46 @@ async function createInviteRoom() {
|
|
|
877
916
|
},
|
|
878
917
|
};
|
|
879
918
|
}
|
|
880
|
-
async function
|
|
919
|
+
async function buildJoinResponse(input) {
|
|
920
|
+
const basePayload = await withAgentIdentity({
|
|
921
|
+
...toPublicRoomResponse(input.joined.response, input.joined.room.room_id),
|
|
922
|
+
joined_via: input.joined_via,
|
|
923
|
+
session_mode: input.session_mode,
|
|
924
|
+
});
|
|
925
|
+
if (input.session_mode === "current") {
|
|
926
|
+
return withJoinRoomAgentPrompt(basePayload);
|
|
927
|
+
}
|
|
928
|
+
const liveSession = await startLocalCodexSession({
|
|
929
|
+
room_id: input.joined.room.room_id,
|
|
930
|
+
room_identifier: input.room_identifier,
|
|
931
|
+
room_code: input.joined.room.code ?? null,
|
|
932
|
+
room_display_name: input.joined.room.display_name ?? null,
|
|
933
|
+
joined_via: input.joined_via,
|
|
934
|
+
cwd: process.cwd(),
|
|
935
|
+
});
|
|
936
|
+
return withJoinRoomAgentPrompt({
|
|
937
|
+
...basePayload,
|
|
938
|
+
local_codex_session: toPublicCodexLiveSession(liveSession.session),
|
|
939
|
+
local_codex_session_started: !liveSession.reused,
|
|
940
|
+
local_codex_session_reused: liveSession.reused,
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
async function joinInviteCode(code, sessionMode) {
|
|
881
944
|
const joined = await joinRoomIdentifier(code, "join_code");
|
|
882
|
-
return
|
|
883
|
-
|
|
945
|
+
return buildJoinResponse({
|
|
946
|
+
joined,
|
|
947
|
+
room_identifier: normalizeInviteCode(code),
|
|
884
948
|
joined_via: "join_code",
|
|
949
|
+
session_mode: sessionMode,
|
|
885
950
|
});
|
|
886
951
|
}
|
|
887
|
-
async function joinNamedRoom(name) {
|
|
952
|
+
async function joinNamedRoom(name, sessionMode) {
|
|
888
953
|
const joined = await joinRoomIdentifier(name, "join_room");
|
|
889
|
-
return
|
|
890
|
-
|
|
954
|
+
return buildJoinResponse({
|
|
955
|
+
joined,
|
|
956
|
+
room_identifier: name.trim(),
|
|
891
957
|
joined_via: "join_room",
|
|
958
|
+
session_mode: sessionMode,
|
|
892
959
|
});
|
|
893
960
|
}
|
|
894
961
|
// ---------------------------------------------------------------------------
|
|
@@ -919,8 +986,8 @@ server.resource("room_messages", new ResourceTemplate("letagents://rooms/{room_i
|
|
|
919
986
|
const result = await roomScopedApiCall({
|
|
920
987
|
room_id: normalizedRoomId,
|
|
921
988
|
project_id: storedSession?.project_id ?? null,
|
|
922
|
-
room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages${qs ? `?${qs}` : ""}
|
|
923
|
-
project_path: (projectId) => `/projects/${encodeURIComponent(projectId)}/messages${qs ? `?${qs}` : ""}
|
|
989
|
+
room_path: (targetRoomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(targetRoomId)}/messages${qs ? `?${qs}` : ""}`),
|
|
990
|
+
project_path: (projectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(projectId)}/messages${qs ? `?${qs}` : ""}`),
|
|
924
991
|
});
|
|
925
992
|
const msgs = result.messages ?? [];
|
|
926
993
|
allMessages.push(...msgs);
|
|
@@ -936,7 +1003,7 @@ server.resource("room_messages", new ResourceTemplate("letagents://rooms/{room_i
|
|
|
936
1003
|
{
|
|
937
1004
|
uri: uri.href,
|
|
938
1005
|
mimeType: "application/json",
|
|
939
|
-
text: JSON.stringify({ messages: allMessages }, null, 2),
|
|
1006
|
+
text: JSON.stringify({ messages: toAgentReadableMessages(allMessages) }, null, 2),
|
|
940
1007
|
},
|
|
941
1008
|
],
|
|
942
1009
|
};
|
|
@@ -948,7 +1015,7 @@ server.tool("create_room", "Create a new invite room on Let Agents Chat. Returns
|
|
|
948
1015
|
content: [
|
|
949
1016
|
{
|
|
950
1017
|
type: "text",
|
|
951
|
-
text: JSON.stringify(created.response, null, 2),
|
|
1018
|
+
text: JSON.stringify(withJoinRoomAgentPrompt(created.response), null, 2),
|
|
952
1019
|
},
|
|
953
1020
|
],
|
|
954
1021
|
};
|
|
@@ -960,7 +1027,7 @@ server.tool("create_project", "Legacy alias for create_room. Creates a new invit
|
|
|
960
1027
|
content: [
|
|
961
1028
|
{
|
|
962
1029
|
type: "text",
|
|
963
|
-
text: JSON.stringify(created.response, null, 2),
|
|
1030
|
+
text: JSON.stringify(withJoinRoomAgentPrompt(created.response), null, 2),
|
|
964
1031
|
},
|
|
965
1032
|
],
|
|
966
1033
|
};
|
|
@@ -968,12 +1035,16 @@ server.tool("create_project", "Legacy alias for create_room. Creates a new invit
|
|
|
968
1035
|
// -- join_code --------------------------------------------------------------
|
|
969
1036
|
server.tool("join_code", "Join an existing room using an invite code.", {
|
|
970
1037
|
code: z.string().describe("The invite code shared for the room (e.g. 'ABCX-7291')"),
|
|
971
|
-
|
|
1038
|
+
session_mode: z
|
|
1039
|
+
.enum(["live", "current"])
|
|
1040
|
+
.optional()
|
|
1041
|
+
.describe("Use 'current' (default) for a normal inline join. Use 'live' to start/reuse a detached local Codex room worker."),
|
|
1042
|
+
}, async ({ code, session_mode }) => {
|
|
972
1043
|
return {
|
|
973
1044
|
content: [
|
|
974
1045
|
{
|
|
975
1046
|
type: "text",
|
|
976
|
-
text: JSON.stringify(await joinInviteCode(code), null, 2),
|
|
1047
|
+
text: JSON.stringify(await joinInviteCode(code, normalizeJoinSessionMode(session_mode)), null, 2),
|
|
977
1048
|
},
|
|
978
1049
|
],
|
|
979
1050
|
};
|
|
@@ -981,12 +1052,16 @@ server.tool("join_code", "Join an existing room using an invite code.", {
|
|
|
981
1052
|
// -- join_project -----------------------------------------------------------
|
|
982
1053
|
server.tool("join_project", "Legacy alias for join_code. Join an existing room using an invite code.", {
|
|
983
1054
|
code: z.string().describe("The invite code shared for the room (e.g. 'ABCX-7291')"),
|
|
984
|
-
|
|
1055
|
+
session_mode: z
|
|
1056
|
+
.enum(["live", "current"])
|
|
1057
|
+
.optional()
|
|
1058
|
+
.describe("Use 'current' (default) for a normal inline join. Use 'live' to start/reuse a detached local Codex room worker."),
|
|
1059
|
+
}, async ({ code, session_mode }) => {
|
|
985
1060
|
return {
|
|
986
1061
|
content: [
|
|
987
1062
|
{
|
|
988
1063
|
type: "text",
|
|
989
|
-
text: JSON.stringify(await joinInviteCode(code), null, 2),
|
|
1064
|
+
text: JSON.stringify(await joinInviteCode(code, normalizeJoinSessionMode(session_mode)), null, 2),
|
|
990
1065
|
},
|
|
991
1066
|
],
|
|
992
1067
|
};
|
|
@@ -994,13 +1069,17 @@ server.tool("join_project", "Legacy alias for join_code. Join an existing room u
|
|
|
994
1069
|
// -- join_room --------------------------------------------------------------
|
|
995
1070
|
server.tool("join_room", "Join a named room on Let Agents Chat. Creates the room if it doesn't exist. Use this for repo-based room joining.", {
|
|
996
1071
|
name: z.string().describe("The room name to join (e.g. 'github.com/owner/repo')"),
|
|
997
|
-
|
|
1072
|
+
session_mode: z
|
|
1073
|
+
.enum(["live", "current"])
|
|
1074
|
+
.optional()
|
|
1075
|
+
.describe("Use 'current' (default) for a normal inline join. Use 'live' to start/reuse a detached local Codex room worker."),
|
|
1076
|
+
}, async ({ name, session_mode }) => {
|
|
998
1077
|
try {
|
|
999
1078
|
return {
|
|
1000
1079
|
content: [
|
|
1001
1080
|
{
|
|
1002
1081
|
type: "text",
|
|
1003
|
-
text: JSON.stringify(await joinNamedRoom(name), null, 2),
|
|
1082
|
+
text: JSON.stringify(await joinNamedRoom(name, normalizeJoinSessionMode(session_mode)), null, 2),
|
|
1004
1083
|
},
|
|
1005
1084
|
],
|
|
1006
1085
|
};
|
|
@@ -1019,6 +1098,136 @@ server.tool("join_room", "Join a named room on Let Agents Chat. Creates the room
|
|
|
1019
1098
|
throw error;
|
|
1020
1099
|
}
|
|
1021
1100
|
});
|
|
1101
|
+
// -- local Codex live sessions ---------------------------------------------
|
|
1102
|
+
server.tool("start_local_codex_session", "Start or reuse a detached local Codex live session for a LetAgents room. The worker will join the room, keep polling, contribute in discussion, and do repo work from the current working directory when asked.", {
|
|
1103
|
+
room: z
|
|
1104
|
+
.string()
|
|
1105
|
+
.describe("Invite code or room name to run as a detached local Codex worker."),
|
|
1106
|
+
cwd: z
|
|
1107
|
+
.string()
|
|
1108
|
+
.optional()
|
|
1109
|
+
.describe("Working directory for repo work. Defaults to the current process directory."),
|
|
1110
|
+
stop_phrase: z
|
|
1111
|
+
.string()
|
|
1112
|
+
.optional()
|
|
1113
|
+
.describe("Exact room message text that tells the worker to stop. Defaults to /stop-codex-room."),
|
|
1114
|
+
max_minutes: z
|
|
1115
|
+
.number()
|
|
1116
|
+
.optional()
|
|
1117
|
+
.describe("Optional hard stop in minutes. Defaults to 0, which means run until stopped."),
|
|
1118
|
+
}, async ({ room, cwd, stop_phrase, max_minutes }) => {
|
|
1119
|
+
const joinedVia = looksLikeInviteCode(room) ? "join_code" : "join_room";
|
|
1120
|
+
try {
|
|
1121
|
+
const joined = await joinRoomIdentifier(room, joinedVia);
|
|
1122
|
+
const liveSession = await startLocalCodexSession({
|
|
1123
|
+
room_id: joined.room.room_id,
|
|
1124
|
+
room_identifier: joinedVia === "join_code" ? normalizeInviteCode(room) : room.trim(),
|
|
1125
|
+
room_code: joined.room.code ?? null,
|
|
1126
|
+
room_display_name: joined.room.display_name ?? null,
|
|
1127
|
+
joined_via: joinedVia,
|
|
1128
|
+
cwd: cwd || process.cwd(),
|
|
1129
|
+
stop_phrase,
|
|
1130
|
+
max_minutes,
|
|
1131
|
+
});
|
|
1132
|
+
return {
|
|
1133
|
+
content: [
|
|
1134
|
+
{
|
|
1135
|
+
type: "text",
|
|
1136
|
+
text: JSON.stringify(await withAgentIdentity({
|
|
1137
|
+
success: true,
|
|
1138
|
+
room: toPublicRoomState(joined.room),
|
|
1139
|
+
local_codex_session: toPublicCodexLiveSession(liveSession.session),
|
|
1140
|
+
local_codex_session_started: !liveSession.reused,
|
|
1141
|
+
local_codex_session_reused: liveSession.reused,
|
|
1142
|
+
}), null, 2),
|
|
1143
|
+
},
|
|
1144
|
+
],
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
catch (error) {
|
|
1148
|
+
if (error instanceof RepoRoomAuthRequiredError) {
|
|
1149
|
+
return {
|
|
1150
|
+
content: [
|
|
1151
|
+
{
|
|
1152
|
+
type: "text",
|
|
1153
|
+
text: JSON.stringify(toRepoRoomAuthRequiredResult(error), null, 2),
|
|
1154
|
+
},
|
|
1155
|
+
],
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
throw error;
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
server.tool("status_local_codex_session", "Inspect the current detached local Codex live session, or a specific one by session_id.", {
|
|
1162
|
+
session_id: z
|
|
1163
|
+
.string()
|
|
1164
|
+
.optional()
|
|
1165
|
+
.describe("Optional session id. Defaults to the current local Codex live session."),
|
|
1166
|
+
}, async ({ session_id }) => {
|
|
1167
|
+
const status = await inspectLocalCodexSession(session_id, currentRoom?.room_id);
|
|
1168
|
+
if (!status) {
|
|
1169
|
+
return {
|
|
1170
|
+
content: [
|
|
1171
|
+
{
|
|
1172
|
+
type: "text",
|
|
1173
|
+
text: JSON.stringify({ success: false, error: "No local Codex live session found." }, null, 2),
|
|
1174
|
+
},
|
|
1175
|
+
],
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
return {
|
|
1179
|
+
content: [
|
|
1180
|
+
{
|
|
1181
|
+
type: "text",
|
|
1182
|
+
text: JSON.stringify({
|
|
1183
|
+
success: true,
|
|
1184
|
+
session: toPublicCodexLiveSession(status.session),
|
|
1185
|
+
server_reachable: status.server_reachable,
|
|
1186
|
+
thread_status: status.thread_status,
|
|
1187
|
+
turn_status: status.turn_status,
|
|
1188
|
+
recent_items: status.recent_items,
|
|
1189
|
+
}, null, 2),
|
|
1190
|
+
},
|
|
1191
|
+
],
|
|
1192
|
+
};
|
|
1193
|
+
});
|
|
1194
|
+
server.tool("stop_local_codex_session", "Stop the current detached local Codex live session, or a specific one by session_id.", {
|
|
1195
|
+
session_id: z
|
|
1196
|
+
.string()
|
|
1197
|
+
.optional()
|
|
1198
|
+
.describe("Optional session id. Defaults to the current local Codex live session."),
|
|
1199
|
+
shutdown_server: z
|
|
1200
|
+
.boolean()
|
|
1201
|
+
.optional()
|
|
1202
|
+
.describe("If true, also terminate the spawned codex app-server process when possible."),
|
|
1203
|
+
}, async ({ session_id, shutdown_server }) => {
|
|
1204
|
+
const stopped = await stopLocalCodexSession({
|
|
1205
|
+
session_id,
|
|
1206
|
+
room_id: currentRoom?.room_id,
|
|
1207
|
+
shutdown_server,
|
|
1208
|
+
});
|
|
1209
|
+
if (!stopped) {
|
|
1210
|
+
return {
|
|
1211
|
+
content: [
|
|
1212
|
+
{
|
|
1213
|
+
type: "text",
|
|
1214
|
+
text: JSON.stringify({ success: false, error: "No local Codex live session found." }, null, 2),
|
|
1215
|
+
},
|
|
1216
|
+
],
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
return {
|
|
1220
|
+
content: [
|
|
1221
|
+
{
|
|
1222
|
+
type: "text",
|
|
1223
|
+
text: JSON.stringify({
|
|
1224
|
+
success: true,
|
|
1225
|
+
session: toPublicCodexLiveSession(stopped),
|
|
1226
|
+
}, null, 2),
|
|
1227
|
+
},
|
|
1228
|
+
],
|
|
1229
|
+
};
|
|
1230
|
+
});
|
|
1022
1231
|
// -- get_current_room -------------------------------------------------------
|
|
1023
1232
|
server.tool("get_current_room", "Get information about the currently joined room, including how it was joined.", {
|
|
1024
1233
|
conversation_id: z
|
|
@@ -1031,9 +1240,11 @@ server.tool("get_current_room", "Get information about the currently joined room
|
|
|
1031
1240
|
{
|
|
1032
1241
|
type: "text",
|
|
1033
1242
|
text: JSON.stringify(currentRoom
|
|
1034
|
-
? {
|
|
1243
|
+
? withJoinRoomAgentPrompt({
|
|
1035
1244
|
connected: true,
|
|
1036
1245
|
...toPublicRoomState(currentRoom),
|
|
1246
|
+
current_local_codex_session: getCurrentLiveSessionPayload(currentRoom.room_id),
|
|
1247
|
+
local_codex_session_count: listStoredCodexLiveSessions().length,
|
|
1037
1248
|
agent_identity: toPublicAgentIdentity(getConversationIdentity(conversation_id)
|
|
1038
1249
|
?? currentAgentIdentity
|
|
1039
1250
|
?? getStoredAgentIdentity(currentAgentIdentityKey)),
|
|
@@ -1044,8 +1255,13 @@ server.tool("get_current_room", "Get information about the currently joined room
|
|
|
1044
1255
|
account: getStoredAuth()?.account ?? null,
|
|
1045
1256
|
}
|
|
1046
1257
|
: null,
|
|
1047
|
-
}
|
|
1048
|
-
: {
|
|
1258
|
+
})
|
|
1259
|
+
: {
|
|
1260
|
+
connected: false,
|
|
1261
|
+
message: "Not currently in any room",
|
|
1262
|
+
current_local_codex_session: getCurrentLiveSessionPayload(), // no room context
|
|
1263
|
+
local_codex_session_count: listStoredCodexLiveSessions().length,
|
|
1264
|
+
}, null, 2),
|
|
1049
1265
|
},
|
|
1050
1266
|
],
|
|
1051
1267
|
};
|
|
@@ -1314,9 +1530,23 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
|
|
|
1314
1530
|
.optional()
|
|
1315
1531
|
.describe("New assignee for the task. Defaults to the current agent when status=assigned."),
|
|
1316
1532
|
pr_url: z.string().optional().describe("PR URL to link to the task"),
|
|
1533
|
+
workflow_artifacts: z
|
|
1534
|
+
.array(z.object({
|
|
1535
|
+
provider: z.enum(["github", "gitlab", "bitbucket", "unknown"]),
|
|
1536
|
+
kind: z.enum(["issue", "branch", "pull_request", "merge_request", "review", "check_run", "merge"]),
|
|
1537
|
+
id: z.string().nullable().optional(),
|
|
1538
|
+
number: z.number().int().nullable().optional(),
|
|
1539
|
+
title: z.string().nullable().optional(),
|
|
1540
|
+
url: z.string().nullable().optional(),
|
|
1541
|
+
ref: z.string().nullable().optional(),
|
|
1542
|
+
state: z.string().nullable().optional(),
|
|
1543
|
+
}).strict())
|
|
1544
|
+
.max(32)
|
|
1545
|
+
.optional()
|
|
1546
|
+
.describe("Persisted provider-neutral task workflow artifacts to attach to the task"),
|
|
1317
1547
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
|
|
1318
1548
|
conversation_id: z.string().optional().describe("Optional conversation ID for per-conversation identity scoping."),
|
|
1319
|
-
}, async ({ task_id, status, assignee, pr_url, room_id, conversation_id }) => {
|
|
1549
|
+
}, async ({ task_id, status, assignee, pr_url, workflow_artifacts, room_id, conversation_id }) => {
|
|
1320
1550
|
const targetRoomId = getTargetRoomId(room_id);
|
|
1321
1551
|
const targetProjectId = getFallbackProjectId();
|
|
1322
1552
|
if (!targetRoomId && !targetProjectId) {
|
|
@@ -1339,6 +1569,7 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
|
|
|
1339
1569
|
status,
|
|
1340
1570
|
assignee: assignee ?? identity?.actor_label,
|
|
1341
1571
|
pr_url,
|
|
1572
|
+
workflow_artifacts,
|
|
1342
1573
|
}),
|
|
1343
1574
|
},
|
|
1344
1575
|
});
|
|
@@ -1549,11 +1780,15 @@ server.tool("send_message", "Send a message to a Let Agents Chat room.", {
|
|
|
1549
1780
|
.optional()
|
|
1550
1781
|
.describe("Deprecated override. Agent identity is resolved automatically on room entry."),
|
|
1551
1782
|
text: z.string().describe("The message text to send"),
|
|
1783
|
+
reply_to: z
|
|
1784
|
+
.string()
|
|
1785
|
+
.optional()
|
|
1786
|
+
.describe("Optional message id to quote-reply to (for example `msg_42`)."),
|
|
1552
1787
|
conversation_id: z
|
|
1553
1788
|
.string()
|
|
1554
1789
|
.optional()
|
|
1555
1790
|
.describe("Optional conversation ID for per-conversation identity scoping."),
|
|
1556
|
-
}, async ({ room_id, sender: _sender, text, conversation_id }) => {
|
|
1791
|
+
}, async ({ room_id, sender: _sender, text, reply_to, conversation_id }) => {
|
|
1557
1792
|
const targetRoomId = getTargetRoomId(room_id);
|
|
1558
1793
|
const targetProjectId = getFallbackProjectId();
|
|
1559
1794
|
if (!targetRoomId && !targetProjectId) {
|
|
@@ -1567,7 +1802,7 @@ server.tool("send_message", "Send a message to a Let Agents Chat room.", {
|
|
|
1567
1802
|
project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages`,
|
|
1568
1803
|
options: {
|
|
1569
1804
|
method: "POST",
|
|
1570
|
-
body: JSON.stringify({ sender: identity.actor_label, text }),
|
|
1805
|
+
body: JSON.stringify({ sender: identity.actor_label, text, reply_to }),
|
|
1571
1806
|
},
|
|
1572
1807
|
});
|
|
1573
1808
|
touchCurrentRoom(typeof message.id === "string" ? message.id : undefined);
|
|
@@ -1601,8 +1836,8 @@ server.tool("read_messages", "Read all messages from a Let Agents Chat room.", {
|
|
|
1601
1836
|
const result = await roomScopedApiCall({
|
|
1602
1837
|
room_id: targetRoomId,
|
|
1603
1838
|
project_id: targetProjectId,
|
|
1604
|
-
room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages${qs ? `?${qs}` : ""}
|
|
1605
|
-
project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages${qs ? `?${qs}` : ""}
|
|
1839
|
+
room_path: (targetRoomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(targetRoomId)}/messages${qs ? `?${qs}` : ""}`),
|
|
1840
|
+
project_path: (targetProjectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(targetProjectId)}/messages${qs ? `?${qs}` : ""}`),
|
|
1606
1841
|
});
|
|
1607
1842
|
roomIdFromResponse = roomIdFromResponse || result.room_id || result.project_id;
|
|
1608
1843
|
const msgs = result.messages ?? [];
|
|
@@ -1615,7 +1850,7 @@ server.tool("read_messages", "Read all messages from a Let Agents Chat room.", {
|
|
|
1615
1850
|
break;
|
|
1616
1851
|
afterCursor = lastMsg.id;
|
|
1617
1852
|
}
|
|
1618
|
-
const output = { messages: allMessages };
|
|
1853
|
+
const output = { messages: toAgentReadableMessages(allMessages) };
|
|
1619
1854
|
if (roomIdFromResponse) {
|
|
1620
1855
|
output[targetRoomId ? "room_id" : "project_id"] = roomIdFromResponse;
|
|
1621
1856
|
}
|
|
@@ -1654,8 +1889,8 @@ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat roo
|
|
|
1654
1889
|
const firstResult = await roomScopedApiCall({
|
|
1655
1890
|
room_id: targetRoomId,
|
|
1656
1891
|
project_id: targetProjectId,
|
|
1657
|
-
room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages/poll?${queryString}
|
|
1658
|
-
project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages/poll?${queryString}
|
|
1892
|
+
room_path: (targetRoomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(targetRoomId)}/messages/poll?${queryString}`),
|
|
1893
|
+
project_path: (targetProjectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(targetProjectId)}/messages/poll?${queryString}`),
|
|
1659
1894
|
options: { signal: AbortSignal.timeout(clientTimeout) },
|
|
1660
1895
|
});
|
|
1661
1896
|
const allMessages = [...(firstResult.messages ?? [])];
|
|
@@ -1670,8 +1905,8 @@ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat roo
|
|
|
1670
1905
|
const page = await roomScopedApiCall({
|
|
1671
1906
|
room_id: targetRoomId,
|
|
1672
1907
|
project_id: targetProjectId,
|
|
1673
|
-
room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages?${qs}
|
|
1674
|
-
project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages?${qs}
|
|
1908
|
+
room_path: (targetRoomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(targetRoomId)}/messages?${qs}`),
|
|
1909
|
+
project_path: (targetProjectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(targetProjectId)}/messages?${qs}`),
|
|
1675
1910
|
});
|
|
1676
1911
|
const msgs = page.messages ?? [];
|
|
1677
1912
|
allMessages.push(...msgs);
|
|
@@ -1682,7 +1917,7 @@ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat roo
|
|
|
1682
1917
|
break;
|
|
1683
1918
|
}
|
|
1684
1919
|
}
|
|
1685
|
-
const output = { messages: allMessages };
|
|
1920
|
+
const output = { messages: toAgentReadableMessages(allMessages) };
|
|
1686
1921
|
if (roomIdFromResponse) {
|
|
1687
1922
|
output[targetRoomId ? "room_id" : "project_id"] = roomIdFromResponse;
|
|
1688
1923
|
}
|
|
@@ -2068,14 +2303,14 @@ server.tool("resume_room_session", "Rejoin the last locally saved room context,
|
|
|
2068
2303
|
content: [
|
|
2069
2304
|
{
|
|
2070
2305
|
type: "text",
|
|
2071
|
-
text: JSON.stringify({
|
|
2306
|
+
text: JSON.stringify(withJoinRoomAgentPrompt({
|
|
2072
2307
|
success: true,
|
|
2073
2308
|
rejoined_from_local_state: true,
|
|
2074
2309
|
server_session_resumed: false,
|
|
2075
2310
|
last_message_id_before_restart: savedRoom.last_message_id ?? null,
|
|
2076
2311
|
room: toPublicRoomState(joined.room),
|
|
2077
2312
|
agent_identity: toPublicAgentIdentity(agentIdentity),
|
|
2078
|
-
}, null, 2),
|
|
2313
|
+
}), null, 2),
|
|
2079
2314
|
},
|
|
2080
2315
|
],
|
|
2081
2316
|
};
|
package/dist/mcp/sse-client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { encodeRoomIdPath } from "./room-id.js";
|
|
2
|
+
import { buildRoomAgentPrompt, normalizeAgentPromptKind } from "../shared/room-agent-prompts.js";
|
|
2
3
|
export class SseClient {
|
|
3
4
|
apiUrl;
|
|
4
5
|
getAccessToken;
|
|
@@ -53,7 +54,7 @@ export class SseClient {
|
|
|
53
54
|
}
|
|
54
55
|
async consumeStream(target, signal, onMessage) {
|
|
55
56
|
try {
|
|
56
|
-
await this.openStream(`${this.apiUrl}/rooms/${encodeRoomIdPath(target.roomId)}/messages/stream
|
|
57
|
+
await this.openStream(this.withIncludePromptOnly(`${this.apiUrl}/rooms/${encodeRoomIdPath(target.roomId)}/messages/stream`), signal, onMessage);
|
|
57
58
|
return;
|
|
58
59
|
}
|
|
59
60
|
catch (error) {
|
|
@@ -61,7 +62,10 @@ export class SseClient {
|
|
|
61
62
|
throw error;
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
|
-
await this.openStream(`${this.apiUrl}/projects/${encodeURIComponent(target.projectId)}/messages/stream
|
|
65
|
+
await this.openStream(this.withIncludePromptOnly(`${this.apiUrl}/projects/${encodeURIComponent(target.projectId)}/messages/stream`), signal, onMessage);
|
|
66
|
+
}
|
|
67
|
+
withIncludePromptOnly(url) {
|
|
68
|
+
return `${url}${url.includes("?") ? "&" : "?"}include_prompt_only=1`;
|
|
65
69
|
}
|
|
66
70
|
async openStream(url, signal, onMessage) {
|
|
67
71
|
const response = await fetch(url, {
|
|
@@ -105,7 +109,19 @@ export class SseClient {
|
|
|
105
109
|
if (dataLines.length === 0) {
|
|
106
110
|
return;
|
|
107
111
|
}
|
|
108
|
-
onMessage(JSON.parse(dataLines.join("\n")));
|
|
112
|
+
onMessage(this.enrichPromptMetadata(JSON.parse(dataLines.join("\n"))));
|
|
113
|
+
}
|
|
114
|
+
enrichPromptMetadata(message) {
|
|
115
|
+
const kind = normalizeAgentPromptKind(message.agent_prompt_kind);
|
|
116
|
+
if (!kind) {
|
|
117
|
+
return message;
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
...message,
|
|
121
|
+
visible_text: message.text,
|
|
122
|
+
agent_prompt: buildRoomAgentPrompt(kind),
|
|
123
|
+
prompt_injected: kind === "inline",
|
|
124
|
+
};
|
|
109
125
|
}
|
|
110
126
|
isMissingRouteError(error) {
|
|
111
127
|
return (error instanceof Error &&
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// TODO: make room agent prompts configurable per room/project instead of hardcoded
|
|
2
|
+
// constants. The current behavior is intentionally global across rooms.
|
|
3
|
+
const BASE_ROOM_AGENT_PROMPT = "Stay in this room continuously. Keep long-polling for new messages. When messages arrive, read them immediately, act on anything actionable, help with review work when asked, and keep monitoring until the human explicitly redirects you. Do not send a visible 'keep polling' acknowledgement unless the human explicitly asks for that phrase.";
|
|
4
|
+
export function buildRoomAgentPrompt(kind) {
|
|
5
|
+
if (kind === "join") {
|
|
6
|
+
return `You just joined this room. ${BASE_ROOM_AGENT_PROMPT}`;
|
|
7
|
+
}
|
|
8
|
+
if (kind === "auto") {
|
|
9
|
+
return `Background reminder. ${BASE_ROOM_AGENT_PROMPT}`;
|
|
10
|
+
}
|
|
11
|
+
return `Treat the visible user message above as the active instruction. After you handle it, ${BASE_ROOM_AGENT_PROMPT}`;
|
|
12
|
+
}
|
|
13
|
+
export function normalizeAgentPromptKind(value) {
|
|
14
|
+
if (typeof value !== "string") {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const normalized = value.trim().toLowerCase();
|
|
18
|
+
if (normalized === "join" || normalized === "inline" || normalized === "auto") {
|
|
19
|
+
return normalized;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
export function isPromptOnlyAgentMessage(text, kind) {
|
|
24
|
+
return normalizeAgentPromptKind(kind) === "auto" && !String(text || "").trim();
|
|
25
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letagents",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
4
4
|
"description": "Let Agents Chat — MCP server for AI agent communication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/mcp/server.js",
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsc",
|
|
17
|
+
"build:web": "cd src/web && npm install && npx vite build",
|
|
18
|
+
"build:all": "npm run build && npm run build:web",
|
|
17
19
|
"db:generate": "drizzle-kit generate",
|
|
18
20
|
"db:migrate": "tsx src/api/migrate.ts",
|
|
19
21
|
"db:studio": "drizzle-kit studio",
|
|
@@ -30,9 +32,9 @@
|
|
|
30
32
|
],
|
|
31
33
|
"repository": {
|
|
32
34
|
"type": "git",
|
|
33
|
-
"url": "https://github.com/
|
|
35
|
+
"url": "https://github.com/BrosInCode/letagents.git"
|
|
34
36
|
},
|
|
35
|
-
"license": "
|
|
37
|
+
"license": "BUSL-1.1",
|
|
36
38
|
"dependencies": {
|
|
37
39
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
38
40
|
"drizzle-orm": "^0.45.1",
|