@tiflis-io/tiflis-code-workstation 0.3.29 → 0.3.31
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/dist/main.js +3252 -759
- package/package.json +3 -1
package/dist/main.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from "./chunk-JSN52PLR.js";
|
|
6
6
|
|
|
7
7
|
// src/main.ts
|
|
8
|
-
import { randomUUID as
|
|
8
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
9
9
|
|
|
10
10
|
// src/app.ts
|
|
11
11
|
import Fastify from "fastify";
|
|
@@ -274,6 +274,8 @@ var SESSION_CONFIG = {
|
|
|
274
274
|
MAX_AGENT_SESSIONS: 10,
|
|
275
275
|
/** Maximum number of concurrent terminal sessions */
|
|
276
276
|
MAX_TERMINAL_SESSIONS: 5,
|
|
277
|
+
/** Maximum number of concurrent backlog sessions */
|
|
278
|
+
MAX_BACKLOG_SESSIONS: 10,
|
|
277
279
|
/** Default terminal columns */
|
|
278
280
|
DEFAULT_TERMINAL_COLS: 80,
|
|
279
281
|
/** Default terminal rows */
|
|
@@ -1116,6 +1118,67 @@ var SubscriptionRepository = class {
|
|
|
1116
1118
|
}
|
|
1117
1119
|
};
|
|
1118
1120
|
|
|
1121
|
+
// src/infrastructure/persistence/repositories/session-repository.ts
|
|
1122
|
+
import { eq as eq3 } from "drizzle-orm";
|
|
1123
|
+
var SessionRepository = class {
|
|
1124
|
+
/**
|
|
1125
|
+
* Creates a new session record or updates if exists.
|
|
1126
|
+
*/
|
|
1127
|
+
create(params) {
|
|
1128
|
+
const db2 = getDatabase();
|
|
1129
|
+
const newSession = {
|
|
1130
|
+
id: params.id,
|
|
1131
|
+
type: params.type,
|
|
1132
|
+
workspace: params.workspace,
|
|
1133
|
+
project: params.project,
|
|
1134
|
+
worktree: params.worktree,
|
|
1135
|
+
workingDir: params.workingDir,
|
|
1136
|
+
status: "active",
|
|
1137
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
1138
|
+
};
|
|
1139
|
+
db2.insert(sessions).values(newSession).onConflictDoUpdate({
|
|
1140
|
+
target: sessions.id,
|
|
1141
|
+
set: {
|
|
1142
|
+
status: "active",
|
|
1143
|
+
terminatedAt: null
|
|
1144
|
+
}
|
|
1145
|
+
}).run();
|
|
1146
|
+
return { ...newSession, terminatedAt: null, createdAt: newSession.createdAt };
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Gets a session by ID.
|
|
1150
|
+
*/
|
|
1151
|
+
getById(sessionId) {
|
|
1152
|
+
const db2 = getDatabase();
|
|
1153
|
+
return db2.select().from(sessions).where(eq3(sessions.id, sessionId)).get();
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Gets all active sessions.
|
|
1157
|
+
*/
|
|
1158
|
+
getActive() {
|
|
1159
|
+
const db2 = getDatabase();
|
|
1160
|
+
return db2.select().from(sessions).where(eq3(sessions.status, "active")).all();
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Marks a session as terminated.
|
|
1164
|
+
*/
|
|
1165
|
+
terminate(sessionId) {
|
|
1166
|
+
const db2 = getDatabase();
|
|
1167
|
+
db2.update(sessions).set({
|
|
1168
|
+
status: "terminated",
|
|
1169
|
+
terminatedAt: /* @__PURE__ */ new Date()
|
|
1170
|
+
}).where(eq3(sessions.id, sessionId)).run();
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Deletes old terminated sessions (for cleanup).
|
|
1174
|
+
*/
|
|
1175
|
+
deleteOldTerminated(_olderThan) {
|
|
1176
|
+
const db2 = getDatabase();
|
|
1177
|
+
const result = db2.delete(sessions).where(eq3(sessions.status, "terminated")).run();
|
|
1178
|
+
return result.changes;
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1119
1182
|
// src/infrastructure/websocket/tunnel-client.ts
|
|
1120
1183
|
import WebSocket from "ws";
|
|
1121
1184
|
|
|
@@ -1171,7 +1234,7 @@ var ListSessionsSchema = z2.object({
|
|
|
1171
1234
|
// Injected by tunnel for tunnel connections
|
|
1172
1235
|
});
|
|
1173
1236
|
var CreateSessionPayloadSchema = z2.object({
|
|
1174
|
-
session_type: z2.enum(["cursor", "claude", "opencode", "terminal"]),
|
|
1237
|
+
session_type: z2.enum(["cursor", "claude", "opencode", "terminal", "backlog-agent"]),
|
|
1175
1238
|
agent_name: z2.string().optional(),
|
|
1176
1239
|
// Custom alias name (e.g., 'zai' for claude with custom config)
|
|
1177
1240
|
workspace: z2.string().min(1, "Workspace is required"),
|
|
@@ -1398,7 +1461,7 @@ function getMessageType(data) {
|
|
|
1398
1461
|
}
|
|
1399
1462
|
|
|
1400
1463
|
// src/infrastructure/websocket/tunnel-client.ts
|
|
1401
|
-
var TunnelClient = class {
|
|
1464
|
+
var TunnelClient = class _TunnelClient {
|
|
1402
1465
|
config;
|
|
1403
1466
|
callbacks;
|
|
1404
1467
|
logger;
|
|
@@ -1412,6 +1475,8 @@ var TunnelClient = class {
|
|
|
1412
1475
|
reconnectTimeout = null;
|
|
1413
1476
|
registrationTimeout = null;
|
|
1414
1477
|
messageBuffer = [];
|
|
1478
|
+
static MAX_SEND_RETRIES = 3;
|
|
1479
|
+
static SEND_RETRY_DELAY_MS = 100;
|
|
1415
1480
|
constructor(config2, callbacks) {
|
|
1416
1481
|
this.config = config2;
|
|
1417
1482
|
this.callbacks = callbacks;
|
|
@@ -1514,8 +1579,9 @@ var TunnelClient = class {
|
|
|
1514
1579
|
this.messageBuffer = [];
|
|
1515
1580
|
}
|
|
1516
1581
|
/**
|
|
1517
|
-
* Sends a message to the tunnel (for forwarding to clients).
|
|
1518
|
-
*
|
|
1582
|
+
* Sends a message to the tunnel (for forwarding to clients) with retry logic (FIX #3).
|
|
1583
|
+
* Buffers during reconnection, retries on transient failures with exponential backoff.
|
|
1584
|
+
* Returns true if send was successful or queued for retry.
|
|
1519
1585
|
*/
|
|
1520
1586
|
send(message) {
|
|
1521
1587
|
if (this.state !== "registered" || !this.ws) {
|
|
@@ -1528,18 +1594,55 @@ var TunnelClient = class {
|
|
|
1528
1594
|
if (this.ws.readyState !== WebSocket.OPEN) {
|
|
1529
1595
|
this.logger.warn(
|
|
1530
1596
|
{ readyState: this.ws.readyState },
|
|
1531
|
-
"Socket not open, triggering reconnection"
|
|
1597
|
+
"Socket not open, buffering message and triggering reconnection"
|
|
1532
1598
|
);
|
|
1599
|
+
this.messageBuffer.push(message);
|
|
1533
1600
|
this.handleSendFailure();
|
|
1601
|
+
return true;
|
|
1602
|
+
}
|
|
1603
|
+
return this.sendWithRetry(message, 0);
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* FIX #3: Send with exponential backoff retry.
|
|
1607
|
+
* On failure, schedules retry with exponential delay (100ms, 200ms, 400ms).
|
|
1608
|
+
*/
|
|
1609
|
+
sendWithRetry(message, attempt) {
|
|
1610
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
1611
|
+
if (attempt === 0) {
|
|
1612
|
+
this.messageBuffer.push(message);
|
|
1613
|
+
}
|
|
1534
1614
|
return false;
|
|
1535
1615
|
}
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1616
|
+
try {
|
|
1617
|
+
this.ws.send(message, (error) => {
|
|
1618
|
+
if (error) {
|
|
1619
|
+
if (attempt < _TunnelClient.MAX_SEND_RETRIES) {
|
|
1620
|
+
const delay = _TunnelClient.SEND_RETRY_DELAY_MS * Math.pow(2, attempt);
|
|
1621
|
+
this.logger.debug(
|
|
1622
|
+
{ attempt: attempt + 1, delay, errorType: error.name },
|
|
1623
|
+
"Send failed, scheduling retry with exponential backoff"
|
|
1624
|
+
);
|
|
1625
|
+
setTimeout(() => {
|
|
1626
|
+
this.sendWithRetry(message, attempt + 1);
|
|
1627
|
+
}, delay);
|
|
1628
|
+
} else {
|
|
1629
|
+
this.logger.error(
|
|
1630
|
+
{ error: error.message, attempts: attempt + 1 },
|
|
1631
|
+
"Send failed after all retries, buffering message"
|
|
1632
|
+
);
|
|
1633
|
+
this.messageBuffer.push(message);
|
|
1634
|
+
this.handleSendFailure();
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
});
|
|
1638
|
+
return true;
|
|
1639
|
+
} catch (error) {
|
|
1640
|
+
this.logger.error({ error: error.message }, "Unexpected error in send");
|
|
1641
|
+
if (attempt === 0) {
|
|
1642
|
+
this.messageBuffer.push(message);
|
|
1540
1643
|
}
|
|
1541
|
-
|
|
1542
|
-
|
|
1644
|
+
return false;
|
|
1645
|
+
}
|
|
1543
1646
|
}
|
|
1544
1647
|
/**
|
|
1545
1648
|
* Handles send failure by disconnecting and scheduling reconnection.
|
|
@@ -2457,6 +2560,8 @@ var FileSystemWorkspaceDiscovery = class {
|
|
|
2457
2560
|
// src/infrastructure/terminal/pty-manager.ts
|
|
2458
2561
|
import * as pty from "node-pty";
|
|
2459
2562
|
import { nanoid } from "nanoid";
|
|
2563
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2564
|
+
import { execSync as execSync3 } from "child_process";
|
|
2460
2565
|
|
|
2461
2566
|
// src/domain/entities/session.ts
|
|
2462
2567
|
var Session = class {
|
|
@@ -2779,15 +2884,26 @@ function isTerminalSession(session) {
|
|
|
2779
2884
|
|
|
2780
2885
|
// src/infrastructure/shell/shell-env.ts
|
|
2781
2886
|
import { execSync as execSync2 } from "child_process";
|
|
2887
|
+
import { existsSync as existsSync2 } from "fs";
|
|
2782
2888
|
var cachedShellEnv = null;
|
|
2783
|
-
function
|
|
2784
|
-
|
|
2889
|
+
function resolveShell() {
|
|
2890
|
+
const primaryShell = process.env.SHELL;
|
|
2891
|
+
if (primaryShell && existsSync2(primaryShell)) {
|
|
2892
|
+
return primaryShell;
|
|
2893
|
+
}
|
|
2894
|
+
const fallbackShells = ["/bin/zsh", "/bin/bash", "/bin/sh"];
|
|
2895
|
+
for (const shell of fallbackShells) {
|
|
2896
|
+
if (existsSync2(shell)) {
|
|
2897
|
+
return shell;
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
return "/bin/bash";
|
|
2785
2901
|
}
|
|
2786
2902
|
function getShellEnv() {
|
|
2787
2903
|
if (cachedShellEnv) {
|
|
2788
2904
|
return cachedShellEnv;
|
|
2789
2905
|
}
|
|
2790
|
-
const shell =
|
|
2906
|
+
const shell = resolveShell();
|
|
2791
2907
|
const isZsh = shell.includes("zsh");
|
|
2792
2908
|
const isBash = shell.includes("bash");
|
|
2793
2909
|
let envOutput;
|
|
@@ -2838,17 +2954,49 @@ function getShellEnv() {
|
|
|
2838
2954
|
return cachedShellEnv;
|
|
2839
2955
|
} catch (error) {
|
|
2840
2956
|
console.warn(
|
|
2841
|
-
"Failed to retrieve shell environment
|
|
2842
|
-
|
|
2957
|
+
"Failed to retrieve shell environment from shell",
|
|
2958
|
+
{
|
|
2959
|
+
shell,
|
|
2960
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2961
|
+
}
|
|
2843
2962
|
);
|
|
2963
|
+
console.warn("Using process.env as fallback");
|
|
2844
2964
|
cachedShellEnv = { ...process.env };
|
|
2845
2965
|
return cachedShellEnv;
|
|
2846
2966
|
}
|
|
2847
2967
|
}
|
|
2848
2968
|
|
|
2849
2969
|
// src/infrastructure/terminal/pty-manager.ts
|
|
2850
|
-
function
|
|
2851
|
-
|
|
2970
|
+
function resolveShell2(logger) {
|
|
2971
|
+
const primaryShell = process.env.SHELL;
|
|
2972
|
+
if (primaryShell && existsSync3(primaryShell)) {
|
|
2973
|
+
logger.debug({ shell: primaryShell }, "Using primary shell from SHELL environment");
|
|
2974
|
+
return primaryShell;
|
|
2975
|
+
}
|
|
2976
|
+
if (primaryShell) {
|
|
2977
|
+
logger.warn(
|
|
2978
|
+
{ primaryShell, exists: false },
|
|
2979
|
+
"Primary shell from SHELL env does not exist, trying alternatives"
|
|
2980
|
+
);
|
|
2981
|
+
}
|
|
2982
|
+
const fallbackShells = ["/bin/zsh", "/bin/bash", "/bin/sh"];
|
|
2983
|
+
for (const shell of fallbackShells) {
|
|
2984
|
+
if (existsSync3(shell)) {
|
|
2985
|
+
logger.debug({ shell }, "Using fallback shell");
|
|
2986
|
+
return shell;
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
try {
|
|
2990
|
+
const whichSh = execSync3("which sh", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
2991
|
+
if (whichSh && existsSync3(whichSh)) {
|
|
2992
|
+
logger.debug({ shell: whichSh }, "Found sh in PATH as last resort");
|
|
2993
|
+
return whichSh;
|
|
2994
|
+
}
|
|
2995
|
+
} catch {
|
|
2996
|
+
logger.warn("Failed to find sh in PATH");
|
|
2997
|
+
}
|
|
2998
|
+
logger.error("No usable shell found, defaulting to /bin/bash");
|
|
2999
|
+
return "/bin/bash";
|
|
2852
3000
|
}
|
|
2853
3001
|
var PtyManager = class {
|
|
2854
3002
|
logger;
|
|
@@ -2860,40 +3008,59 @@ var PtyManager = class {
|
|
|
2860
3008
|
}
|
|
2861
3009
|
/**
|
|
2862
3010
|
* Creates a new terminal session.
|
|
3011
|
+
* Resolves shell with fallbacks and includes detailed error logging for debugging M1 issues.
|
|
2863
3012
|
*/
|
|
2864
3013
|
create(workingDir, cols, rows) {
|
|
2865
3014
|
const sessionId = new SessionId(nanoid(12));
|
|
2866
|
-
const shell =
|
|
3015
|
+
const shell = resolveShell2(this.logger);
|
|
2867
3016
|
this.logger.debug(
|
|
2868
3017
|
{ sessionId: sessionId.value, workingDir, shell, cols, rows },
|
|
2869
3018
|
"Creating terminal session"
|
|
2870
3019
|
);
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
3020
|
+
try {
|
|
3021
|
+
const shellEnv = getShellEnv();
|
|
3022
|
+
const ptyProcess = pty.spawn(shell, [], {
|
|
3023
|
+
name: "xterm-256color",
|
|
3024
|
+
cols,
|
|
3025
|
+
rows,
|
|
3026
|
+
cwd: workingDir,
|
|
3027
|
+
env: {
|
|
3028
|
+
...shellEnv,
|
|
3029
|
+
TERM: "xterm-256color",
|
|
3030
|
+
// Disable zsh partial line marker (inverse % sign on startup)
|
|
3031
|
+
PROMPT_EOL_MARK: ""
|
|
3032
|
+
}
|
|
3033
|
+
});
|
|
3034
|
+
const session = new TerminalSession({
|
|
3035
|
+
id: sessionId,
|
|
3036
|
+
pty: ptyProcess,
|
|
3037
|
+
cols,
|
|
3038
|
+
rows,
|
|
3039
|
+
workingDir,
|
|
3040
|
+
bufferSize: this.bufferSize
|
|
3041
|
+
});
|
|
3042
|
+
this.logger.info(
|
|
3043
|
+
{ sessionId: sessionId.value, pid: ptyProcess.pid, shell },
|
|
3044
|
+
"Terminal session created"
|
|
3045
|
+
);
|
|
3046
|
+
return Promise.resolve(session);
|
|
3047
|
+
} catch (error) {
|
|
3048
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
3049
|
+
this.logger.error(
|
|
3050
|
+
{
|
|
3051
|
+
sessionId: sessionId.value,
|
|
3052
|
+
shell,
|
|
3053
|
+
workingDir,
|
|
3054
|
+
cols,
|
|
3055
|
+
rows,
|
|
3056
|
+
error: errorMsg,
|
|
3057
|
+
stack: error instanceof Error ? error.stack : void 0
|
|
3058
|
+
},
|
|
3059
|
+
"Failed to create terminal session"
|
|
3060
|
+
);
|
|
3061
|
+
const message = `Terminal creation failed: ${errorMsg}. Shell: ${shell}, Working dir: ${workingDir}`;
|
|
3062
|
+
throw new Error(message);
|
|
3063
|
+
}
|
|
2897
3064
|
}
|
|
2898
3065
|
/**
|
|
2899
3066
|
* Writes data to a terminal session.
|
|
@@ -3055,39 +3222,32 @@ var HeadlessAgentExecutor = class extends EventEmitter {
|
|
|
3055
3222
|
cwd: this.workingDir,
|
|
3056
3223
|
env: {
|
|
3057
3224
|
...shellEnv,
|
|
3058
|
-
// Apply alias env vars (e.g., CLAUDE_CONFIG_DIR)
|
|
3059
3225
|
...aliasEnvVars,
|
|
3060
|
-
// Ensure proper terminal environment
|
|
3061
3226
|
TERM: "xterm-256color",
|
|
3062
|
-
// Disable interactive prompts
|
|
3063
3227
|
CI: "true"
|
|
3064
3228
|
},
|
|
3065
3229
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3066
|
-
|
|
3067
|
-
detached: true,
|
|
3068
|
-
// Create new process group for clean termination
|
|
3069
|
-
// Ensure child doesn't interfere with parent's signal handling
|
|
3070
|
-
// @ts-expect-error - Node.js 16+ option
|
|
3071
|
-
ignoreParentSignals: true
|
|
3230
|
+
detached: true
|
|
3072
3231
|
});
|
|
3073
|
-
this.subprocess
|
|
3232
|
+
const proc = this.subprocess;
|
|
3233
|
+
proc.stdout?.on("data", (data) => {
|
|
3074
3234
|
if (this.isKilled) return;
|
|
3075
3235
|
const text2 = data.toString();
|
|
3076
3236
|
this.emit("stdout", text2);
|
|
3077
3237
|
});
|
|
3078
|
-
|
|
3238
|
+
proc.stderr?.on("data", (data) => {
|
|
3079
3239
|
if (this.isKilled) return;
|
|
3080
3240
|
const text2 = data.toString();
|
|
3081
3241
|
this.emit("stderr", text2);
|
|
3082
3242
|
});
|
|
3083
|
-
|
|
3243
|
+
proc.on("exit", (code) => {
|
|
3084
3244
|
this.clearExecutionTimeout();
|
|
3085
3245
|
if (!this.isKilled) {
|
|
3086
3246
|
this.emit("exit", code);
|
|
3087
3247
|
}
|
|
3088
3248
|
this.subprocess = null;
|
|
3089
3249
|
});
|
|
3090
|
-
|
|
3250
|
+
proc.on("error", (error) => {
|
|
3091
3251
|
this.clearExecutionTimeout();
|
|
3092
3252
|
if (!this.isKilled) {
|
|
3093
3253
|
this.emit("error", error);
|
|
@@ -4638,11 +4798,15 @@ var CreateSessionUseCase = class {
|
|
|
4638
4798
|
* Creates a new session.
|
|
4639
4799
|
*/
|
|
4640
4800
|
async execute(params) {
|
|
4641
|
-
const { requestId, sessionType, agentName, workspace, project, worktree } = params;
|
|
4642
|
-
|
|
4801
|
+
const { requestId, sessionType, agentName, workspace, project, worktree: rawWorktree, backlogAgent, backlogId } = params;
|
|
4802
|
+
let worktree = rawWorktree;
|
|
4803
|
+
if (worktree && (worktree.toLowerCase() === "main" || worktree.toLowerCase() === "master")) {
|
|
4804
|
+
worktree = void 0;
|
|
4805
|
+
}
|
|
4806
|
+
this.logger.info({ requestId, sessionType, agentName, workspace, project, worktree, backlogAgent, backlogId }, "CreateSession execute called");
|
|
4643
4807
|
let workingDir;
|
|
4644
4808
|
let workspacePath = null;
|
|
4645
|
-
if (sessionType === "terminal") {
|
|
4809
|
+
if (sessionType === "terminal" || sessionType === "backlog-agent") {
|
|
4646
4810
|
const hasRealWorkspace = workspace && workspace !== "home";
|
|
4647
4811
|
const hasRealProject = project && project !== "default";
|
|
4648
4812
|
this.logger.info({ hasRealWorkspace, hasRealProject, workspace, project }, "Terminal session path logic");
|
|
@@ -4754,6 +4918,11 @@ var CreateSessionUseCase = class {
|
|
|
4754
4918
|
if (count2 >= SESSION_CONFIG.MAX_TERMINAL_SESSIONS) {
|
|
4755
4919
|
throw new SessionLimitReachedError("terminal", SESSION_CONFIG.MAX_TERMINAL_SESSIONS);
|
|
4756
4920
|
}
|
|
4921
|
+
} else if (sessionType === "backlog-agent") {
|
|
4922
|
+
const count2 = this.deps.sessionManager.countByType("backlog-agent");
|
|
4923
|
+
if (count2 >= SESSION_CONFIG.MAX_BACKLOG_SESSIONS) {
|
|
4924
|
+
throw new SessionLimitReachedError("backlog", SESSION_CONFIG.MAX_BACKLOG_SESSIONS);
|
|
4925
|
+
}
|
|
4757
4926
|
} else if (sessionType !== "supervisor") {
|
|
4758
4927
|
const agentCount = this.deps.sessionManager.countByType("cursor") + this.deps.sessionManager.countByType("claude") + this.deps.sessionManager.countByType("opencode");
|
|
4759
4928
|
if (agentCount >= SESSION_CONFIG.MAX_AGENT_SESSIONS) {
|
|
@@ -5067,13 +5236,58 @@ var SubscriptionService = class {
|
|
|
5067
5236
|
};
|
|
5068
5237
|
|
|
5069
5238
|
// src/application/services/message-broadcaster-impl.ts
|
|
5070
|
-
var MessageBroadcasterImpl = class {
|
|
5239
|
+
var MessageBroadcasterImpl = class _MessageBroadcasterImpl {
|
|
5071
5240
|
deps;
|
|
5072
5241
|
logger;
|
|
5242
|
+
/** FIX #4: Buffer messages for devices during auth flow */
|
|
5243
|
+
authBuffers = /* @__PURE__ */ new Map();
|
|
5244
|
+
static AUTH_BUFFER_TTL_MS = 5e3;
|
|
5245
|
+
// 5 second buffer TTL
|
|
5073
5246
|
constructor(deps) {
|
|
5074
5247
|
this.deps = deps;
|
|
5075
5248
|
this.logger = deps.logger.child({ service: "broadcaster" });
|
|
5076
5249
|
}
|
|
5250
|
+
/**
|
|
5251
|
+
* FIX #4: Buffers a message for a device during auth flow.
|
|
5252
|
+
* Called when subscribing clients are not yet authenticated.
|
|
5253
|
+
* Messages are buffered with TTL and flushed when auth completes.
|
|
5254
|
+
*/
|
|
5255
|
+
bufferMessageForAuth(deviceId, sessionId, message) {
|
|
5256
|
+
if (!this.authBuffers.has(deviceId)) {
|
|
5257
|
+
this.authBuffers.set(deviceId, []);
|
|
5258
|
+
}
|
|
5259
|
+
const buffer = this.authBuffers.get(deviceId) ?? [];
|
|
5260
|
+
buffer.push({ sessionId, message, timestamp: Date.now() });
|
|
5261
|
+
const now = Date.now();
|
|
5262
|
+
const filtered = buffer.filter(
|
|
5263
|
+
(item) => now - item.timestamp < _MessageBroadcasterImpl.AUTH_BUFFER_TTL_MS
|
|
5264
|
+
);
|
|
5265
|
+
this.authBuffers.set(deviceId, filtered);
|
|
5266
|
+
this.logger.debug(
|
|
5267
|
+
{ deviceId, bufferSize: filtered.length },
|
|
5268
|
+
"Buffered message for authenticating device"
|
|
5269
|
+
);
|
|
5270
|
+
}
|
|
5271
|
+
/**
|
|
5272
|
+
* FIX #4: Flushes buffered messages for a device after authentication.
|
|
5273
|
+
* Called from main.ts after subscription restore completes.
|
|
5274
|
+
* Returns buffered messages so they can be sent to subscribed sessions.
|
|
5275
|
+
*/
|
|
5276
|
+
flushAuthBuffer(deviceId) {
|
|
5277
|
+
const buffer = this.authBuffers.get(deviceId) ?? [];
|
|
5278
|
+
this.authBuffers.delete(deviceId);
|
|
5279
|
+
const now = Date.now();
|
|
5280
|
+
const validMessages = buffer.filter(
|
|
5281
|
+
(item) => now - item.timestamp < _MessageBroadcasterImpl.AUTH_BUFFER_TTL_MS
|
|
5282
|
+
);
|
|
5283
|
+
if (validMessages.length > 0) {
|
|
5284
|
+
this.logger.info(
|
|
5285
|
+
{ deviceId, messageCount: validMessages.length },
|
|
5286
|
+
"Flushing auth buffer - delivering buffered messages"
|
|
5287
|
+
);
|
|
5288
|
+
}
|
|
5289
|
+
return validMessages;
|
|
5290
|
+
}
|
|
5077
5291
|
/**
|
|
5078
5292
|
* Broadcasts a message to all connected clients via tunnel.
|
|
5079
5293
|
* The tunnel handles client filtering and delivery.
|
|
@@ -5115,6 +5329,8 @@ var MessageBroadcasterImpl = class {
|
|
|
5115
5329
|
* Broadcasts a message to all clients subscribed to a session (by session ID string).
|
|
5116
5330
|
* Sends targeted messages to each subscriber in parallel with timeout.
|
|
5117
5331
|
* Prevents slow clients from blocking others.
|
|
5332
|
+
*
|
|
5333
|
+
* FIX #4: Also buffers messages for unauthenticated subscribers during auth flow.
|
|
5118
5334
|
*/
|
|
5119
5335
|
async broadcastToSubscribers(sessionId, message) {
|
|
5120
5336
|
const session = new SessionId(sessionId);
|
|
@@ -5123,9 +5339,36 @@ var MessageBroadcasterImpl = class {
|
|
|
5123
5339
|
(c) => c.isAuthenticated
|
|
5124
5340
|
);
|
|
5125
5341
|
if (authenticatedSubscribers.length === 0) {
|
|
5342
|
+
const unauthenticatedCount = subscribers.length - authenticatedSubscribers.length;
|
|
5343
|
+
if (unauthenticatedCount > 0) {
|
|
5344
|
+
this.logger.debug(
|
|
5345
|
+
{ sessionId, unauthenticatedCount },
|
|
5346
|
+
"Subscribers are authenticating - buffering message for delivery after auth"
|
|
5347
|
+
);
|
|
5348
|
+
for (const client of subscribers) {
|
|
5349
|
+
if (!client.isAuthenticated) {
|
|
5350
|
+
this.bufferMessageForAuth(client.deviceId.value, sessionId, message);
|
|
5351
|
+
}
|
|
5352
|
+
}
|
|
5353
|
+
} else {
|
|
5354
|
+
let messageType = "unknown";
|
|
5355
|
+
try {
|
|
5356
|
+
const parsed = JSON.parse(message);
|
|
5357
|
+
messageType = parsed.type ?? "unknown";
|
|
5358
|
+
} catch {
|
|
5359
|
+
}
|
|
5360
|
+
this.logger.debug(
|
|
5361
|
+
{ sessionId, messageType },
|
|
5362
|
+
"No subscribers found for session"
|
|
5363
|
+
);
|
|
5364
|
+
}
|
|
5126
5365
|
return;
|
|
5127
5366
|
}
|
|
5128
5367
|
const SEND_TIMEOUT_MS = 2e3;
|
|
5368
|
+
this.logger.debug(
|
|
5369
|
+
{ sessionId, subscriberCount: authenticatedSubscribers.length },
|
|
5370
|
+
"Broadcasting message to subscribers"
|
|
5371
|
+
);
|
|
5129
5372
|
const sendPromises = authenticatedSubscribers.map(async (client) => {
|
|
5130
5373
|
try {
|
|
5131
5374
|
await Promise.race([
|
|
@@ -5162,12 +5405,23 @@ var MessageBroadcasterImpl = class {
|
|
|
5162
5405
|
);
|
|
5163
5406
|
}
|
|
5164
5407
|
});
|
|
5165
|
-
await Promise.allSettled(sendPromises);
|
|
5408
|
+
const results = await Promise.allSettled(sendPromises);
|
|
5409
|
+
const failedCount = results.filter((r) => r.status === "rejected").length;
|
|
5410
|
+
if (failedCount > 0) {
|
|
5411
|
+
this.logger.warn(
|
|
5412
|
+
{ sessionId, totalSubscribers: authenticatedSubscribers.length, failedCount },
|
|
5413
|
+
"Some subscribers failed to receive message"
|
|
5414
|
+
);
|
|
5415
|
+
}
|
|
5416
|
+
}
|
|
5417
|
+
getAuthenticatedDeviceIds() {
|
|
5418
|
+
const clients = this.deps.clientRegistry.getAll();
|
|
5419
|
+
return clients.filter((c) => c.isAuthenticated).map((c) => c.deviceId.value);
|
|
5166
5420
|
}
|
|
5167
5421
|
};
|
|
5168
5422
|
|
|
5169
5423
|
// src/infrastructure/persistence/repositories/message-repository.ts
|
|
5170
|
-
import { eq as
|
|
5424
|
+
import { eq as eq4, desc, gt, lt, and as and2, max, count } from "drizzle-orm";
|
|
5171
5425
|
import { nanoid as nanoid2 } from "nanoid";
|
|
5172
5426
|
var MessageRepository = class {
|
|
5173
5427
|
/**
|
|
@@ -5175,7 +5429,7 @@ var MessageRepository = class {
|
|
|
5175
5429
|
*/
|
|
5176
5430
|
create(params) {
|
|
5177
5431
|
const db2 = getDatabase();
|
|
5178
|
-
const result = db2.select({ maxSeq: max(messages.sequence) }).from(messages).where(
|
|
5432
|
+
const result = db2.select({ maxSeq: max(messages.sequence) }).from(messages).where(eq4(messages.sessionId, params.sessionId)).get();
|
|
5179
5433
|
const nextSequence = (result?.maxSeq ?? 0) + 1;
|
|
5180
5434
|
const newMessage = {
|
|
5181
5435
|
id: params.messageId ?? nanoid2(16),
|
|
@@ -5196,17 +5450,17 @@ var MessageRepository = class {
|
|
|
5196
5450
|
}
|
|
5197
5451
|
getBySession(sessionId, limit = 100) {
|
|
5198
5452
|
const db2 = getDatabase();
|
|
5199
|
-
return db2.select().from(messages).where(
|
|
5453
|
+
return db2.select().from(messages).where(eq4(messages.sessionId, sessionId)).orderBy(desc(messages.sequence)).limit(limit).all();
|
|
5200
5454
|
}
|
|
5201
5455
|
getBySessionPaginated(sessionId, options = {}) {
|
|
5202
5456
|
const db2 = getDatabase();
|
|
5203
5457
|
const limit = Math.min(options.limit ?? 20, 50);
|
|
5204
|
-
const totalResult = db2.select({ count: count() }).from(messages).where(
|
|
5458
|
+
const totalResult = db2.select({ count: count() }).from(messages).where(eq4(messages.sessionId, sessionId)).get();
|
|
5205
5459
|
const totalCount = totalResult?.count ?? 0;
|
|
5206
5460
|
const whereClause = options.beforeSequence ? and2(
|
|
5207
|
-
|
|
5461
|
+
eq4(messages.sessionId, sessionId),
|
|
5208
5462
|
lt(messages.sequence, options.beforeSequence)
|
|
5209
|
-
) :
|
|
5463
|
+
) : eq4(messages.sessionId, sessionId);
|
|
5210
5464
|
const rows = db2.select().from(messages).where(whereClause).orderBy(desc(messages.sequence)).limit(limit + 1).all();
|
|
5211
5465
|
const hasMore = rows.length > limit;
|
|
5212
5466
|
const resultMessages = hasMore ? rows.slice(0, limit) : rows;
|
|
@@ -5214,117 +5468,56 @@ var MessageRepository = class {
|
|
|
5214
5468
|
}
|
|
5215
5469
|
getAfterTimestamp(sessionId, timestamp, limit = 100) {
|
|
5216
5470
|
const db2 = getDatabase();
|
|
5217
|
-
return db2.select().from(messages).where(and2(
|
|
5471
|
+
return db2.select().from(messages).where(and2(eq4(messages.sessionId, sessionId), gt(messages.createdAt, timestamp))).orderBy(messages.createdAt).limit(limit).all();
|
|
5218
5472
|
}
|
|
5219
5473
|
/**
|
|
5220
5474
|
* Updates message completion status.
|
|
5221
5475
|
*/
|
|
5222
5476
|
markComplete(messageId) {
|
|
5223
5477
|
const db2 = getDatabase();
|
|
5224
|
-
db2.update(messages).set({ isComplete: true }).where(
|
|
5478
|
+
db2.update(messages).set({ isComplete: true }).where(eq4(messages.id, messageId)).run();
|
|
5225
5479
|
}
|
|
5226
5480
|
/**
|
|
5227
5481
|
* Updates message audio output path.
|
|
5228
5482
|
*/
|
|
5229
5483
|
setAudioOutput(messageId, audioPath) {
|
|
5230
5484
|
const db2 = getDatabase();
|
|
5231
|
-
db2.update(messages).set({ audioOutputPath: audioPath }).where(
|
|
5485
|
+
db2.update(messages).set({ audioOutputPath: audioPath }).where(eq4(messages.id, messageId)).run();
|
|
5232
5486
|
}
|
|
5233
5487
|
/**
|
|
5234
5488
|
* Updates message content blocks (JSON string).
|
|
5235
5489
|
*/
|
|
5236
5490
|
updateContentBlocks(messageId, contentBlocks) {
|
|
5237
5491
|
const db2 = getDatabase();
|
|
5238
|
-
db2.update(messages).set({ contentBlocks }).where(
|
|
5492
|
+
db2.update(messages).set({ contentBlocks }).where(eq4(messages.id, messageId)).run();
|
|
5239
5493
|
}
|
|
5240
5494
|
/**
|
|
5241
5495
|
* Gets the last message for a session.
|
|
5242
5496
|
*/
|
|
5243
5497
|
getLastBySession(sessionId) {
|
|
5244
5498
|
const db2 = getDatabase();
|
|
5245
|
-
return db2.select().from(messages).where(
|
|
5499
|
+
return db2.select().from(messages).where(eq4(messages.sessionId, sessionId)).orderBy(desc(messages.sequence)).limit(1).get();
|
|
5246
5500
|
}
|
|
5247
5501
|
/**
|
|
5248
5502
|
* Gets a message by ID.
|
|
5249
5503
|
*/
|
|
5250
5504
|
getById(messageId) {
|
|
5251
5505
|
const db2 = getDatabase();
|
|
5252
|
-
return db2.select().from(messages).where(
|
|
5506
|
+
return db2.select().from(messages).where(eq4(messages.id, messageId)).get();
|
|
5253
5507
|
}
|
|
5254
5508
|
/**
|
|
5255
5509
|
* Deletes all messages for a session.
|
|
5256
5510
|
*/
|
|
5257
5511
|
deleteBySession(sessionId) {
|
|
5258
5512
|
const db2 = getDatabase();
|
|
5259
|
-
db2.delete(messages).where(
|
|
5260
|
-
}
|
|
5261
|
-
};
|
|
5262
|
-
|
|
5263
|
-
// src/infrastructure/persistence/repositories/session-repository.ts
|
|
5264
|
-
import { eq as eq4 } from "drizzle-orm";
|
|
5265
|
-
var SessionRepository = class {
|
|
5266
|
-
/**
|
|
5267
|
-
* Creates a new session record or updates if exists.
|
|
5268
|
-
*/
|
|
5269
|
-
create(params) {
|
|
5270
|
-
const db2 = getDatabase();
|
|
5271
|
-
const newSession = {
|
|
5272
|
-
id: params.id,
|
|
5273
|
-
type: params.type,
|
|
5274
|
-
workspace: params.workspace,
|
|
5275
|
-
project: params.project,
|
|
5276
|
-
worktree: params.worktree,
|
|
5277
|
-
workingDir: params.workingDir,
|
|
5278
|
-
status: "active",
|
|
5279
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
5280
|
-
};
|
|
5281
|
-
db2.insert(sessions).values(newSession).onConflictDoUpdate({
|
|
5282
|
-
target: sessions.id,
|
|
5283
|
-
set: {
|
|
5284
|
-
status: "active",
|
|
5285
|
-
terminatedAt: null
|
|
5286
|
-
}
|
|
5287
|
-
}).run();
|
|
5288
|
-
return { ...newSession, terminatedAt: null, createdAt: newSession.createdAt };
|
|
5289
|
-
}
|
|
5290
|
-
/**
|
|
5291
|
-
* Gets a session by ID.
|
|
5292
|
-
*/
|
|
5293
|
-
getById(sessionId) {
|
|
5294
|
-
const db2 = getDatabase();
|
|
5295
|
-
return db2.select().from(sessions).where(eq4(sessions.id, sessionId)).get();
|
|
5296
|
-
}
|
|
5297
|
-
/**
|
|
5298
|
-
* Gets all active sessions.
|
|
5299
|
-
*/
|
|
5300
|
-
getActive() {
|
|
5301
|
-
const db2 = getDatabase();
|
|
5302
|
-
return db2.select().from(sessions).where(eq4(sessions.status, "active")).all();
|
|
5303
|
-
}
|
|
5304
|
-
/**
|
|
5305
|
-
* Marks a session as terminated.
|
|
5306
|
-
*/
|
|
5307
|
-
terminate(sessionId) {
|
|
5308
|
-
const db2 = getDatabase();
|
|
5309
|
-
db2.update(sessions).set({
|
|
5310
|
-
status: "terminated",
|
|
5311
|
-
terminatedAt: /* @__PURE__ */ new Date()
|
|
5312
|
-
}).where(eq4(sessions.id, sessionId)).run();
|
|
5313
|
-
}
|
|
5314
|
-
/**
|
|
5315
|
-
* Deletes old terminated sessions (for cleanup).
|
|
5316
|
-
*/
|
|
5317
|
-
deleteOldTerminated(_olderThan) {
|
|
5318
|
-
const db2 = getDatabase();
|
|
5319
|
-
const result = db2.delete(sessions).where(eq4(sessions.status, "terminated")).run();
|
|
5320
|
-
return result.changes;
|
|
5513
|
+
db2.delete(messages).where(eq4(messages.sessionId, sessionId)).run();
|
|
5321
5514
|
}
|
|
5322
5515
|
};
|
|
5323
5516
|
|
|
5324
5517
|
// src/infrastructure/persistence/storage/audio-storage.ts
|
|
5325
5518
|
import { mkdir as mkdir2, writeFile, readFile, unlink, rm, readdir as readdir2 } from "fs/promises";
|
|
5326
5519
|
import { join as join5 } from "path";
|
|
5327
|
-
import { existsSync as
|
|
5520
|
+
import { existsSync as existsSync4 } from "fs";
|
|
5328
5521
|
var AudioStorage = class {
|
|
5329
5522
|
baseDir;
|
|
5330
5523
|
constructor(dataDir) {
|
|
@@ -5367,7 +5560,7 @@ var AudioStorage = class {
|
|
|
5367
5560
|
* Deletes a specific audio file.
|
|
5368
5561
|
*/
|
|
5369
5562
|
async deleteAudio(path) {
|
|
5370
|
-
if (
|
|
5563
|
+
if (existsSync4(path)) {
|
|
5371
5564
|
await unlink(path);
|
|
5372
5565
|
}
|
|
5373
5566
|
}
|
|
@@ -5377,10 +5570,10 @@ var AudioStorage = class {
|
|
|
5377
5570
|
async deleteSessionAudio(sessionId) {
|
|
5378
5571
|
const inputDir = join5(this.baseDir, "input", sessionId);
|
|
5379
5572
|
const outputDir = join5(this.baseDir, "output", sessionId);
|
|
5380
|
-
if (
|
|
5573
|
+
if (existsSync4(inputDir)) {
|
|
5381
5574
|
await rm(inputDir, { recursive: true, force: true });
|
|
5382
5575
|
}
|
|
5383
|
-
if (
|
|
5576
|
+
if (existsSync4(outputDir)) {
|
|
5384
5577
|
await rm(outputDir, { recursive: true, force: true });
|
|
5385
5578
|
}
|
|
5386
5579
|
}
|
|
@@ -5388,7 +5581,7 @@ var AudioStorage = class {
|
|
|
5388
5581
|
* Checks if an audio file exists.
|
|
5389
5582
|
*/
|
|
5390
5583
|
exists(path) {
|
|
5391
|
-
return
|
|
5584
|
+
return existsSync4(path);
|
|
5392
5585
|
}
|
|
5393
5586
|
/**
|
|
5394
5587
|
* Finds audio file by message ID across all sessions.
|
|
@@ -5400,7 +5593,7 @@ var AudioStorage = class {
|
|
|
5400
5593
|
*/
|
|
5401
5594
|
async findAudioByMessageId(messageId, type) {
|
|
5402
5595
|
const typeDir = join5(this.baseDir, type);
|
|
5403
|
-
if (!
|
|
5596
|
+
if (!existsSync4(typeDir)) {
|
|
5404
5597
|
return null;
|
|
5405
5598
|
}
|
|
5406
5599
|
try {
|
|
@@ -6385,7 +6578,9 @@ Test Files 3 passed (3)
|
|
|
6385
6578
|
};
|
|
6386
6579
|
|
|
6387
6580
|
// src/infrastructure/persistence/in-memory-session-manager.ts
|
|
6388
|
-
import { EventEmitter as
|
|
6581
|
+
import { EventEmitter as EventEmitter6 } from "events";
|
|
6582
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
|
|
6583
|
+
import { join as join9 } from "path";
|
|
6389
6584
|
import { nanoid as nanoid3 } from "nanoid";
|
|
6390
6585
|
|
|
6391
6586
|
// src/domain/entities/supervisor-session.ts
|
|
@@ -6421,35 +6616,1830 @@ var SupervisorSession = class extends Session {
|
|
|
6421
6616
|
}
|
|
6422
6617
|
};
|
|
6423
6618
|
|
|
6424
|
-
// src/
|
|
6425
|
-
var
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
this.
|
|
6435
|
-
this.
|
|
6436
|
-
this.setupAgentSessionSync();
|
|
6619
|
+
// src/domain/entities/backlog-agent-session.ts
|
|
6620
|
+
var BacklogAgentSession = class extends Session {
|
|
6621
|
+
_agentName;
|
|
6622
|
+
_backlogId;
|
|
6623
|
+
_harnessRunning = false;
|
|
6624
|
+
constructor(props) {
|
|
6625
|
+
super({
|
|
6626
|
+
...props,
|
|
6627
|
+
type: "backlog-agent"
|
|
6628
|
+
});
|
|
6629
|
+
this._agentName = props.agentName;
|
|
6630
|
+
this._backlogId = props.backlogId;
|
|
6437
6631
|
}
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
this.
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6632
|
+
get agentName() {
|
|
6633
|
+
return this._agentName;
|
|
6634
|
+
}
|
|
6635
|
+
get backlogId() {
|
|
6636
|
+
return this._backlogId;
|
|
6637
|
+
}
|
|
6638
|
+
get harnessRunning() {
|
|
6639
|
+
return this._harnessRunning;
|
|
6640
|
+
}
|
|
6641
|
+
setHarnessRunning(running) {
|
|
6642
|
+
this._harnessRunning = running;
|
|
6643
|
+
if (running) {
|
|
6644
|
+
this.markBusy();
|
|
6645
|
+
} else {
|
|
6646
|
+
this.markIdle();
|
|
6647
|
+
}
|
|
6648
|
+
}
|
|
6649
|
+
terminate() {
|
|
6650
|
+
this._harnessRunning = false;
|
|
6651
|
+
this.markTerminated();
|
|
6652
|
+
return Promise.resolve();
|
|
6653
|
+
}
|
|
6654
|
+
toInfo() {
|
|
6655
|
+
const info = super.toInfo();
|
|
6656
|
+
return {
|
|
6657
|
+
...info,
|
|
6658
|
+
agent_name: this._agentName,
|
|
6659
|
+
backlog_id: this._backlogId,
|
|
6660
|
+
harness_running: this._harnessRunning
|
|
6661
|
+
// backlog_summary will be populated by BacklogAgentManager if needed
|
|
6662
|
+
};
|
|
6663
|
+
}
|
|
6664
|
+
};
|
|
6665
|
+
|
|
6666
|
+
// src/infrastructure/agents/backlog-agent-manager.ts
|
|
6667
|
+
import { EventEmitter as EventEmitter5 } from "events";
|
|
6668
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
6669
|
+
import { join as join8 } from "path";
|
|
6670
|
+
|
|
6671
|
+
// src/domain/value-objects/backlog.ts
|
|
6672
|
+
import { z as z3 } from "zod";
|
|
6673
|
+
var TaskSchema = z3.object({
|
|
6674
|
+
id: z3.number().describe("Internal task ID (sequential)"),
|
|
6675
|
+
external_id: z3.string().optional().describe("External system ID (e.g., AUTH-123)"),
|
|
6676
|
+
external_url: z3.string().optional().describe("Link to external issue"),
|
|
6677
|
+
title: z3.string().describe("Task title"),
|
|
6678
|
+
description: z3.string().describe("Detailed description"),
|
|
6679
|
+
acceptance_criteria: z3.array(z3.string()).describe("Acceptance criteria"),
|
|
6680
|
+
dependencies: z3.array(z3.number()).describe("Task IDs this task depends on"),
|
|
6681
|
+
priority: z3.enum(["low", "medium", "high"]).describe("Priority level"),
|
|
6682
|
+
complexity: z3.enum(["simple", "moderate", "complex"]).describe("Estimated complexity"),
|
|
6683
|
+
status: z3.enum(["pending", "in_progress", "completed", "failed", "skipped"]).describe("Current status"),
|
|
6684
|
+
started_at: z3.string().datetime().optional().describe("When task execution started"),
|
|
6685
|
+
completed_at: z3.string().datetime().optional().describe("When task was completed"),
|
|
6686
|
+
error: z3.string().optional().describe("Error message if task failed"),
|
|
6687
|
+
commit_hash: z3.string().optional().describe("Git commit hash if code was written"),
|
|
6688
|
+
retry_count: z3.number().default(0).describe("Number of times task was retried")
|
|
6689
|
+
});
|
|
6690
|
+
var TaskSourceSchema = z3.object({
|
|
6691
|
+
type: z3.enum(["jira", "github", "gitlab", "linear", "notion", "manual"]).describe("Source system"),
|
|
6692
|
+
system: z3.string().optional().describe("Human-friendly system name"),
|
|
6693
|
+
issue_id: z3.string().optional().describe("Single issue/epic ID"),
|
|
6694
|
+
sprint_id: z3.number().optional().describe("Jira sprint ID"),
|
|
6695
|
+
project_key: z3.string().optional().describe("Jira project key"),
|
|
6696
|
+
repository: z3.string().optional().describe("GitHub repo (owner/repo)"),
|
|
6697
|
+
labels: z3.array(z3.string()).optional().describe("GitHub/GitLab labels"),
|
|
6698
|
+
url: z3.string().optional().describe("URL for custom sources")
|
|
6699
|
+
});
|
|
6700
|
+
var BacklogSchema = z3.object({
|
|
6701
|
+
id: z3.string().describe("Unique backlog identifier"),
|
|
6702
|
+
project: z3.string().describe("Project name"),
|
|
6703
|
+
worktree: z3.string().optional().describe("Git worktree/branch name (omitted for main/master)"),
|
|
6704
|
+
agent: z3.enum(["claude", "cursor", "opencode"]).describe("Agent to use for execution"),
|
|
6705
|
+
source: TaskSourceSchema.describe("Where tasks came from"),
|
|
6706
|
+
created_at: z3.string().datetime().describe("When backlog was created"),
|
|
6707
|
+
updated_at: z3.string().datetime().optional().describe("Last update time"),
|
|
6708
|
+
tasks: z3.array(TaskSchema).describe("List of tasks"),
|
|
6709
|
+
completed_at: z3.string().datetime().optional().describe("When all tasks completed"),
|
|
6710
|
+
summary: z3.object({
|
|
6711
|
+
total: z3.number(),
|
|
6712
|
+
completed: z3.number(),
|
|
6713
|
+
failed: z3.number(),
|
|
6714
|
+
in_progress: z3.number(),
|
|
6715
|
+
pending: z3.number()
|
|
6716
|
+
}).optional().describe("Task count summary")
|
|
6717
|
+
});
|
|
6718
|
+
function recalculateSummary(backlog) {
|
|
6719
|
+
const summary = {
|
|
6720
|
+
total: backlog.tasks.length,
|
|
6721
|
+
completed: 0,
|
|
6722
|
+
failed: 0,
|
|
6723
|
+
in_progress: 0,
|
|
6724
|
+
pending: 0
|
|
6725
|
+
};
|
|
6726
|
+
for (const task of backlog.tasks) {
|
|
6727
|
+
if (task.status === "completed") summary.completed++;
|
|
6728
|
+
else if (task.status === "failed") summary.failed++;
|
|
6729
|
+
else if (task.status === "in_progress") summary.in_progress++;
|
|
6730
|
+
else if (task.status === "pending") summary.pending++;
|
|
6731
|
+
}
|
|
6732
|
+
return {
|
|
6733
|
+
...backlog,
|
|
6734
|
+
summary,
|
|
6735
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
6736
|
+
};
|
|
6737
|
+
}
|
|
6738
|
+
|
|
6739
|
+
// src/infrastructure/agents/backlog-harness.ts
|
|
6740
|
+
import { EventEmitter as EventEmitter3 } from "events";
|
|
6741
|
+
import { readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
6742
|
+
import { join as join6 } from "path";
|
|
6743
|
+
var BacklogHarness = class _BacklogHarness extends EventEmitter3 {
|
|
6744
|
+
backlog;
|
|
6745
|
+
isRunning = false;
|
|
6746
|
+
isPaused = false;
|
|
6747
|
+
workingDir;
|
|
6748
|
+
agentSessionManager;
|
|
6749
|
+
logger;
|
|
6750
|
+
startTime = 0;
|
|
6751
|
+
selectedAgent;
|
|
6752
|
+
constructor(backlog, workingDir, selectedAgent, agentSessionManager, logger) {
|
|
6753
|
+
super();
|
|
6754
|
+
this.backlog = backlog;
|
|
6755
|
+
this.workingDir = workingDir;
|
|
6756
|
+
this.selectedAgent = selectedAgent;
|
|
6757
|
+
this.agentSessionManager = agentSessionManager;
|
|
6758
|
+
this.logger = logger;
|
|
6759
|
+
}
|
|
6760
|
+
/**
|
|
6761
|
+
* Start autonomous execution of tasks.
|
|
6762
|
+
*/
|
|
6763
|
+
async start() {
|
|
6764
|
+
if (this.isRunning) {
|
|
6765
|
+
this.logger.warn("Harness already running");
|
|
6766
|
+
return;
|
|
6767
|
+
}
|
|
6768
|
+
this.isRunning = true;
|
|
6769
|
+
this.isPaused = false;
|
|
6770
|
+
this.startTime = Date.now();
|
|
6771
|
+
this.emit("harness-started", {
|
|
6772
|
+
totalTasks: this.backlog.tasks.length
|
|
6773
|
+
});
|
|
6774
|
+
this.broadcastOutput({
|
|
6775
|
+
id: "harness-start",
|
|
6776
|
+
block_type: "status",
|
|
6777
|
+
content: `\u{1F680} Starting Harness. Total tasks: ${this.backlog.tasks.length}`
|
|
6778
|
+
});
|
|
6779
|
+
try {
|
|
6780
|
+
await this.executeLoop();
|
|
6781
|
+
} catch (error) {
|
|
6782
|
+
this.logger.error({ error }, "Harness error");
|
|
6783
|
+
this.broadcastOutput({
|
|
6784
|
+
id: "harness-error",
|
|
6785
|
+
block_type: "error",
|
|
6786
|
+
content: `Harness error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
6787
|
+
});
|
|
6788
|
+
} finally {
|
|
6789
|
+
this.isRunning = false;
|
|
6790
|
+
}
|
|
6791
|
+
}
|
|
6792
|
+
/**
|
|
6793
|
+
* Pause execution (current task finishes, then pauses).
|
|
6794
|
+
*/
|
|
6795
|
+
pause() {
|
|
6796
|
+
if (!this.isRunning) {
|
|
6797
|
+
this.logger.warn("Harness not running");
|
|
6798
|
+
return;
|
|
6799
|
+
}
|
|
6800
|
+
this.isPaused = true;
|
|
6801
|
+
this.emit("harness-paused", void 0);
|
|
6802
|
+
this.broadcastOutput({
|
|
6803
|
+
id: "harness-pause",
|
|
6804
|
+
block_type: "status",
|
|
6805
|
+
content: "\u23F8\uFE0F Harness paused"
|
|
6806
|
+
});
|
|
6807
|
+
}
|
|
6808
|
+
/**
|
|
6809
|
+
* Resume paused execution.
|
|
6810
|
+
*/
|
|
6811
|
+
resume() {
|
|
6812
|
+
if (!this.isRunning || !this.isPaused) {
|
|
6813
|
+
this.logger.warn("Cannot resume harness");
|
|
6814
|
+
return;
|
|
6815
|
+
}
|
|
6816
|
+
this.isPaused = false;
|
|
6817
|
+
this.emit("harness-resumed", void 0);
|
|
6818
|
+
this.broadcastOutput({
|
|
6819
|
+
id: "harness-resume",
|
|
6820
|
+
block_type: "status",
|
|
6821
|
+
content: "\u25B6\uFE0F Harness resumed"
|
|
6822
|
+
});
|
|
6823
|
+
}
|
|
6824
|
+
/**
|
|
6825
|
+
* Stop execution immediately.
|
|
6826
|
+
*/
|
|
6827
|
+
stop() {
|
|
6828
|
+
this.isRunning = false;
|
|
6829
|
+
this.isPaused = false;
|
|
6830
|
+
this.emit("harness-stopped", void 0);
|
|
6831
|
+
this.broadcastOutput({
|
|
6832
|
+
id: "harness-stop",
|
|
6833
|
+
block_type: "status",
|
|
6834
|
+
content: "\u23F9\uFE0F Harness stopped"
|
|
6835
|
+
});
|
|
6836
|
+
}
|
|
6837
|
+
/**
|
|
6838
|
+
* Main execution loop.
|
|
6839
|
+
*/
|
|
6840
|
+
async executeLoop() {
|
|
6841
|
+
let completedCount = 0;
|
|
6842
|
+
let failedCount = 0;
|
|
6843
|
+
while (this.isRunning) {
|
|
6844
|
+
while (this.isPaused && this.isRunning) {
|
|
6845
|
+
await this.sleep(1e3);
|
|
6846
|
+
}
|
|
6847
|
+
if (!this.isRunning) break;
|
|
6848
|
+
const task = this.findNextPendingTask();
|
|
6849
|
+
if (!task) {
|
|
6850
|
+
break;
|
|
6851
|
+
}
|
|
6852
|
+
if (!this.canExecuteTask(task)) {
|
|
6853
|
+
this.logger.debug(`Task ${task.id} dependencies not ready, skipping`);
|
|
6854
|
+
continue;
|
|
6855
|
+
}
|
|
6856
|
+
try {
|
|
6857
|
+
await this.executeTask(task);
|
|
6858
|
+
completedCount++;
|
|
6859
|
+
} catch (error) {
|
|
6860
|
+
this.logger.error({ error, taskId: task.id }, `Task ${task.id} failed`);
|
|
6861
|
+
failedCount++;
|
|
6862
|
+
this.emit("task-failed", {
|
|
6863
|
+
taskId: task.id,
|
|
6864
|
+
title: task.title,
|
|
6865
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
6866
|
+
});
|
|
6867
|
+
this.updateTaskStatus(task.id, "failed", {
|
|
6868
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
6869
|
+
});
|
|
6870
|
+
}
|
|
6871
|
+
this.saveBacklog();
|
|
6872
|
+
}
|
|
6873
|
+
const duration = Date.now() - this.startTime;
|
|
6874
|
+
const summary = this.backlog.summary ?? {
|
|
6875
|
+
total: this.backlog.tasks.length,
|
|
6876
|
+
completed: completedCount,
|
|
6877
|
+
failed: failedCount,
|
|
6878
|
+
in_progress: 0,
|
|
6879
|
+
pending: this.backlog.tasks.filter((t) => t.status === "pending").length
|
|
6880
|
+
};
|
|
6881
|
+
this.emit("harness-completed", {
|
|
6882
|
+
completed: summary.completed,
|
|
6883
|
+
failed: summary.failed,
|
|
6884
|
+
total: summary.total,
|
|
6885
|
+
duration
|
|
6886
|
+
});
|
|
6887
|
+
this.broadcastOutput({
|
|
6888
|
+
id: "harness-complete",
|
|
6889
|
+
block_type: "status",
|
|
6890
|
+
content: `\u2705 Harness completed. Completed: ${summary.completed}/${summary.total}, Failed: ${summary.failed}`
|
|
6891
|
+
});
|
|
6892
|
+
}
|
|
6893
|
+
/**
|
|
6894
|
+
* Execute a single task.
|
|
6895
|
+
*/
|
|
6896
|
+
async executeTask(task) {
|
|
6897
|
+
const startTime = Date.now();
|
|
6898
|
+
this.emit("task-started", {
|
|
6899
|
+
taskId: task.id,
|
|
6900
|
+
title: task.title,
|
|
6901
|
+
externalId: task.external_id
|
|
6902
|
+
});
|
|
6903
|
+
this.updateTaskStatus(task.id, "in_progress", { started_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
6904
|
+
this.broadcastOutput({
|
|
6905
|
+
id: `task-${task.id}-start`,
|
|
6906
|
+
block_type: "status",
|
|
6907
|
+
content: `\u{1F4CC} Task ${task.id}: ${task.title}`
|
|
6908
|
+
});
|
|
6909
|
+
const prompt = this.buildTaskPrompt(task);
|
|
6910
|
+
const taskIndex = this.backlog.tasks.findIndex((t) => t.id === task.id) + 1;
|
|
6911
|
+
const totalTasks = this.backlog.tasks.length;
|
|
6912
|
+
const taskSessionId = `backlog-${this.backlog.id}-task-${task.id}`;
|
|
6913
|
+
const taskAgentName = `Task ${taskIndex}/${totalTasks}: ${task.title}`;
|
|
6914
|
+
this.logger.info(
|
|
6915
|
+
{ sessionId: taskSessionId, agent: this.selectedAgent, taskIndex, totalTasks },
|
|
6916
|
+
"Creating new agent session for task iteration"
|
|
6917
|
+
);
|
|
6918
|
+
const agentType = this.selectedAgent;
|
|
6919
|
+
const newSession = this.agentSessionManager.createSession(
|
|
6920
|
+
agentType,
|
|
6921
|
+
this.workingDir,
|
|
6922
|
+
taskSessionId,
|
|
6923
|
+
taskAgentName
|
|
6924
|
+
);
|
|
6925
|
+
const sessionId = newSession.sessionId;
|
|
6926
|
+
this.logger.info(
|
|
6927
|
+
{ sessionId, agent: this.selectedAgent, taskIndex, taskTitle: task.title },
|
|
6928
|
+
"Created new agent session for task"
|
|
6929
|
+
);
|
|
6930
|
+
await new Promise((resolve2, reject) => {
|
|
6931
|
+
let isResolved = false;
|
|
6932
|
+
let executionTimeoutHandle;
|
|
6933
|
+
const onBlocks = (emittedSessionId, blocks, isComplete) => {
|
|
6934
|
+
if (emittedSessionId !== sessionId) {
|
|
6935
|
+
return;
|
|
6936
|
+
}
|
|
6937
|
+
this.broadcastOutput(...blocks);
|
|
6938
|
+
if (isResolved) {
|
|
6939
|
+
return;
|
|
6940
|
+
}
|
|
6941
|
+
let commitHash;
|
|
6942
|
+
let hasCommitDetected = false;
|
|
6943
|
+
for (const block of blocks) {
|
|
6944
|
+
if (block.block_type === "text") {
|
|
6945
|
+
const content = block.content;
|
|
6946
|
+
const commitRegex = /commit\s+([a-f0-9]{7,40})/i;
|
|
6947
|
+
const commitMatch = commitRegex.exec(content);
|
|
6948
|
+
if (commitMatch) {
|
|
6949
|
+
commitHash = commitMatch[1];
|
|
6950
|
+
hasCommitDetected = true;
|
|
6951
|
+
break;
|
|
6952
|
+
}
|
|
6953
|
+
const headRegex = /HEAD\s+is\s+now\s+at\s+([a-f0-9]{7,40})/i;
|
|
6954
|
+
const headMatch = headRegex.exec(content);
|
|
6955
|
+
if (headMatch) {
|
|
6956
|
+
commitHash = headMatch[1];
|
|
6957
|
+
hasCommitDetected = true;
|
|
6958
|
+
break;
|
|
6959
|
+
}
|
|
6960
|
+
}
|
|
6961
|
+
}
|
|
6962
|
+
if (hasCommitDetected && commitHash) {
|
|
6963
|
+
isResolved = true;
|
|
6964
|
+
if (executionTimeoutHandle) clearTimeout(executionTimeoutHandle);
|
|
6965
|
+
this.agentSessionManager.removeListener("blocks", onBlocks);
|
|
6966
|
+
const duration = Date.now() - startTime;
|
|
6967
|
+
this.emit("task-completed", {
|
|
6968
|
+
taskId: task.id,
|
|
6969
|
+
title: task.title,
|
|
6970
|
+
success: true,
|
|
6971
|
+
commitHash,
|
|
6972
|
+
duration
|
|
6973
|
+
});
|
|
6974
|
+
this.updateTaskStatus(task.id, "completed", {
|
|
6975
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6976
|
+
commit_hash: commitHash
|
|
6977
|
+
});
|
|
6978
|
+
this.logger.info(
|
|
6979
|
+
{ taskId: task.id, title: task.title, commitHash, duration },
|
|
6980
|
+
"Task completed with commit"
|
|
6981
|
+
);
|
|
6982
|
+
resolve2();
|
|
6983
|
+
return;
|
|
6984
|
+
}
|
|
6985
|
+
if (isComplete && !isResolved) {
|
|
6986
|
+
isResolved = true;
|
|
6987
|
+
if (executionTimeoutHandle) clearTimeout(executionTimeoutHandle);
|
|
6988
|
+
this.agentSessionManager.removeListener("blocks", onBlocks);
|
|
6989
|
+
const duration = Date.now() - startTime;
|
|
6990
|
+
const errorMsg = "Task execution completed but no commit was detected. The agent may not have completed the implementation.";
|
|
6991
|
+
this.emit("task-completed", {
|
|
6992
|
+
taskId: task.id,
|
|
6993
|
+
title: task.title,
|
|
6994
|
+
success: false,
|
|
6995
|
+
duration
|
|
6996
|
+
});
|
|
6997
|
+
this.updateTaskStatus(task.id, "failed", {
|
|
6998
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6999
|
+
error: errorMsg
|
|
7000
|
+
});
|
|
7001
|
+
this.logger.warn(
|
|
7002
|
+
{ taskId: task.id, title: task.title, duration },
|
|
7003
|
+
errorMsg
|
|
7004
|
+
);
|
|
7005
|
+
reject(new Error(errorMsg));
|
|
7006
|
+
}
|
|
7007
|
+
};
|
|
7008
|
+
this.agentSessionManager.on("blocks", onBlocks);
|
|
7009
|
+
executionTimeoutHandle = setTimeout(() => {
|
|
7010
|
+
if (!isResolved) {
|
|
7011
|
+
isResolved = true;
|
|
7012
|
+
this.agentSessionManager.removeListener("blocks", onBlocks);
|
|
7013
|
+
const duration = Date.now() - startTime;
|
|
7014
|
+
this.emit("task-completed", {
|
|
7015
|
+
taskId: task.id,
|
|
7016
|
+
title: task.title,
|
|
7017
|
+
success: false,
|
|
7018
|
+
duration
|
|
7019
|
+
});
|
|
7020
|
+
this.updateTaskStatus(task.id, "failed", {
|
|
7021
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7022
|
+
error: "Task execution timeout (30 minutes exceeded)"
|
|
7023
|
+
});
|
|
7024
|
+
reject(new Error("Task execution timeout"));
|
|
7025
|
+
}
|
|
7026
|
+
}, 30 * 60 * 1e3);
|
|
7027
|
+
this.agentSessionManager.executeCommand(sessionId, prompt).catch((error) => {
|
|
7028
|
+
if (!isResolved) {
|
|
7029
|
+
isResolved = true;
|
|
7030
|
+
if (executionTimeoutHandle) clearTimeout(executionTimeoutHandle);
|
|
7031
|
+
this.agentSessionManager.removeListener("blocks", onBlocks);
|
|
7032
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
7033
|
+
}
|
|
7034
|
+
});
|
|
7035
|
+
});
|
|
7036
|
+
}
|
|
7037
|
+
/**
|
|
7038
|
+
* Build a prompt for the coding agent based on task.
|
|
7039
|
+
*/
|
|
7040
|
+
buildTaskPrompt(task) {
|
|
7041
|
+
return `
|
|
7042
|
+
You are a senior developer. Complete this task:
|
|
7043
|
+
|
|
7044
|
+
## Task: ${task.title}
|
|
7045
|
+
|
|
7046
|
+
${task.description}
|
|
7047
|
+
|
|
7048
|
+
## Acceptance Criteria:
|
|
7049
|
+
${task.acceptance_criteria.map((c) => `- ${c}`).join("\n")}
|
|
7050
|
+
|
|
7051
|
+
## Instructions:
|
|
7052
|
+
1. Implement the feature/fix according to the acceptance criteria
|
|
7053
|
+
2. Write/update tests to cover your changes
|
|
7054
|
+
3. Run all tests and make sure they pass
|
|
7055
|
+
4. Commit your changes with a descriptive message
|
|
7056
|
+
5. Ensure code follows project conventions
|
|
7057
|
+
|
|
7058
|
+
## IMPORTANT - Completion Signal:
|
|
7059
|
+
When you have COMPLETED the task and committed your changes:
|
|
7060
|
+
- Make sure your last action is a git commit
|
|
7061
|
+
- Output the full commit hash in your final summary
|
|
7062
|
+
- Write a clear summary confirming what was implemented and that acceptance criteria are met
|
|
7063
|
+
|
|
7064
|
+
Do NOT write "Task complete" or similar - just commit your code and include the hash in your output.
|
|
7065
|
+
`.trim();
|
|
7066
|
+
}
|
|
7067
|
+
/**
|
|
7068
|
+
* Find next pending task.
|
|
7069
|
+
*/
|
|
7070
|
+
findNextPendingTask() {
|
|
7071
|
+
return this.backlog.tasks.find((t) => t.status === "pending");
|
|
7072
|
+
}
|
|
7073
|
+
/**
|
|
7074
|
+
* Check if task dependencies are satisfied.
|
|
7075
|
+
*/
|
|
7076
|
+
canExecuteTask(task) {
|
|
7077
|
+
if (task.dependencies.length === 0) {
|
|
7078
|
+
return true;
|
|
7079
|
+
}
|
|
7080
|
+
return task.dependencies.every((depId) => {
|
|
7081
|
+
const depTask = this.backlog.tasks.find((t) => t.id === depId);
|
|
7082
|
+
return depTask?.status === "completed";
|
|
7083
|
+
});
|
|
7084
|
+
}
|
|
7085
|
+
/**
|
|
7086
|
+
* Update task status.
|
|
7087
|
+
*/
|
|
7088
|
+
updateTaskStatus(taskId, status, updates = {}) {
|
|
7089
|
+
const task = this.backlog.tasks.find((t) => t.id === taskId);
|
|
7090
|
+
if (task) {
|
|
7091
|
+
task.status = status;
|
|
7092
|
+
Object.assign(task, updates);
|
|
7093
|
+
this.backlog = recalculateSummary(this.backlog);
|
|
7094
|
+
}
|
|
7095
|
+
}
|
|
7096
|
+
/**
|
|
7097
|
+
* Save backlog to file.
|
|
7098
|
+
*/
|
|
7099
|
+
saveBacklog() {
|
|
7100
|
+
const backlogPath = join6(this.workingDir, "backlog.json");
|
|
7101
|
+
try {
|
|
7102
|
+
writeFileSync(backlogPath, JSON.stringify(this.backlog, null, 2));
|
|
7103
|
+
this.logger.debug(`Saved backlog to ${backlogPath}`);
|
|
7104
|
+
} catch (error) {
|
|
7105
|
+
this.logger.error({ error }, "Failed to save backlog");
|
|
7106
|
+
}
|
|
7107
|
+
}
|
|
7108
|
+
/**
|
|
7109
|
+
* Broadcast output blocks to UI.
|
|
7110
|
+
*/
|
|
7111
|
+
broadcastOutput(...blocks) {
|
|
7112
|
+
this.emit("output", blocks);
|
|
7113
|
+
}
|
|
7114
|
+
/**
|
|
7115
|
+
* Sleep helper.
|
|
7116
|
+
*/
|
|
7117
|
+
sleep(ms) {
|
|
7118
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
7119
|
+
}
|
|
7120
|
+
/**
|
|
7121
|
+
* Load backlog from file.
|
|
7122
|
+
* @param selectedAgent - The agent type to use for harness execution
|
|
7123
|
+
*/
|
|
7124
|
+
static loadFromFile(path, selectedAgent, agentSessionManager, logger) {
|
|
7125
|
+
const content = readFileSync2(path, "utf-8");
|
|
7126
|
+
const backlog = JSON.parse(content);
|
|
7127
|
+
const workingDir = path.substring(0, path.lastIndexOf("/"));
|
|
7128
|
+
return new _BacklogHarness(backlog, workingDir, selectedAgent, agentSessionManager, logger);
|
|
7129
|
+
}
|
|
7130
|
+
};
|
|
7131
|
+
|
|
7132
|
+
// src/infrastructure/agents/base/lang-graph-agent.ts
|
|
7133
|
+
import { EventEmitter as EventEmitter4 } from "events";
|
|
7134
|
+
import { ChatOpenAI } from "@langchain/openai";
|
|
7135
|
+
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
|
7136
|
+
import { HumanMessage, AIMessage, isAIMessage } from "@langchain/core/messages";
|
|
7137
|
+
var LangGraphAgent = class extends EventEmitter4 {
|
|
7138
|
+
logger;
|
|
7139
|
+
agent = null;
|
|
7140
|
+
conversationHistory = [];
|
|
7141
|
+
abortController = null;
|
|
7142
|
+
isExecuting = false;
|
|
7143
|
+
isCancelled = false;
|
|
7144
|
+
/** Tracks if we're processing a command (including STT, before LLM execution) */
|
|
7145
|
+
isProcessingCommand = false;
|
|
7146
|
+
/** Timestamp when current execution started (for race condition protection) */
|
|
7147
|
+
executionStartedAt = 0;
|
|
7148
|
+
constructor(logger) {
|
|
7149
|
+
super();
|
|
7150
|
+
this.logger = logger.child({ component: this.constructor.name });
|
|
7151
|
+
}
|
|
7152
|
+
/**
|
|
7153
|
+
* Called after successful execution completion.
|
|
7154
|
+
* Subclasses can override to perform agent-specific post-execution logic.
|
|
7155
|
+
*/
|
|
7156
|
+
async onExecutionComplete(_blocks, _finalOutput) {
|
|
7157
|
+
}
|
|
7158
|
+
/**
|
|
7159
|
+
* Initializes the LangGraph agent and state manager.
|
|
7160
|
+
* Called by subclasses during construction.
|
|
7161
|
+
*/
|
|
7162
|
+
initializeAgent() {
|
|
7163
|
+
if (this.agent) {
|
|
7164
|
+
return;
|
|
7165
|
+
}
|
|
7166
|
+
try {
|
|
7167
|
+
const env = getEnv();
|
|
7168
|
+
const llm = this.createLLM(env);
|
|
7169
|
+
const tools = this.createTools();
|
|
7170
|
+
this.logger.info({ toolCount: tools.length }, `Creating ${this.constructor.name} with tools`);
|
|
7171
|
+
this.agent = createReactAgent({
|
|
7172
|
+
llm,
|
|
7173
|
+
tools
|
|
7174
|
+
});
|
|
7175
|
+
} catch (error) {
|
|
7176
|
+
this.logger.error({ error }, `Failed to initialize ${this.constructor.name}`);
|
|
7177
|
+
throw error;
|
|
7178
|
+
}
|
|
7179
|
+
}
|
|
7180
|
+
/**
|
|
7181
|
+
* Creates the LLM instance based on environment configuration.
|
|
7182
|
+
*/
|
|
7183
|
+
createLLM(env) {
|
|
7184
|
+
const provider = env.AGENT_PROVIDER;
|
|
7185
|
+
const apiKey = env.AGENT_API_KEY;
|
|
7186
|
+
const modelName = env.AGENT_MODEL_NAME;
|
|
7187
|
+
const baseUrl = env.AGENT_BASE_URL;
|
|
7188
|
+
const temperature = env.AGENT_TEMPERATURE;
|
|
7189
|
+
if (!apiKey) {
|
|
7190
|
+
throw new Error(`AGENT_API_KEY is required for ${this.constructor.name}`);
|
|
7191
|
+
}
|
|
7192
|
+
this.logger.info({ provider, model: modelName }, "Initializing LLM");
|
|
7193
|
+
return new ChatOpenAI({
|
|
7194
|
+
openAIApiKey: apiKey,
|
|
7195
|
+
modelName,
|
|
7196
|
+
temperature,
|
|
7197
|
+
configuration: baseUrl ? {
|
|
7198
|
+
baseURL: baseUrl
|
|
7199
|
+
} : void 0
|
|
7200
|
+
});
|
|
7201
|
+
}
|
|
7202
|
+
/**
|
|
7203
|
+
* Executes a command with streaming output.
|
|
7204
|
+
* Emits 'blocks' events as content is generated.
|
|
7205
|
+
*
|
|
7206
|
+
* This is the unified streaming method used by all agents.
|
|
7207
|
+
* Subclasses should NOT override this unless they have very specific requirements.
|
|
7208
|
+
*/
|
|
7209
|
+
async executeWithStream(command, deviceId) {
|
|
7210
|
+
this.logger.info(
|
|
7211
|
+
{ command, deviceId, wasCancelledBefore: this.isCancelled },
|
|
7212
|
+
`Executing ${this.constructor.name} with streaming`
|
|
7213
|
+
);
|
|
7214
|
+
this.abortController = new AbortController();
|
|
7215
|
+
this.isExecuting = true;
|
|
7216
|
+
this.isCancelled = false;
|
|
7217
|
+
this.executionStartedAt = Date.now();
|
|
7218
|
+
this.logger.debug({ isCancelled: this.isCancelled, isExecuting: this.isExecuting }, "Flags reset for new execution");
|
|
7219
|
+
try {
|
|
7220
|
+
const stateManager = this.createStateManager();
|
|
7221
|
+
const history = await stateManager.loadHistory();
|
|
7222
|
+
this.conversationHistory = history;
|
|
7223
|
+
const messages2 = [
|
|
7224
|
+
...this.buildSystemMessage(),
|
|
7225
|
+
...this.buildHistoryMessages(history),
|
|
7226
|
+
new HumanMessage(command)
|
|
7227
|
+
];
|
|
7228
|
+
if (this.isExecuting && !this.isCancelled) {
|
|
7229
|
+
const statusBlock = createStatusBlock("Processing...");
|
|
7230
|
+
this.logger.debug({ deviceId, blockType: "status" }, "Emitting status block");
|
|
7231
|
+
this.emit("blocks", deviceId, [statusBlock], false);
|
|
7232
|
+
}
|
|
7233
|
+
this.logger.info({ deviceId }, `Starting LangGraph agent stream for ${this.constructor.name}`);
|
|
7234
|
+
if (!this.agent) {
|
|
7235
|
+
throw new Error("Agent not initialized");
|
|
7236
|
+
}
|
|
7237
|
+
const stream = await this.agent.stream(
|
|
7238
|
+
{ messages: messages2 },
|
|
7239
|
+
{
|
|
7240
|
+
streamMode: "values",
|
|
7241
|
+
signal: this.abortController.signal
|
|
7242
|
+
}
|
|
7243
|
+
);
|
|
7244
|
+
let finalOutput = "";
|
|
7245
|
+
const allBlocks = [];
|
|
7246
|
+
for await (const chunk of stream) {
|
|
7247
|
+
if (this.isCancelled || !this.isExecuting) {
|
|
7248
|
+
this.logger.info(
|
|
7249
|
+
{ deviceId, isCancelled: this.isCancelled, isExecuting: this.isExecuting },
|
|
7250
|
+
`${this.constructor.name} execution cancelled, stopping stream processing`
|
|
7251
|
+
);
|
|
7252
|
+
return;
|
|
7253
|
+
}
|
|
7254
|
+
const chunkData = chunk;
|
|
7255
|
+
const chunkMessages = chunkData.messages;
|
|
7256
|
+
if (!chunkMessages || chunkMessages.length === 0) continue;
|
|
7257
|
+
const lastMessage = chunkMessages[chunkMessages.length - 1];
|
|
7258
|
+
if (!lastMessage) continue;
|
|
7259
|
+
if (isAIMessage(lastMessage)) {
|
|
7260
|
+
const content = lastMessage.content;
|
|
7261
|
+
if (typeof content === "string" && content.length > 0) {
|
|
7262
|
+
finalOutput = content;
|
|
7263
|
+
if (this.isExecuting && !this.isCancelled) {
|
|
7264
|
+
const textBlock = createTextBlock(content);
|
|
7265
|
+
this.emit("blocks", deviceId, [textBlock], false);
|
|
7266
|
+
accumulateBlocks(allBlocks, [textBlock]);
|
|
7267
|
+
}
|
|
7268
|
+
} else if (Array.isArray(content)) {
|
|
7269
|
+
for (const item of content) {
|
|
7270
|
+
if (typeof item === "object" && this.isExecuting && !this.isCancelled) {
|
|
7271
|
+
const block = this.parseContentItem(item);
|
|
7272
|
+
if (block) {
|
|
7273
|
+
this.emit("blocks", deviceId, [block], false);
|
|
7274
|
+
accumulateBlocks(allBlocks, [block]);
|
|
7275
|
+
}
|
|
7276
|
+
}
|
|
7277
|
+
}
|
|
7278
|
+
}
|
|
7279
|
+
}
|
|
7280
|
+
if (lastMessage.getType() === "tool" && this.isExecuting && !this.isCancelled) {
|
|
7281
|
+
const toolContent = lastMessage.content;
|
|
7282
|
+
const toolName = lastMessage.name ?? "tool";
|
|
7283
|
+
const toolCallId = lastMessage.tool_call_id;
|
|
7284
|
+
const toolBlock = createToolBlock(
|
|
7285
|
+
toolName,
|
|
7286
|
+
"completed",
|
|
7287
|
+
void 0,
|
|
7288
|
+
typeof toolContent === "string" ? toolContent : JSON.stringify(toolContent),
|
|
7289
|
+
toolCallId
|
|
7290
|
+
);
|
|
7291
|
+
this.emit("blocks", deviceId, [toolBlock], false);
|
|
7292
|
+
accumulateBlocks(allBlocks, [toolBlock]);
|
|
7293
|
+
}
|
|
7294
|
+
}
|
|
7295
|
+
if (this.isExecuting && !this.isCancelled) {
|
|
7296
|
+
this.addToHistory("user", command);
|
|
7297
|
+
this.addToHistory("assistant", finalOutput);
|
|
7298
|
+
const finalBlocks = mergeToolBlocks(allBlocks);
|
|
7299
|
+
const stateManager2 = this.createStateManager();
|
|
7300
|
+
await stateManager2.saveHistory(this.conversationHistory);
|
|
7301
|
+
await this.onExecutionComplete(finalBlocks, finalOutput);
|
|
7302
|
+
const completionBlock = createStatusBlock("Complete");
|
|
7303
|
+
this.emit("blocks", deviceId, [completionBlock], true, finalOutput, finalBlocks);
|
|
7304
|
+
this.logger.debug({ output: finalOutput.slice(0, 200) }, `${this.constructor.name} streaming completed`);
|
|
7305
|
+
} else {
|
|
7306
|
+
this.logger.info(
|
|
7307
|
+
{ deviceId, isCancelled: this.isCancelled, isExecuting: this.isExecuting },
|
|
7308
|
+
`${this.constructor.name} streaming ended due to cancellation`
|
|
7309
|
+
);
|
|
7310
|
+
}
|
|
7311
|
+
} catch (error) {
|
|
7312
|
+
if (this.isCancelled || !this.isExecuting) {
|
|
7313
|
+
this.logger.info({ deviceId }, `${this.constructor.name} execution cancelled (caught in error handler)`);
|
|
7314
|
+
return;
|
|
7315
|
+
}
|
|
7316
|
+
this.logger.error({ error, command }, `${this.constructor.name} streaming failed`);
|
|
7317
|
+
const errorMessage = error instanceof Error ? error.message : "An unexpected error occurred";
|
|
7318
|
+
const errorBlock = createErrorBlock(errorMessage);
|
|
7319
|
+
this.emit("blocks", deviceId, [errorBlock], true);
|
|
7320
|
+
} finally {
|
|
7321
|
+
this.isExecuting = false;
|
|
7322
|
+
this.abortController = null;
|
|
7323
|
+
}
|
|
7324
|
+
}
|
|
7325
|
+
/**
|
|
7326
|
+
* Parses a content item from LangGraph into a ContentBlock.
|
|
7327
|
+
*/
|
|
7328
|
+
parseContentItem(item) {
|
|
7329
|
+
const type = item.type;
|
|
7330
|
+
if (type === "text" && typeof item.text === "string" && item.text.trim()) {
|
|
7331
|
+
return createTextBlock(item.text);
|
|
7332
|
+
}
|
|
7333
|
+
if (type === "tool_use") {
|
|
7334
|
+
const name = typeof item.name === "string" ? item.name : "tool";
|
|
7335
|
+
const input = item.input;
|
|
7336
|
+
const toolUseId = typeof item.id === "string" ? item.id : void 0;
|
|
7337
|
+
return createToolBlock(name, "running", input, void 0, toolUseId);
|
|
7338
|
+
}
|
|
7339
|
+
return null;
|
|
7340
|
+
}
|
|
7341
|
+
/**
|
|
7342
|
+
* Cancels the current execution if running.
|
|
7343
|
+
* Returns true if cancellation was initiated.
|
|
7344
|
+
*/
|
|
7345
|
+
cancel() {
|
|
7346
|
+
if (!this.isProcessingCommand && !this.isExecuting) {
|
|
7347
|
+
this.logger.debug(
|
|
7348
|
+
{ isProcessingCommand: this.isProcessingCommand, isExecuting: this.isExecuting },
|
|
7349
|
+
"No active execution to cancel"
|
|
7350
|
+
);
|
|
7351
|
+
return false;
|
|
7352
|
+
}
|
|
7353
|
+
const timeSinceStart = Date.now() - this.executionStartedAt;
|
|
7354
|
+
if (this.isExecuting && timeSinceStart < 500) {
|
|
7355
|
+
this.logger.info(
|
|
7356
|
+
{ timeSinceStart, isProcessingCommand: this.isProcessingCommand },
|
|
7357
|
+
"Ignoring cancel - execution just started (race condition protection)"
|
|
7358
|
+
);
|
|
7359
|
+
return false;
|
|
7360
|
+
}
|
|
7361
|
+
this.logger.info(
|
|
7362
|
+
{ isProcessingCommand: this.isProcessingCommand, isExecuting: this.isExecuting, timeSinceStart },
|
|
7363
|
+
`Cancelling ${this.constructor.name} execution`
|
|
7364
|
+
);
|
|
7365
|
+
this.isCancelled = true;
|
|
7366
|
+
this.isExecuting = false;
|
|
7367
|
+
this.isProcessingCommand = false;
|
|
7368
|
+
if (this.abortController) {
|
|
7369
|
+
this.abortController.abort();
|
|
7370
|
+
}
|
|
7371
|
+
return true;
|
|
7372
|
+
}
|
|
7373
|
+
/**
|
|
7374
|
+
* Check if execution was cancelled.
|
|
7375
|
+
* Used by clients to filter out any late-arriving blocks.
|
|
7376
|
+
*/
|
|
7377
|
+
wasCancelled() {
|
|
7378
|
+
return this.isCancelled;
|
|
7379
|
+
}
|
|
7380
|
+
/**
|
|
7381
|
+
* Starts command processing (before STT/LLM execution).
|
|
7382
|
+
* Returns an AbortController that can be used to cancel STT and other operations.
|
|
7383
|
+
*/
|
|
7384
|
+
startProcessing() {
|
|
7385
|
+
this.abortController = new AbortController();
|
|
7386
|
+
this.isProcessingCommand = true;
|
|
7387
|
+
this.isCancelled = false;
|
|
7388
|
+
this.logger.debug("Started command processing");
|
|
7389
|
+
return this.abortController;
|
|
7390
|
+
}
|
|
7391
|
+
/**
|
|
7392
|
+
* Checks if command processing is active (STT or LLM execution).
|
|
7393
|
+
*/
|
|
7394
|
+
isProcessing() {
|
|
7395
|
+
return this.isProcessingCommand || this.isExecuting;
|
|
7396
|
+
}
|
|
7397
|
+
/**
|
|
7398
|
+
* Ends command processing (called after completion or error, not after cancel).
|
|
7399
|
+
*/
|
|
7400
|
+
endProcessing() {
|
|
7401
|
+
this.isProcessingCommand = false;
|
|
7402
|
+
this.logger.debug("Ended command processing");
|
|
7403
|
+
}
|
|
7404
|
+
/**
|
|
7405
|
+
* Gets conversation history.
|
|
7406
|
+
*/
|
|
7407
|
+
getConversationHistory() {
|
|
7408
|
+
return this.conversationHistory;
|
|
7409
|
+
}
|
|
7410
|
+
/**
|
|
7411
|
+
* Resets the cancellation state.
|
|
7412
|
+
* Call this before starting a new command to ensure previous cancellation doesn't affect it.
|
|
7413
|
+
*/
|
|
7414
|
+
resetCancellationState() {
|
|
7415
|
+
this.isCancelled = false;
|
|
7416
|
+
}
|
|
7417
|
+
/**
|
|
7418
|
+
* Restores conversation history from persistent storage.
|
|
7419
|
+
* Called on startup to sync in-memory cache with database.
|
|
7420
|
+
*/
|
|
7421
|
+
restoreHistory(history) {
|
|
7422
|
+
if (history.length === 0) return;
|
|
7423
|
+
this.conversationHistory = history.slice(-20);
|
|
7424
|
+
this.logger.debug({ messageCount: this.conversationHistory.length }, "Conversation history restored");
|
|
7425
|
+
}
|
|
7426
|
+
/**
|
|
7427
|
+
* Builds the system message for the agent.
|
|
7428
|
+
*/
|
|
7429
|
+
buildSystemMessage() {
|
|
7430
|
+
const systemPrompt = this.buildSystemPrompt();
|
|
7431
|
+
return [new HumanMessage(`[System Instructions]
|
|
7432
|
+
${systemPrompt}
|
|
7433
|
+
[End Instructions]`)];
|
|
7434
|
+
}
|
|
7435
|
+
/**
|
|
7436
|
+
* Builds messages from conversation history.
|
|
7437
|
+
*/
|
|
7438
|
+
buildHistoryMessages(history) {
|
|
7439
|
+
return history.map(
|
|
7440
|
+
(entry) => entry.role === "user" ? new HumanMessage(entry.content) : new AIMessage(entry.content)
|
|
7441
|
+
);
|
|
7442
|
+
}
|
|
7443
|
+
/**
|
|
7444
|
+
* Adds an entry to conversation history.
|
|
7445
|
+
*/
|
|
7446
|
+
addToHistory(role, content) {
|
|
7447
|
+
this.conversationHistory.push({ role, content });
|
|
7448
|
+
if (this.conversationHistory.length > 20) {
|
|
7449
|
+
this.conversationHistory.splice(0, this.conversationHistory.length - 20);
|
|
7450
|
+
}
|
|
7451
|
+
}
|
|
7452
|
+
};
|
|
7453
|
+
|
|
7454
|
+
// src/infrastructure/agents/base/backlog-state-manager.ts
|
|
7455
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
7456
|
+
import { join as join7 } from "path";
|
|
7457
|
+
var BacklogStateManager = class {
|
|
7458
|
+
constructor(workingDir, logger) {
|
|
7459
|
+
this.workingDir = workingDir;
|
|
7460
|
+
this.logger = logger;
|
|
7461
|
+
}
|
|
7462
|
+
getHistoryPath() {
|
|
7463
|
+
return join7(this.workingDir, "conversation-history.json");
|
|
7464
|
+
}
|
|
7465
|
+
loadHistory() {
|
|
7466
|
+
const historyPath = this.getHistoryPath();
|
|
7467
|
+
if (!existsSync5(historyPath)) {
|
|
7468
|
+
return Promise.resolve([]);
|
|
7469
|
+
}
|
|
7470
|
+
try {
|
|
7471
|
+
const content = readFileSync3(historyPath, "utf-8");
|
|
7472
|
+
const data = JSON.parse(content);
|
|
7473
|
+
this.logger.debug({ messageCount: data.length }, "Loaded conversation history from file");
|
|
7474
|
+
return Promise.resolve(data);
|
|
7475
|
+
} catch (error) {
|
|
7476
|
+
this.logger.error({ error }, "Failed to load conversation history from file");
|
|
7477
|
+
return Promise.resolve([]);
|
|
7478
|
+
}
|
|
7479
|
+
}
|
|
7480
|
+
saveHistory(history) {
|
|
7481
|
+
const historyPath = this.getHistoryPath();
|
|
7482
|
+
try {
|
|
7483
|
+
writeFileSync2(historyPath, JSON.stringify(history, null, 2), "utf-8");
|
|
7484
|
+
this.logger.debug({ messageCount: history.length }, "Saved conversation history to file");
|
|
7485
|
+
} catch (error) {
|
|
7486
|
+
this.logger.error({ error }, "Failed to save conversation history to file");
|
|
7487
|
+
}
|
|
7488
|
+
return Promise.resolve();
|
|
7489
|
+
}
|
|
7490
|
+
clearHistory() {
|
|
7491
|
+
const historyPath = this.getHistoryPath();
|
|
7492
|
+
try {
|
|
7493
|
+
if (existsSync5(historyPath)) {
|
|
7494
|
+
writeFileSync2(historyPath, JSON.stringify([], null, 2), "utf-8");
|
|
7495
|
+
}
|
|
7496
|
+
this.logger.info("Cleared conversation history");
|
|
7497
|
+
} catch (error) {
|
|
7498
|
+
this.logger.error({ error }, "Failed to clear conversation history");
|
|
7499
|
+
}
|
|
7500
|
+
return Promise.resolve();
|
|
7501
|
+
}
|
|
7502
|
+
loadAdditionalState(key) {
|
|
7503
|
+
if (key !== "backlog") {
|
|
7504
|
+
return Promise.resolve(null);
|
|
7505
|
+
}
|
|
7506
|
+
const backlogPath = join7(this.workingDir, "backlog.json");
|
|
7507
|
+
if (!existsSync5(backlogPath)) {
|
|
7508
|
+
return Promise.resolve(null);
|
|
7509
|
+
}
|
|
7510
|
+
try {
|
|
7511
|
+
const content = readFileSync3(backlogPath, "utf-8");
|
|
7512
|
+
const data = JSON.parse(content);
|
|
7513
|
+
this.logger.debug("Loaded backlog state from file");
|
|
7514
|
+
return Promise.resolve(data);
|
|
7515
|
+
} catch (error) {
|
|
7516
|
+
this.logger.error({ error }, "Failed to load backlog state from file");
|
|
7517
|
+
return Promise.resolve(null);
|
|
7518
|
+
}
|
|
7519
|
+
}
|
|
7520
|
+
saveAdditionalState(key, state) {
|
|
7521
|
+
if (key !== "backlog") {
|
|
7522
|
+
return Promise.resolve();
|
|
7523
|
+
}
|
|
7524
|
+
const backlogPath = join7(this.workingDir, "backlog.json");
|
|
7525
|
+
try {
|
|
7526
|
+
writeFileSync2(backlogPath, JSON.stringify(state, null, 2), "utf-8");
|
|
7527
|
+
this.logger.debug("Saved backlog state to file");
|
|
7528
|
+
} catch (error) {
|
|
7529
|
+
this.logger.error({ error }, "Failed to save backlog state to file");
|
|
7530
|
+
}
|
|
7531
|
+
return Promise.resolve();
|
|
7532
|
+
}
|
|
7533
|
+
close() {
|
|
7534
|
+
return Promise.resolve();
|
|
7535
|
+
}
|
|
7536
|
+
};
|
|
7537
|
+
|
|
7538
|
+
// src/infrastructure/agents/backlog-agent-tools.ts
|
|
7539
|
+
import { tool } from "@langchain/core/tools";
|
|
7540
|
+
import { z as z4 } from "zod";
|
|
7541
|
+
function createBacklogAgentTools(context) {
|
|
7542
|
+
const getStatus = tool(
|
|
7543
|
+
() => {
|
|
7544
|
+
const blocks = context.getStatus();
|
|
7545
|
+
return blocks.map((b) => b.content).join("\n");
|
|
7546
|
+
},
|
|
7547
|
+
{
|
|
7548
|
+
name: "get_backlog_status",
|
|
7549
|
+
description: "Get the current status of the backlog including task counts and progress",
|
|
7550
|
+
schema: z4.object({})
|
|
7551
|
+
}
|
|
7552
|
+
);
|
|
7553
|
+
const startHarness = tool(
|
|
7554
|
+
() => {
|
|
7555
|
+
const blocks = context.startHarness();
|
|
7556
|
+
return blocks.map((b) => b.content).join("\n");
|
|
7557
|
+
},
|
|
7558
|
+
{
|
|
7559
|
+
name: "start_backlog_harness",
|
|
7560
|
+
description: "Start the backlog harness to begin executing tasks",
|
|
7561
|
+
schema: z4.object({})
|
|
7562
|
+
}
|
|
7563
|
+
);
|
|
7564
|
+
const stopHarness = tool(
|
|
7565
|
+
() => {
|
|
7566
|
+
const blocks = context.stopHarness();
|
|
7567
|
+
return blocks.map((b) => b.content).join("\n");
|
|
7568
|
+
},
|
|
7569
|
+
{
|
|
7570
|
+
name: "stop_backlog_harness",
|
|
7571
|
+
description: "Stop the backlog harness execution",
|
|
7572
|
+
schema: z4.object({})
|
|
7573
|
+
}
|
|
7574
|
+
);
|
|
7575
|
+
const pauseHarness = tool(
|
|
7576
|
+
() => {
|
|
7577
|
+
const blocks = context.pauseHarness();
|
|
7578
|
+
return blocks.map((b) => b.content).join("\n");
|
|
7579
|
+
},
|
|
7580
|
+
{
|
|
7581
|
+
name: "pause_backlog_harness",
|
|
7582
|
+
description: "Pause the backlog harness (current task will complete)",
|
|
7583
|
+
schema: z4.object({})
|
|
7584
|
+
}
|
|
7585
|
+
);
|
|
7586
|
+
const resumeHarness = tool(
|
|
7587
|
+
() => {
|
|
7588
|
+
const blocks = context.resumeHarness();
|
|
7589
|
+
return blocks.map((b) => b.content).join("\n");
|
|
7590
|
+
},
|
|
7591
|
+
{
|
|
7592
|
+
name: "resume_backlog_harness",
|
|
7593
|
+
description: "Resume a paused backlog harness",
|
|
7594
|
+
schema: z4.object({})
|
|
7595
|
+
}
|
|
7596
|
+
);
|
|
7597
|
+
const listTasks = tool(
|
|
7598
|
+
() => {
|
|
7599
|
+
const blocks = context.listTasks();
|
|
7600
|
+
return blocks.map((b) => b.content).join("\n");
|
|
7601
|
+
},
|
|
7602
|
+
{
|
|
7603
|
+
name: "list_backlog_tasks",
|
|
7604
|
+
description: "List all tasks in the backlog with their status",
|
|
7605
|
+
schema: z4.object({})
|
|
7606
|
+
}
|
|
7607
|
+
);
|
|
7608
|
+
const addTask = tool(
|
|
7609
|
+
({ title, description }) => {
|
|
7610
|
+
const blocks = context.addTask(title, description);
|
|
7611
|
+
return blocks.map((b) => b.content).join("\n");
|
|
7612
|
+
},
|
|
7613
|
+
{
|
|
7614
|
+
name: "add_backlog_task",
|
|
7615
|
+
description: "Add a new task to the backlog",
|
|
7616
|
+
schema: z4.object({
|
|
7617
|
+
title: z4.string().describe("Task title"),
|
|
7618
|
+
description: z4.string().optional().describe("Task description")
|
|
7619
|
+
})
|
|
7620
|
+
}
|
|
7621
|
+
);
|
|
7622
|
+
const getAvailableAgents2 = tool(
|
|
7623
|
+
async () => {
|
|
7624
|
+
const agents = await context.getAvailableAgents();
|
|
7625
|
+
const agentsList = agents.map((a) => `- ${a.name}${a.isAlias ? " (alias)" : ""}: ${a.description}`).join("\n");
|
|
7626
|
+
return `Available agents:
|
|
7627
|
+
${agentsList}`;
|
|
7628
|
+
},
|
|
7629
|
+
{
|
|
7630
|
+
name: "get_available_agents",
|
|
7631
|
+
description: "Get list of all available coding agents (base agents and custom aliases)",
|
|
7632
|
+
schema: z4.object({})
|
|
7633
|
+
}
|
|
7634
|
+
);
|
|
7635
|
+
const parseAgentSelection = tool(
|
|
7636
|
+
async ({ userResponse }) => {
|
|
7637
|
+
const result = await context.parseAgentSelection(userResponse);
|
|
7638
|
+
return `Agent selection result: ${result.valid ? `Selected agent: ${result.agentName}` : result.message}`;
|
|
7639
|
+
},
|
|
7640
|
+
{
|
|
7641
|
+
name: "parse_agent_selection",
|
|
7642
|
+
description: "Parse user response to extract selected agent name and validate it against available agents",
|
|
7643
|
+
schema: z4.object({
|
|
7644
|
+
userResponse: z4.string().describe("The user response containing agent selection")
|
|
7645
|
+
})
|
|
7646
|
+
}
|
|
7647
|
+
);
|
|
7648
|
+
return [getStatus, startHarness, stopHarness, pauseHarness, resumeHarness, listTasks, addTask, getAvailableAgents2, parseAgentSelection];
|
|
7649
|
+
}
|
|
7650
|
+
|
|
7651
|
+
// src/infrastructure/agents/backlog-agent.ts
|
|
7652
|
+
var BACKLOG_AGENT_SYSTEM_PROMPT = `You are a Backlog Agent responsible for managing development tasks in a backlog.
|
|
7653
|
+
|
|
7654
|
+
You have the following capabilities:
|
|
7655
|
+
- View backlog status and task progress
|
|
7656
|
+
- Start/stop/pause/resume harness execution
|
|
7657
|
+
- List tasks in the backlog
|
|
7658
|
+
- Add new tasks
|
|
7659
|
+
- Show available coding agents
|
|
7660
|
+
- Parse agent selection from user responses
|
|
7661
|
+
|
|
7662
|
+
When the user sends a message, interpret their intent and use the appropriate tool(s) to fulfill their request.
|
|
7663
|
+
|
|
7664
|
+
Guidelines:
|
|
7665
|
+
- If the user asks about progress, status, or the current state \u2192 use get_backlog_status
|
|
7666
|
+
- If the user wants to start execution \u2192 use start_backlog_harness
|
|
7667
|
+
- If the user wants to stop, pause, or resume \u2192 use the appropriate harness tool
|
|
7668
|
+
- If the user wants to see tasks \u2192 use list_backlog_tasks
|
|
7669
|
+
- If the user describes something to do or wants to add a task \u2192 use add_backlog_task
|
|
7670
|
+
|
|
7671
|
+
SPECIAL CASE - Agent Selection for Harness Execution:
|
|
7672
|
+
When start_backlog_harness tool returns a message asking which agent to use:
|
|
7673
|
+
1. Acknowledge that we're waiting for agent selection
|
|
7674
|
+
2. DO NOT call any other tools until the user provides an agent selection
|
|
7675
|
+
3. When the user responds with an agent name (e.g., "I want to use claude" or "use cursor"):
|
|
7676
|
+
- Call parse_agent_selection with their response to validate and extract the agent name
|
|
7677
|
+
- Report the results to the user
|
|
7678
|
+
- The agent will then proceed with harness creation
|
|
7679
|
+
|
|
7680
|
+
You can call multiple tools in sequence if needed to fully satisfy the user's request (e.g., get status AND list tasks). After all tools have returned, provide a concise summary of what was accomplished.
|
|
7681
|
+
|
|
7682
|
+
Be helpful and informative. Always confirm actions and provide status updates.`;
|
|
7683
|
+
var BacklogAgent = class extends LangGraphAgent {
|
|
7684
|
+
toolsContext;
|
|
7685
|
+
workingDir;
|
|
7686
|
+
constructor(toolsContext, workingDir, logger) {
|
|
7687
|
+
super(logger);
|
|
7688
|
+
this.toolsContext = toolsContext;
|
|
7689
|
+
this.workingDir = workingDir;
|
|
7690
|
+
this.initializeAgent();
|
|
7691
|
+
}
|
|
7692
|
+
/**
|
|
7693
|
+
* Implements abstract method: build system prompt for Backlog Agent.
|
|
7694
|
+
*/
|
|
7695
|
+
buildSystemPrompt() {
|
|
7696
|
+
return BACKLOG_AGENT_SYSTEM_PROMPT;
|
|
7697
|
+
}
|
|
7698
|
+
/**
|
|
7699
|
+
* Implements abstract method: create tools for Backlog Agent.
|
|
7700
|
+
*/
|
|
7701
|
+
createTools() {
|
|
7702
|
+
return createBacklogAgentTools(this.toolsContext);
|
|
7703
|
+
}
|
|
7704
|
+
/**
|
|
7705
|
+
* Implements abstract method: create state manager for Backlog Agent.
|
|
7706
|
+
* Uses file-backed persistence for backlog state and conversation history.
|
|
7707
|
+
*/
|
|
7708
|
+
createStateManager() {
|
|
7709
|
+
return new BacklogStateManager(this.workingDir, this.logger);
|
|
7710
|
+
}
|
|
7711
|
+
};
|
|
7712
|
+
|
|
7713
|
+
// src/infrastructure/agents/backlog-agent-manager.ts
|
|
7714
|
+
var BacklogAgentManager = class _BacklogAgentManager extends EventEmitter5 {
|
|
7715
|
+
session;
|
|
7716
|
+
backlog;
|
|
7717
|
+
harness = null;
|
|
7718
|
+
conversationHistory = [];
|
|
7719
|
+
workingDir;
|
|
7720
|
+
agentSessionManager;
|
|
7721
|
+
logger;
|
|
7722
|
+
llmAgent;
|
|
7723
|
+
selectedAgent = null;
|
|
7724
|
+
agentSelectionInProgress = false;
|
|
7725
|
+
constructor(session, backlog, workingDir, agentSessionManager, logger) {
|
|
7726
|
+
super();
|
|
7727
|
+
this.session = session;
|
|
7728
|
+
this.backlog = backlog;
|
|
7729
|
+
this.workingDir = workingDir;
|
|
7730
|
+
this.agentSessionManager = agentSessionManager;
|
|
7731
|
+
this.logger = logger;
|
|
7732
|
+
this.llmAgent = new BacklogAgent(
|
|
7733
|
+
{
|
|
7734
|
+
getStatus: () => this.getStatusBlocks(),
|
|
7735
|
+
startHarness: () => this.startHarnessCommand(),
|
|
7736
|
+
stopHarness: () => this.stopHarnessCommand(),
|
|
7737
|
+
pauseHarness: () => this.pauseHarnessCommand(),
|
|
7738
|
+
resumeHarness: () => this.resumeHarnessCommand(),
|
|
7739
|
+
listTasks: () => this.listTasksBlocks(),
|
|
7740
|
+
addTask: (title, description) => this.addTaskCommand({ title, description }),
|
|
7741
|
+
getAvailableAgents: () => this.getAvailableAgentsData(),
|
|
7742
|
+
parseAgentSelection: (userResponse) => this.parseAgentSelectionData(userResponse)
|
|
7743
|
+
},
|
|
7744
|
+
workingDir,
|
|
7745
|
+
logger
|
|
7746
|
+
);
|
|
7747
|
+
}
|
|
7748
|
+
/**
|
|
7749
|
+
* Get session info.
|
|
7750
|
+
*/
|
|
7751
|
+
getSession() {
|
|
7752
|
+
return this.session;
|
|
7753
|
+
}
|
|
7754
|
+
/**
|
|
7755
|
+
* Get current backlog.
|
|
7756
|
+
*/
|
|
7757
|
+
getBacklog() {
|
|
7758
|
+
return this.backlog;
|
|
7759
|
+
}
|
|
7760
|
+
/**
|
|
7761
|
+
* Update backlog from file.
|
|
7762
|
+
*/
|
|
7763
|
+
updateBacklogFromFile() {
|
|
7764
|
+
const backlogPath = join8(this.workingDir, "backlog.json");
|
|
7765
|
+
if (existsSync6(backlogPath)) {
|
|
7766
|
+
try {
|
|
7767
|
+
const content = readFileSync4(backlogPath, "utf-8");
|
|
7768
|
+
this.backlog = BacklogSchema.parse(JSON.parse(content));
|
|
7769
|
+
this.logger.debug("Updated backlog from file");
|
|
7770
|
+
} catch (error) {
|
|
7771
|
+
this.logger.error({ error }, "Failed to load backlog from file");
|
|
7772
|
+
}
|
|
7773
|
+
}
|
|
7774
|
+
}
|
|
7775
|
+
/**
|
|
7776
|
+
* Process user command using streaming LLM execution.
|
|
7777
|
+
*
|
|
7778
|
+
* This method now uses the unified streaming mode from LangGraphAgent
|
|
7779
|
+
* which emits blocks in real-time to all connected clients.
|
|
7780
|
+
*
|
|
7781
|
+
* For MVP, the command processor:
|
|
7782
|
+
* - Parses user intent (start_harness, add_task, get_status, etc.)
|
|
7783
|
+
* - Returns appropriate response through LLM streaming
|
|
7784
|
+
* - Handles agent selection if in progress
|
|
7785
|
+
*/
|
|
7786
|
+
async executeCommand(userMessage) {
|
|
7787
|
+
this.conversationHistory.push({ role: "user", content: userMessage });
|
|
7788
|
+
if (this.agentSelectionInProgress && !this.selectedAgent) {
|
|
7789
|
+
const selectionBlocks = this.handleAgentSelection(userMessage);
|
|
7790
|
+
const responseText = selectionBlocks.map((b) => b.content).join("\n");
|
|
7791
|
+
this.conversationHistory.push({ role: "assistant", content: responseText });
|
|
7792
|
+
this.saveBacklog();
|
|
7793
|
+
return selectionBlocks;
|
|
7794
|
+
}
|
|
7795
|
+
const streamedBlocks = [];
|
|
7796
|
+
const blockHandler = (_deviceId, blocks, isComplete, finalOutput) => {
|
|
7797
|
+
streamedBlocks.push(...blocks);
|
|
7798
|
+
if (isComplete && finalOutput) {
|
|
7799
|
+
this.conversationHistory.push({ role: "assistant", content: finalOutput });
|
|
7800
|
+
this.saveBacklog();
|
|
7801
|
+
}
|
|
7802
|
+
};
|
|
7803
|
+
this.llmAgent.on("blocks", blockHandler);
|
|
7804
|
+
try {
|
|
7805
|
+
await this.llmAgent.executeWithStream(userMessage, "backlog-manager");
|
|
7806
|
+
} finally {
|
|
7807
|
+
this.llmAgent.removeListener("blocks", blockHandler);
|
|
7808
|
+
}
|
|
7809
|
+
return streamedBlocks;
|
|
7810
|
+
}
|
|
7811
|
+
/**
|
|
7812
|
+
* Get status blocks for current backlog.
|
|
7813
|
+
*/
|
|
7814
|
+
getStatusBlocks() {
|
|
7815
|
+
const summary = this.backlog.summary ?? {
|
|
7816
|
+
total: this.backlog.tasks.length,
|
|
7817
|
+
completed: 0,
|
|
7818
|
+
failed: 0,
|
|
7819
|
+
in_progress: 0,
|
|
7820
|
+
pending: 0
|
|
7821
|
+
};
|
|
7822
|
+
const percentage = summary.total > 0 ? Math.round(summary.completed / summary.total * 100) : 0;
|
|
7823
|
+
const worktreeDisplay = this.backlog.worktree ?? "main";
|
|
7824
|
+
const statusText = `
|
|
7825
|
+
\u{1F4CA} **Backlog Status**: ${this.backlog.id}
|
|
7826
|
+
|
|
7827
|
+
**Progress**: ${summary.completed}/${summary.total} tasks (${percentage}%)
|
|
7828
|
+
|
|
7829
|
+
**Breakdown**:
|
|
7830
|
+
- \u2705 Completed: ${summary.completed}
|
|
7831
|
+
- \u23F3 In Progress: ${summary.in_progress}
|
|
7832
|
+
- \u23F8\uFE0F Pending: ${summary.pending}
|
|
7833
|
+
- \u274C Failed: ${summary.failed}
|
|
7834
|
+
|
|
7835
|
+
**Agent**: ${this.backlog.agent}
|
|
7836
|
+
**Worktree**: ${worktreeDisplay}
|
|
7837
|
+
|
|
7838
|
+
**Harness Status**: ${this.harness ? this.session.harnessRunning ? "\u{1F7E2} Running" : "\u{1F534} Stopped" : "\u274C Not started"}
|
|
7839
|
+
`.trim();
|
|
7840
|
+
return [
|
|
7841
|
+
{
|
|
7842
|
+
id: "status",
|
|
7843
|
+
block_type: "text",
|
|
7844
|
+
content: statusText
|
|
7845
|
+
}
|
|
7846
|
+
];
|
|
7847
|
+
}
|
|
7848
|
+
startHarnessCommand() {
|
|
7849
|
+
if (this.harness) {
|
|
7850
|
+
return [
|
|
7851
|
+
{
|
|
7852
|
+
id: "harness-already-running",
|
|
7853
|
+
block_type: "text",
|
|
7854
|
+
content: '\u26A0\uFE0F Harness is already running! Use "stop" to stop it first.'
|
|
7855
|
+
}
|
|
7856
|
+
];
|
|
7857
|
+
}
|
|
7858
|
+
const pendingCount = this.backlog.tasks.filter((t) => t.status === "pending").length;
|
|
7859
|
+
const inProgressCount = this.backlog.tasks.filter((t) => t.status === "in_progress").length;
|
|
7860
|
+
if (pendingCount === 0 && inProgressCount === 0) {
|
|
7861
|
+
return [
|
|
7862
|
+
{
|
|
7863
|
+
id: "no-tasks-to-run",
|
|
7864
|
+
block_type: "text",
|
|
7865
|
+
content: '\u274C No pending tasks to execute. Add tasks first using "add task" command.'
|
|
7866
|
+
}
|
|
7867
|
+
];
|
|
7868
|
+
}
|
|
7869
|
+
if (!this.selectedAgent && !this.agentSelectionInProgress) {
|
|
7870
|
+
return this.askForAgentSelection();
|
|
7871
|
+
}
|
|
7872
|
+
if (this.agentSelectionInProgress) {
|
|
7873
|
+
return [
|
|
7874
|
+
{
|
|
7875
|
+
id: "agent-selection-pending",
|
|
7876
|
+
block_type: "status",
|
|
7877
|
+
content: "\u23F3 Waiting for you to select an agent..."
|
|
7878
|
+
}
|
|
7879
|
+
];
|
|
7880
|
+
}
|
|
7881
|
+
return this.createAndStartHarness();
|
|
7882
|
+
}
|
|
7883
|
+
/**
|
|
7884
|
+
* Ask user which agent to use for harness execution.
|
|
7885
|
+
*/
|
|
7886
|
+
askForAgentSelection() {
|
|
7887
|
+
this.agentSelectionInProgress = true;
|
|
7888
|
+
const agents = Array.from(getAvailableAgents().values());
|
|
7889
|
+
const agentList = agents.map((a) => `\u2022 **${a.name}**${a.isAlias ? " (alias)" : ""}: ${a.description}`).join("\n");
|
|
7890
|
+
const question = `
|
|
7891
|
+
\u{1F916} **Select a coding agent** to execute the harness tasks:
|
|
7892
|
+
|
|
7893
|
+
${agentList}
|
|
7894
|
+
|
|
7895
|
+
Please respond with the agent name you'd like to use (e.g., "claude", "cursor", or your alias name).
|
|
7896
|
+
`.trim();
|
|
7897
|
+
return [
|
|
7898
|
+
{
|
|
7899
|
+
id: "agent-selection-question",
|
|
7900
|
+
block_type: "text",
|
|
7901
|
+
content: question
|
|
7902
|
+
}
|
|
7903
|
+
];
|
|
7904
|
+
}
|
|
7905
|
+
/**
|
|
7906
|
+
* Handle user's agent selection response.
|
|
7907
|
+
*/
|
|
7908
|
+
handleAgentSelection(userMessage) {
|
|
7909
|
+
const availableAgents = getAvailableAgents();
|
|
7910
|
+
const agentNames = Array.from(availableAgents.keys());
|
|
7911
|
+
const selectedAgent = this.findBestAgentMatch(userMessage, agentNames);
|
|
7912
|
+
if (!selectedAgent) {
|
|
7913
|
+
const agentList = agentNames.map((name) => {
|
|
7914
|
+
const config2 = availableAgents.get(name);
|
|
7915
|
+
return `\u2022 **${name}**${config2?.isAlias ? " (alias)" : ""}: ${config2?.description ?? ""}`;
|
|
7916
|
+
}).join("\n");
|
|
7917
|
+
return [
|
|
7918
|
+
{
|
|
7919
|
+
id: "agent-selection-invalid",
|
|
7920
|
+
block_type: "text",
|
|
7921
|
+
content: `\u274C I didn't recognize that agent name. Here are the available options:
|
|
7922
|
+
|
|
7923
|
+
${agentList}
|
|
7924
|
+
|
|
7925
|
+
Please try again.`
|
|
7926
|
+
}
|
|
7927
|
+
];
|
|
7928
|
+
}
|
|
7929
|
+
this.selectedAgent = selectedAgent;
|
|
7930
|
+
this.agentSelectionInProgress = false;
|
|
7931
|
+
const selectedConfig = availableAgents.get(selectedAgent);
|
|
7932
|
+
const confirmation = `\u2705 Great! I'll use **${selectedAgent}**${selectedConfig?.isAlias ? " (alias)" : ""} to execute the tasks.
|
|
7933
|
+
|
|
7934
|
+
Now starting the harness...`;
|
|
7935
|
+
const confirmationBlocks = [
|
|
7936
|
+
{
|
|
7937
|
+
id: "agent-selection-confirmed",
|
|
7938
|
+
block_type: "status",
|
|
7939
|
+
content: confirmation
|
|
7940
|
+
}
|
|
7941
|
+
];
|
|
7942
|
+
const harnessBlocks = this.createAndStartHarness();
|
|
7943
|
+
return [...confirmationBlocks, ...harnessBlocks];
|
|
7944
|
+
}
|
|
7945
|
+
/**
|
|
7946
|
+
* Find best agent match from user response using improved fuzzy matching.
|
|
7947
|
+
* Handles variations like "claude code", "use cursor", "my zai alias", etc.
|
|
7948
|
+
*/
|
|
7949
|
+
findBestAgentMatch(userMessage, agentNames) {
|
|
7950
|
+
const lowerMessage = userMessage.toLowerCase();
|
|
7951
|
+
for (const agentName of agentNames) {
|
|
7952
|
+
if (lowerMessage === agentName.toLowerCase()) {
|
|
7953
|
+
return agentName;
|
|
7954
|
+
}
|
|
7955
|
+
}
|
|
7956
|
+
for (const agentName of agentNames) {
|
|
7957
|
+
const lowerAgent = agentName.toLowerCase();
|
|
7958
|
+
if (lowerMessage.includes(lowerAgent)) {
|
|
7959
|
+
return agentName;
|
|
7960
|
+
}
|
|
7961
|
+
}
|
|
7962
|
+
const commonPatterns = {
|
|
7963
|
+
claude: ["claude", "claude code", "claudecode", "claude-code", "claude agent"],
|
|
7964
|
+
cursor: ["cursor", "cursor agent", "cursoragent", "cursor-agent"],
|
|
7965
|
+
opencode: ["opencode", "open code", "opencode agent", "open-code"]
|
|
7966
|
+
};
|
|
7967
|
+
for (const [baseName, patterns] of Object.entries(commonPatterns)) {
|
|
7968
|
+
const agentName = agentNames.find((n) => n.toLowerCase() === baseName);
|
|
7969
|
+
if (agentName) {
|
|
7970
|
+
for (const pattern of patterns) {
|
|
7971
|
+
if (lowerMessage.includes(pattern.toLowerCase())) {
|
|
7972
|
+
return agentName;
|
|
7973
|
+
}
|
|
7974
|
+
}
|
|
7975
|
+
}
|
|
7976
|
+
}
|
|
7977
|
+
let bestMatch = null;
|
|
7978
|
+
const threshold = 0.6;
|
|
7979
|
+
for (const agentName of agentNames) {
|
|
7980
|
+
const similarity = this.calculateStringSimilarity(lowerMessage, agentName.toLowerCase());
|
|
7981
|
+
if (similarity > threshold && (!bestMatch || similarity > bestMatch.score)) {
|
|
7982
|
+
bestMatch = { name: agentName, score: similarity };
|
|
7983
|
+
}
|
|
7984
|
+
}
|
|
7985
|
+
return bestMatch ? bestMatch.name : null;
|
|
7986
|
+
}
|
|
7987
|
+
/**
|
|
7988
|
+
* Calculate string similarity score (0-1) using Levenshtein distance.
|
|
7989
|
+
*/
|
|
7990
|
+
calculateStringSimilarity(str1, str2) {
|
|
7991
|
+
const maxLen = Math.max(str1.length, str2.length);
|
|
7992
|
+
if (maxLen === 0) return 1;
|
|
7993
|
+
const distance = this.levenshteinDistance(str1, str2);
|
|
7994
|
+
return 1 - distance / maxLen;
|
|
7995
|
+
}
|
|
7996
|
+
/**
|
|
7997
|
+
* Calculate Levenshtein distance between two strings.
|
|
7998
|
+
*/
|
|
7999
|
+
levenshteinDistance(str1, str2) {
|
|
8000
|
+
const len1 = str1.length;
|
|
8001
|
+
const len2 = str2.length;
|
|
8002
|
+
const matrix = Array.from(
|
|
8003
|
+
{ length: len1 + 1 },
|
|
8004
|
+
() => Array.from({ length: len2 + 1 }, () => 0)
|
|
8005
|
+
);
|
|
8006
|
+
for (let i = 0; i <= len1; i++) {
|
|
8007
|
+
const row = matrix[i];
|
|
8008
|
+
if (row) row[0] = i;
|
|
8009
|
+
}
|
|
8010
|
+
for (let j = 0; j <= len2; j++) {
|
|
8011
|
+
const firstRow = matrix[0];
|
|
8012
|
+
if (firstRow) firstRow[j] = j;
|
|
8013
|
+
}
|
|
8014
|
+
for (let i = 1; i <= len1; i++) {
|
|
8015
|
+
for (let j = 1; j <= len2; j++) {
|
|
8016
|
+
const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
|
|
8017
|
+
const row = matrix[i];
|
|
8018
|
+
const prevRow = matrix[i - 1];
|
|
8019
|
+
if (row && prevRow) {
|
|
8020
|
+
row[j] = Math.min(
|
|
8021
|
+
(prevRow[j] ?? 0) + 1,
|
|
8022
|
+
(row[j - 1] ?? 0) + 1,
|
|
8023
|
+
(prevRow[j - 1] ?? 0) + cost
|
|
8024
|
+
);
|
|
8025
|
+
}
|
|
8026
|
+
}
|
|
8027
|
+
}
|
|
8028
|
+
return matrix[len1]?.[len2] ?? 0;
|
|
8029
|
+
}
|
|
8030
|
+
/**
|
|
8031
|
+
* Create and start the harness with selected agent.
|
|
8032
|
+
*/
|
|
8033
|
+
createAndStartHarness() {
|
|
8034
|
+
if (!this.selectedAgent) {
|
|
8035
|
+
return [
|
|
8036
|
+
{
|
|
8037
|
+
id: "no-agent-selected",
|
|
8038
|
+
block_type: "error",
|
|
8039
|
+
content: "\u274C No agent selected. Please select an agent first."
|
|
8040
|
+
}
|
|
8041
|
+
];
|
|
8042
|
+
}
|
|
8043
|
+
this.harness = new BacklogHarness(
|
|
8044
|
+
this.backlog,
|
|
8045
|
+
this.workingDir,
|
|
8046
|
+
this.selectedAgent,
|
|
8047
|
+
this.agentSessionManager,
|
|
8048
|
+
this.logger
|
|
8049
|
+
);
|
|
8050
|
+
this.harness.on("output", (blocks) => {
|
|
8051
|
+
this.emit("output", blocks);
|
|
8052
|
+
});
|
|
8053
|
+
this.harness.on("harness-completed", () => {
|
|
8054
|
+
this.selectedAgent = null;
|
|
8055
|
+
this.agentSelectionInProgress = false;
|
|
8056
|
+
});
|
|
8057
|
+
this.session.setHarnessRunning(true);
|
|
8058
|
+
void this.harness.start().catch((error) => {
|
|
8059
|
+
this.logger.error({ error }, "Harness error");
|
|
8060
|
+
this.session.setHarnessRunning(false);
|
|
8061
|
+
const errorMsg = error instanceof Error ? error.message : "Unknown error";
|
|
8062
|
+
this.emit("output", [
|
|
8063
|
+
{
|
|
8064
|
+
id: "harness-error",
|
|
8065
|
+
block_type: "error",
|
|
8066
|
+
content: `\u{1F534} Harness error: ${errorMsg}`
|
|
8067
|
+
}
|
|
8068
|
+
]);
|
|
8069
|
+
});
|
|
8070
|
+
const worktreeDisplay = this.backlog.worktree ?? "main";
|
|
8071
|
+
const pendingCount = this.backlog.tasks.filter((t) => t.status === "pending").length;
|
|
8072
|
+
const inProgressCount = this.backlog.tasks.filter((t) => t.status === "in_progress").length;
|
|
8073
|
+
const tasksInfo = inProgressCount > 0 ? `${inProgressCount} task(s) in progress, ${pendingCount} pending` : `${pendingCount} task(s) to execute`;
|
|
8074
|
+
return [
|
|
8075
|
+
{
|
|
8076
|
+
id: "harness-started",
|
|
8077
|
+
block_type: "status",
|
|
8078
|
+
content: `\u{1F680} Harness started for ${worktreeDisplay}. ${tasksInfo}.`
|
|
8079
|
+
},
|
|
8080
|
+
{
|
|
8081
|
+
id: "harness-notice",
|
|
8082
|
+
block_type: "text",
|
|
8083
|
+
content: 'The harness will execute tasks sequentially. Use "status" to check progress, "pause" to pause, or "stop" to stop.'
|
|
8084
|
+
}
|
|
8085
|
+
];
|
|
8086
|
+
}
|
|
8087
|
+
/**
|
|
8088
|
+
* Stop harness execution and reset agent selection.
|
|
8089
|
+
*/
|
|
8090
|
+
stopHarnessCommand() {
|
|
8091
|
+
if (!this.harness || !this.session.harnessRunning) {
|
|
8092
|
+
return [
|
|
8093
|
+
{
|
|
8094
|
+
id: "harness-not-running",
|
|
8095
|
+
block_type: "text",
|
|
8096
|
+
content: "\u26A0\uFE0F Harness is not running."
|
|
8097
|
+
}
|
|
8098
|
+
];
|
|
8099
|
+
}
|
|
8100
|
+
this.harness.stop();
|
|
8101
|
+
this.session.setHarnessRunning(false);
|
|
8102
|
+
this.selectedAgent = null;
|
|
8103
|
+
this.agentSelectionInProgress = false;
|
|
8104
|
+
return [
|
|
8105
|
+
{
|
|
8106
|
+
id: "harness-stopped",
|
|
8107
|
+
block_type: "status",
|
|
8108
|
+
content: "\u23F9\uFE0F Harness stopped. Agent selection reset."
|
|
8109
|
+
}
|
|
8110
|
+
];
|
|
8111
|
+
}
|
|
8112
|
+
pauseHarnessCommand() {
|
|
8113
|
+
if (!this.harness || !this.session.harnessRunning) {
|
|
8114
|
+
return [
|
|
8115
|
+
{
|
|
8116
|
+
id: "harness-not-running",
|
|
8117
|
+
block_type: "text",
|
|
8118
|
+
content: "\u26A0\uFE0F Harness is not running."
|
|
8119
|
+
}
|
|
8120
|
+
];
|
|
8121
|
+
}
|
|
8122
|
+
this.harness.pause();
|
|
8123
|
+
return [
|
|
8124
|
+
{
|
|
8125
|
+
id: "harness-paused",
|
|
8126
|
+
block_type: "status",
|
|
8127
|
+
content: "\u23F8\uFE0F Harness paused (current task will complete)."
|
|
8128
|
+
}
|
|
8129
|
+
];
|
|
8130
|
+
}
|
|
8131
|
+
resumeHarnessCommand() {
|
|
8132
|
+
if (!this.harness) {
|
|
8133
|
+
return [
|
|
8134
|
+
{
|
|
8135
|
+
id: "harness-not-running",
|
|
8136
|
+
block_type: "text",
|
|
8137
|
+
content: "\u26A0\uFE0F Harness is not running."
|
|
8138
|
+
}
|
|
8139
|
+
];
|
|
8140
|
+
}
|
|
8141
|
+
this.harness.resume();
|
|
8142
|
+
return [
|
|
8143
|
+
{
|
|
8144
|
+
id: "harness-resumed",
|
|
8145
|
+
block_type: "status",
|
|
8146
|
+
content: "\u25B6\uFE0F Harness resumed."
|
|
8147
|
+
}
|
|
8148
|
+
];
|
|
8149
|
+
}
|
|
8150
|
+
/**
|
|
8151
|
+
* Add a new task to backlog.
|
|
8152
|
+
*/
|
|
8153
|
+
addTaskCommand(params) {
|
|
8154
|
+
const priorityValue = params.priority;
|
|
8155
|
+
const complexityValue = params.complexity;
|
|
8156
|
+
const newTask = {
|
|
8157
|
+
id: Math.max(0, ...this.backlog.tasks.map((t) => t.id)) + 1,
|
|
8158
|
+
title: params.title ?? "Untitled",
|
|
8159
|
+
description: params.description ?? "",
|
|
8160
|
+
acceptance_criteria: params.criteria ? [params.criteria] : [],
|
|
8161
|
+
dependencies: [],
|
|
8162
|
+
priority: priorityValue ?? "medium",
|
|
8163
|
+
complexity: complexityValue ?? "moderate",
|
|
8164
|
+
status: "pending",
|
|
8165
|
+
retry_count: 0
|
|
8166
|
+
};
|
|
8167
|
+
this.backlog.tasks.push(newTask);
|
|
8168
|
+
return [
|
|
8169
|
+
{
|
|
8170
|
+
id: "task-added",
|
|
8171
|
+
block_type: "text",
|
|
8172
|
+
content: `\u2705 Task ${newTask.id} added: "${newTask.title}"`
|
|
8173
|
+
}
|
|
8174
|
+
];
|
|
8175
|
+
}
|
|
8176
|
+
/**
|
|
8177
|
+
* Reorder tasks by priority/dependency.
|
|
8178
|
+
*/
|
|
8179
|
+
listTasksBlocks() {
|
|
8180
|
+
const statusEmoji = {
|
|
8181
|
+
pending: "\u2B1C",
|
|
8182
|
+
in_progress: "\u{1F7E8}",
|
|
8183
|
+
completed: "\u2705",
|
|
8184
|
+
failed: "\u274C",
|
|
8185
|
+
skipped: "\u23ED\uFE0F"
|
|
8186
|
+
};
|
|
8187
|
+
const tasksList = this.backlog.tasks.map((t) => {
|
|
8188
|
+
const emoji = statusEmoji[t.status] ?? "\u2753";
|
|
8189
|
+
const status = t.status.replace("_", " ").toUpperCase();
|
|
8190
|
+
let line = `${emoji} **${t.id}. ${t.title}** [${status}]`;
|
|
8191
|
+
if (t.description) {
|
|
8192
|
+
line += `
|
|
8193
|
+
${t.description.split("\n")[0]}`;
|
|
8194
|
+
}
|
|
8195
|
+
if (t.error) {
|
|
8196
|
+
line += `
|
|
8197
|
+
\u274C Error: ${t.error}`;
|
|
8198
|
+
}
|
|
8199
|
+
return line;
|
|
8200
|
+
}).join("\n\n");
|
|
8201
|
+
const summary = this.backlog.summary ?? {
|
|
8202
|
+
total: this.backlog.tasks.length,
|
|
8203
|
+
completed: 0,
|
|
8204
|
+
failed: 0,
|
|
8205
|
+
in_progress: 0,
|
|
8206
|
+
pending: this.backlog.tasks.length
|
|
8207
|
+
};
|
|
8208
|
+
const content = `
|
|
8209
|
+
\u{1F4CB} **Tasks in ${this.backlog.id}**
|
|
8210
|
+
|
|
8211
|
+
**Progress**: ${summary.completed}/${summary.total} completed
|
|
8212
|
+
|
|
8213
|
+
**Breakdown**:
|
|
8214
|
+
\u2705 Completed: ${summary.completed}
|
|
8215
|
+
\u{1F7E8} In Progress: ${summary.in_progress}
|
|
8216
|
+
\u2B1C Pending: ${summary.pending}
|
|
8217
|
+
\u274C Failed: ${summary.failed}
|
|
8218
|
+
|
|
8219
|
+
**Task List**:
|
|
8220
|
+
${tasksList}
|
|
8221
|
+
`.trim();
|
|
8222
|
+
return [
|
|
8223
|
+
{
|
|
8224
|
+
id: "tasks-list",
|
|
8225
|
+
block_type: "text",
|
|
8226
|
+
content
|
|
8227
|
+
}
|
|
8228
|
+
];
|
|
8229
|
+
}
|
|
8230
|
+
getAvailableAgentsData() {
|
|
8231
|
+
const availableAgents = getAvailableAgents();
|
|
8232
|
+
const agents = Array.from(availableAgents.values()).map((config2) => ({
|
|
8233
|
+
name: config2.name,
|
|
8234
|
+
description: config2.description,
|
|
8235
|
+
isAlias: config2.isAlias
|
|
8236
|
+
}));
|
|
8237
|
+
return Promise.resolve(agents);
|
|
8238
|
+
}
|
|
8239
|
+
parseAgentSelectionData(userResponse) {
|
|
8240
|
+
const availableAgents = getAvailableAgents();
|
|
8241
|
+
const agentNames = Array.from(availableAgents.keys());
|
|
8242
|
+
const selectedAgent = this.findBestAgentMatch(userResponse, agentNames);
|
|
8243
|
+
if (!selectedAgent) {
|
|
8244
|
+
const availableList = agentNames.join(", ");
|
|
8245
|
+
return Promise.resolve({
|
|
8246
|
+
agentName: null,
|
|
8247
|
+
valid: false,
|
|
8248
|
+
message: `Invalid agent. Available agents: ${availableList}`
|
|
8249
|
+
});
|
|
8250
|
+
}
|
|
8251
|
+
return Promise.resolve({
|
|
8252
|
+
agentName: selectedAgent,
|
|
8253
|
+
valid: true,
|
|
8254
|
+
message: `Selected agent: ${selectedAgent}`
|
|
8255
|
+
});
|
|
8256
|
+
}
|
|
8257
|
+
saveBacklog() {
|
|
8258
|
+
const backlogPath = join8(this.workingDir, "backlog.json");
|
|
8259
|
+
try {
|
|
8260
|
+
writeFileSync3(backlogPath, JSON.stringify(this.backlog, null, 2));
|
|
8261
|
+
this.logger.debug({ backlogPath }, "Saved backlog");
|
|
8262
|
+
} catch (error) {
|
|
8263
|
+
this.logger.error({ error }, "Failed to save backlog");
|
|
8264
|
+
}
|
|
8265
|
+
}
|
|
8266
|
+
/**
|
|
8267
|
+
* Create a new BacklogAgentManager for manual input.
|
|
8268
|
+
*/
|
|
8269
|
+
static createEmpty(session, workingDir, agentSessionManager, logger) {
|
|
8270
|
+
const agentType = session.agentName;
|
|
8271
|
+
const backlog = {
|
|
8272
|
+
id: session.backlogId,
|
|
8273
|
+
project: workingDir.split("/").pop() ?? "project",
|
|
8274
|
+
worktree: session.workspacePath?.worktree,
|
|
8275
|
+
agent: agentType,
|
|
8276
|
+
source: { type: "manual" },
|
|
8277
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8278
|
+
tasks: [],
|
|
8279
|
+
summary: { total: 0, completed: 0, failed: 0, in_progress: 0, pending: 0 }
|
|
8280
|
+
};
|
|
8281
|
+
return new _BacklogAgentManager(session, backlog, workingDir, agentSessionManager, logger);
|
|
8282
|
+
}
|
|
8283
|
+
/**
|
|
8284
|
+
* Creates a manager and attempts to load backlog from file.
|
|
8285
|
+
* If backlog.json doesn't exist, starts with empty backlog.
|
|
8286
|
+
* Used during session restoration to load persisted backlog state.
|
|
8287
|
+
*/
|
|
8288
|
+
static createAndLoadFromFile(session, workingDir, agentSessionManager, logger) {
|
|
8289
|
+
const manager = _BacklogAgentManager.createEmpty(session, workingDir, agentSessionManager, logger);
|
|
8290
|
+
manager.updateBacklogFromFile();
|
|
8291
|
+
return manager;
|
|
8292
|
+
}
|
|
8293
|
+
};
|
|
8294
|
+
|
|
8295
|
+
// src/infrastructure/persistence/in-memory-session-manager.ts
|
|
8296
|
+
var InMemorySessionManager = class extends EventEmitter6 {
|
|
8297
|
+
sessions = /* @__PURE__ */ new Map();
|
|
8298
|
+
ptyManager;
|
|
8299
|
+
agentSessionManager;
|
|
8300
|
+
logger;
|
|
8301
|
+
supervisorSession = null;
|
|
8302
|
+
backlogManagers;
|
|
8303
|
+
sessionRepository;
|
|
8304
|
+
constructor(config2) {
|
|
8305
|
+
super();
|
|
8306
|
+
this.ptyManager = config2.ptyManager;
|
|
8307
|
+
this.agentSessionManager = config2.agentSessionManager;
|
|
8308
|
+
this.logger = config2.logger.child({ component: "session-manager" });
|
|
8309
|
+
this.backlogManagers = config2.backlogManagers ?? /* @__PURE__ */ new Map();
|
|
8310
|
+
this.sessionRepository = config2.sessionRepository;
|
|
8311
|
+
this.setupAgentSessionSync();
|
|
8312
|
+
}
|
|
8313
|
+
/**
|
|
8314
|
+
* Restores persisted sessions from the database on startup.
|
|
8315
|
+
*/
|
|
8316
|
+
restoreSessions() {
|
|
8317
|
+
this.logger.info("restoreSessions() called");
|
|
8318
|
+
if (!this.sessionRepository) {
|
|
8319
|
+
this.logger.warn("No session repository - skipping session restoration");
|
|
8320
|
+
return Promise.resolve();
|
|
8321
|
+
}
|
|
8322
|
+
let persistedSessions = [];
|
|
8323
|
+
try {
|
|
8324
|
+
persistedSessions = this.sessionRepository.getActive();
|
|
8325
|
+
this.logger.info(
|
|
8326
|
+
{ count: persistedSessions.length, sessions: persistedSessions.map((s) => ({ id: s.id, type: s.type })) },
|
|
8327
|
+
"Restoring persisted sessions from database"
|
|
8328
|
+
);
|
|
8329
|
+
if (persistedSessions.length === 0) {
|
|
8330
|
+
this.logger.info("No persisted sessions found in database");
|
|
8331
|
+
return Promise.resolve();
|
|
8332
|
+
}
|
|
8333
|
+
} catch (dbError) {
|
|
8334
|
+
this.logger.error(
|
|
8335
|
+
{ error: dbError instanceof Error ? dbError.message : String(dbError) },
|
|
8336
|
+
"Error fetching sessions from database"
|
|
8337
|
+
);
|
|
8338
|
+
return Promise.resolve();
|
|
8339
|
+
}
|
|
8340
|
+
let backlogRestoreCount = 0;
|
|
8341
|
+
for (const persistedSession of persistedSessions) {
|
|
8342
|
+
try {
|
|
8343
|
+
if (persistedSession.type === "backlog-agent") {
|
|
8344
|
+
this.logger.info(
|
|
8345
|
+
{ sessionId: persistedSession.id, project: persistedSession.project, workingDir: persistedSession.workingDir },
|
|
8346
|
+
"Starting restoration of backlog-agent session"
|
|
8347
|
+
);
|
|
8348
|
+
const backlogPath = join9(persistedSession.workingDir, "backlog.json");
|
|
8349
|
+
let agentName = "claude";
|
|
8350
|
+
if (existsSync7(backlogPath)) {
|
|
8351
|
+
try {
|
|
8352
|
+
const backlogContent = readFileSync5(backlogPath, "utf-8");
|
|
8353
|
+
const backlogData = JSON.parse(backlogContent);
|
|
8354
|
+
if (backlogData.agent && ["claude", "cursor", "opencode"].includes(backlogData.agent)) {
|
|
8355
|
+
agentName = backlogData.agent;
|
|
8356
|
+
}
|
|
8357
|
+
this.logger.debug(
|
|
8358
|
+
{ sessionId: persistedSession.id, agentName },
|
|
8359
|
+
"Loaded agent type from backlog.json"
|
|
8360
|
+
);
|
|
8361
|
+
} catch (parseError) {
|
|
8362
|
+
this.logger.warn(
|
|
8363
|
+
{ sessionId: persistedSession.id, error: parseError },
|
|
8364
|
+
"Failed to parse backlog.json, using default agent"
|
|
8365
|
+
);
|
|
8366
|
+
}
|
|
8367
|
+
}
|
|
8368
|
+
const backlogSession = new BacklogAgentSession({
|
|
8369
|
+
id: new SessionId(persistedSession.id),
|
|
8370
|
+
type: "backlog-agent",
|
|
8371
|
+
workspacePath: persistedSession.workspace ? new WorkspacePath(
|
|
8372
|
+
persistedSession.workspace,
|
|
8373
|
+
persistedSession.project ?? void 0,
|
|
8374
|
+
persistedSession.worktree ?? void 0
|
|
8375
|
+
) : void 0,
|
|
8376
|
+
workingDir: persistedSession.workingDir,
|
|
8377
|
+
agentName,
|
|
8378
|
+
backlogId: `${persistedSession.project ?? "unknown"}-${Date.now()}`
|
|
8379
|
+
});
|
|
8380
|
+
this.sessions.set(persistedSession.id, backlogSession);
|
|
8381
|
+
try {
|
|
8382
|
+
const manager = BacklogAgentManager.createAndLoadFromFile(
|
|
8383
|
+
backlogSession,
|
|
8384
|
+
persistedSession.workingDir,
|
|
8385
|
+
this.agentSessionManager,
|
|
8386
|
+
this.logger
|
|
8387
|
+
);
|
|
8388
|
+
this.backlogManagers.set(persistedSession.id, manager);
|
|
8389
|
+
backlogRestoreCount++;
|
|
8390
|
+
this.logger.info(
|
|
8391
|
+
{
|
|
8392
|
+
sessionId: persistedSession.id,
|
|
8393
|
+
project: persistedSession.project,
|
|
8394
|
+
backlogManagersCount: this.backlogManagers.size,
|
|
8395
|
+
isInMap: this.backlogManagers.has(persistedSession.id)
|
|
8396
|
+
},
|
|
8397
|
+
"Successfully restored backlog-agent session from database"
|
|
8398
|
+
);
|
|
8399
|
+
} catch (managerError) {
|
|
8400
|
+
this.logger.error(
|
|
8401
|
+
{ sessionId: persistedSession.id, error: managerError instanceof Error ? managerError.message : String(managerError), stack: managerError instanceof Error ? managerError.stack : void 0 },
|
|
8402
|
+
"Failed to create backlog manager during restoration"
|
|
8403
|
+
);
|
|
8404
|
+
throw managerError;
|
|
8405
|
+
}
|
|
8406
|
+
} else if (persistedSession.type === "supervisor") {
|
|
8407
|
+
this.logger.debug({ sessionId: persistedSession.id }, "Skipping supervisor session restoration (singleton)");
|
|
8408
|
+
} else if (persistedSession.type === "terminal") {
|
|
8409
|
+
this.logger.debug({ sessionId: persistedSession.id }, "Skipping terminal session restoration (requires PTY)");
|
|
8410
|
+
}
|
|
8411
|
+
} catch (error) {
|
|
8412
|
+
this.logger.error(
|
|
8413
|
+
{ sessionId: persistedSession.id, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : void 0 },
|
|
8414
|
+
"Error restoring session from database"
|
|
8415
|
+
);
|
|
8416
|
+
}
|
|
8417
|
+
}
|
|
8418
|
+
this.logger.info(
|
|
8419
|
+
{
|
|
8420
|
+
backlogManagersRestored: backlogRestoreCount,
|
|
8421
|
+
totalBacklogManagers: this.backlogManagers.size,
|
|
8422
|
+
backlogManagerIds: Array.from(this.backlogManagers.keys())
|
|
8423
|
+
},
|
|
8424
|
+
"Session restoration complete"
|
|
8425
|
+
);
|
|
8426
|
+
return Promise.resolve();
|
|
8427
|
+
}
|
|
8428
|
+
/**
|
|
8429
|
+
* Sets up event listeners to sync agent session state.
|
|
8430
|
+
*/
|
|
8431
|
+
setupAgentSessionSync() {
|
|
8432
|
+
this.agentSessionManager.on("sessionCreated", (state) => {
|
|
8433
|
+
if (!this.sessions.has(state.sessionId)) {
|
|
8434
|
+
const session = new AgentSession({
|
|
8435
|
+
id: new SessionId(state.sessionId),
|
|
8436
|
+
type: state.agentType,
|
|
8437
|
+
agentName: state.agentName,
|
|
8438
|
+
workingDir: state.workingDir
|
|
8439
|
+
});
|
|
8440
|
+
this.sessions.set(state.sessionId, session);
|
|
8441
|
+
this.logger.debug(
|
|
8442
|
+
{ sessionId: state.sessionId, agentType: state.agentType },
|
|
6453
8443
|
"Agent session registered from external creation"
|
|
6454
8444
|
);
|
|
6455
8445
|
}
|
|
@@ -6473,7 +8463,7 @@ var InMemorySessionManager = class extends EventEmitter3 {
|
|
|
6473
8463
|
* Creates a new session.
|
|
6474
8464
|
*/
|
|
6475
8465
|
async createSession(params) {
|
|
6476
|
-
const { sessionType, workingDir, terminalSize, agentName } = params;
|
|
8466
|
+
const { sessionType, workingDir, terminalSize, agentName, backlogAgent, backlogId } = params;
|
|
6477
8467
|
if (sessionType === "supervisor") {
|
|
6478
8468
|
return this.getOrCreateSupervisor(workingDir);
|
|
6479
8469
|
}
|
|
@@ -6482,6 +8472,16 @@ var InMemorySessionManager = class extends EventEmitter3 {
|
|
|
6482
8472
|
const rows = terminalSize?.rows ?? SESSION_CONFIG.DEFAULT_TERMINAL_ROWS;
|
|
6483
8473
|
const session = await this.ptyManager.create(workingDir, cols, rows);
|
|
6484
8474
|
this.sessions.set(session.id.value, session);
|
|
8475
|
+
if (this.sessionRepository) {
|
|
8476
|
+
this.sessionRepository.create({
|
|
8477
|
+
id: session.id.value,
|
|
8478
|
+
type: "terminal",
|
|
8479
|
+
workspace: params.workspacePath?.workspace,
|
|
8480
|
+
project: params.workspacePath?.project,
|
|
8481
|
+
worktree: params.workspacePath?.worktree,
|
|
8482
|
+
workingDir
|
|
8483
|
+
});
|
|
8484
|
+
}
|
|
6485
8485
|
this.logger.info(
|
|
6486
8486
|
{ sessionId: session.id.value, sessionType, workingDir },
|
|
6487
8487
|
"Terminal session created"
|
|
@@ -6489,6 +8489,9 @@ var InMemorySessionManager = class extends EventEmitter3 {
|
|
|
6489
8489
|
this.emit("terminalSessionCreated", session);
|
|
6490
8490
|
return session;
|
|
6491
8491
|
}
|
|
8492
|
+
if (sessionType === "backlog-agent") {
|
|
8493
|
+
return this.createBacklogSession(workingDir, backlogAgent ?? "claude", backlogId, params.workspacePath);
|
|
8494
|
+
}
|
|
6492
8495
|
if (isAgentType(sessionType)) {
|
|
6493
8496
|
return this.createAgentSession(sessionType, workingDir, agentName, params.workspacePath);
|
|
6494
8497
|
}
|
|
@@ -6521,6 +8524,16 @@ var InMemorySessionManager = class extends EventEmitter3 {
|
|
|
6521
8524
|
cliSessionId: agentState.cliSessionId
|
|
6522
8525
|
});
|
|
6523
8526
|
this.sessions.set(sessionId.value, session);
|
|
8527
|
+
if (this.sessionRepository) {
|
|
8528
|
+
this.sessionRepository.create({
|
|
8529
|
+
id: sessionId.value,
|
|
8530
|
+
type: agentName ?? agentType,
|
|
8531
|
+
workspace: workspacePath?.workspace,
|
|
8532
|
+
project: workspacePath?.project,
|
|
8533
|
+
worktree: workspacePath?.worktree,
|
|
8534
|
+
workingDir
|
|
8535
|
+
});
|
|
8536
|
+
}
|
|
6524
8537
|
this.logger.info(
|
|
6525
8538
|
{ sessionId: sessionId.value, agentType, agentName: resolvedAgentName, workingDir, workspacePath },
|
|
6526
8539
|
"Agent session created"
|
|
@@ -6541,6 +8554,13 @@ var InMemorySessionManager = class extends EventEmitter3 {
|
|
|
6541
8554
|
workingDir
|
|
6542
8555
|
});
|
|
6543
8556
|
this.sessions.set(sessionId.value, this.supervisorSession);
|
|
8557
|
+
if (this.sessionRepository) {
|
|
8558
|
+
this.sessionRepository.create({
|
|
8559
|
+
id: sessionId.value,
|
|
8560
|
+
type: "supervisor",
|
|
8561
|
+
workingDir
|
|
8562
|
+
});
|
|
8563
|
+
}
|
|
6544
8564
|
this.logger.info(
|
|
6545
8565
|
{ sessionId: sessionId.value },
|
|
6546
8566
|
"Supervisor session created"
|
|
@@ -6648,19 +8668,111 @@ var InMemorySessionManager = class extends EventEmitter3 {
|
|
|
6648
8668
|
getAgentSessionManager() {
|
|
6649
8669
|
return this.agentSessionManager;
|
|
6650
8670
|
}
|
|
8671
|
+
/**
|
|
8672
|
+
* Creates a backlog agent session.
|
|
8673
|
+
*/
|
|
8674
|
+
createBacklogSession(workingDir, backlogAgent, backlogId, workspacePath) {
|
|
8675
|
+
const sessionId = new SessionId(`backlog-${nanoid3(8)}`);
|
|
8676
|
+
const finalBacklogId = backlogId ?? `backlog-${nanoid3(8)}`;
|
|
8677
|
+
const session = new BacklogAgentSession({
|
|
8678
|
+
id: sessionId,
|
|
8679
|
+
type: "backlog-agent",
|
|
8680
|
+
workspacePath,
|
|
8681
|
+
workingDir,
|
|
8682
|
+
agentName: backlogAgent,
|
|
8683
|
+
backlogId: finalBacklogId
|
|
8684
|
+
});
|
|
8685
|
+
this.sessions.set(sessionId.value, session);
|
|
8686
|
+
const manager = BacklogAgentManager.createEmpty(
|
|
8687
|
+
session,
|
|
8688
|
+
workingDir,
|
|
8689
|
+
this.agentSessionManager,
|
|
8690
|
+
this.logger
|
|
8691
|
+
);
|
|
8692
|
+
this.backlogManagers.set(sessionId.value, manager);
|
|
8693
|
+
this.logger.debug(
|
|
8694
|
+
{
|
|
8695
|
+
sessionId: sessionId.value,
|
|
8696
|
+
backlogManagersCount: this.backlogManagers.size
|
|
8697
|
+
},
|
|
8698
|
+
"BacklogAgentManager registered"
|
|
8699
|
+
);
|
|
8700
|
+
if (this.sessionRepository) {
|
|
8701
|
+
this.sessionRepository.create({
|
|
8702
|
+
id: sessionId.value,
|
|
8703
|
+
type: "backlog-agent",
|
|
8704
|
+
workspace: workspacePath?.workspace,
|
|
8705
|
+
project: workspacePath?.project,
|
|
8706
|
+
worktree: workspacePath?.worktree,
|
|
8707
|
+
workingDir
|
|
8708
|
+
});
|
|
8709
|
+
}
|
|
8710
|
+
this.logger.info(
|
|
8711
|
+
{
|
|
8712
|
+
sessionId: sessionId.value,
|
|
8713
|
+
backlogId: finalBacklogId,
|
|
8714
|
+
backlogAgent,
|
|
8715
|
+
workingDir,
|
|
8716
|
+
workspacePath
|
|
8717
|
+
},
|
|
8718
|
+
"Backlog agent session created"
|
|
8719
|
+
);
|
|
8720
|
+
return session;
|
|
8721
|
+
}
|
|
8722
|
+
/**
|
|
8723
|
+
* Gets the backlog managers registry.
|
|
8724
|
+
*/
|
|
8725
|
+
getBacklogManagers() {
|
|
8726
|
+
return this.backlogManagers;
|
|
8727
|
+
}
|
|
6651
8728
|
};
|
|
6652
8729
|
|
|
6653
|
-
// src/infrastructure/agents/supervisor/supervisor-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
8730
|
+
// src/infrastructure/agents/supervisor/supervisor-state-manager.ts
|
|
8731
|
+
var SupervisorStateManager = class {
|
|
8732
|
+
constructor(chatHistoryService) {
|
|
8733
|
+
this.chatHistoryService = chatHistoryService;
|
|
8734
|
+
}
|
|
8735
|
+
loadHistory() {
|
|
8736
|
+
const history = this.chatHistoryService.getSupervisorHistory();
|
|
8737
|
+
const entries = history.map((entry) => ({
|
|
8738
|
+
role: entry.role,
|
|
8739
|
+
content: entry.content,
|
|
8740
|
+
timestamp: new Date(entry.createdAt).getTime()
|
|
8741
|
+
}));
|
|
8742
|
+
return Promise.resolve(entries);
|
|
8743
|
+
}
|
|
8744
|
+
saveHistory(history) {
|
|
8745
|
+
if (history.length === 0) return Promise.resolve();
|
|
8746
|
+
const lastEntry = history[history.length - 1];
|
|
8747
|
+
if (!lastEntry) return Promise.resolve();
|
|
8748
|
+
if (lastEntry.role === "system") return Promise.resolve();
|
|
8749
|
+
const storedHistory = this.chatHistoryService.getSupervisorHistory(1);
|
|
8750
|
+
const mostRecentStored = storedHistory[0];
|
|
8751
|
+
if (!mostRecentStored || mostRecentStored.content !== lastEntry.content) {
|
|
8752
|
+
this.chatHistoryService.saveSupervisorMessage(lastEntry.role, lastEntry.content);
|
|
8753
|
+
}
|
|
8754
|
+
return Promise.resolve();
|
|
8755
|
+
}
|
|
8756
|
+
clearHistory() {
|
|
8757
|
+
this.chatHistoryService.clearSupervisorHistory();
|
|
8758
|
+
return Promise.resolve();
|
|
8759
|
+
}
|
|
8760
|
+
loadAdditionalState(_key) {
|
|
8761
|
+
return Promise.resolve(null);
|
|
8762
|
+
}
|
|
8763
|
+
saveAdditionalState(_key, _state) {
|
|
8764
|
+
return Promise.resolve();
|
|
8765
|
+
}
|
|
8766
|
+
close() {
|
|
8767
|
+
return Promise.resolve();
|
|
8768
|
+
}
|
|
8769
|
+
};
|
|
6658
8770
|
|
|
6659
|
-
// src/infrastructure/agents/supervisor/tools/workspace-tools.ts
|
|
6660
|
-
import { tool } from "@langchain/core/tools";
|
|
6661
|
-
import { z as
|
|
8771
|
+
// src/infrastructure/agents/supervisor/tools/workspace-tools.ts
|
|
8772
|
+
import { tool as tool2 } from "@langchain/core/tools";
|
|
8773
|
+
import { z as z5 } from "zod";
|
|
6662
8774
|
function createWorkspaceTools(workspaceDiscovery) {
|
|
6663
|
-
const listWorkspaces =
|
|
8775
|
+
const listWorkspaces = tool2(
|
|
6664
8776
|
async () => {
|
|
6665
8777
|
const workspaces = await workspaceDiscovery.listWorkspaces();
|
|
6666
8778
|
if (workspaces.length === 0) {
|
|
@@ -6672,10 +8784,10 @@ ${workspaces.map((w) => `- ${w.name} (${w.projectCount} projects)`).join("\n")}`
|
|
|
6672
8784
|
{
|
|
6673
8785
|
name: "list_workspaces",
|
|
6674
8786
|
description: "Lists all available workspaces (top-level directories in the workspaces root). Use this to discover what workspaces exist.",
|
|
6675
|
-
schema:
|
|
8787
|
+
schema: z5.object({})
|
|
6676
8788
|
}
|
|
6677
8789
|
);
|
|
6678
|
-
const listProjects =
|
|
8790
|
+
const listProjects = tool2(
|
|
6679
8791
|
async ({ workspace }) => {
|
|
6680
8792
|
try {
|
|
6681
8793
|
const projects = await workspaceDiscovery.listProjects(workspace);
|
|
@@ -6691,12 +8803,12 @@ ${projects.map((p) => `- ${p.name}${p.isGitRepo ? " (git)" : ""}`).join("\n")}`;
|
|
|
6691
8803
|
{
|
|
6692
8804
|
name: "list_projects",
|
|
6693
8805
|
description: "Lists all projects in a specific workspace. Projects are git repositories or directories.",
|
|
6694
|
-
schema:
|
|
6695
|
-
workspace:
|
|
8806
|
+
schema: z5.object({
|
|
8807
|
+
workspace: z5.string().describe("Name of the workspace to list projects from")
|
|
6696
8808
|
})
|
|
6697
8809
|
}
|
|
6698
8810
|
);
|
|
6699
|
-
const getProjectInfo =
|
|
8811
|
+
const getProjectInfo = tool2(
|
|
6700
8812
|
async ({ workspace, project }) => {
|
|
6701
8813
|
try {
|
|
6702
8814
|
const projectInfo = await workspaceDiscovery.getProject(workspace, project);
|
|
@@ -6717,13 +8829,13 @@ Default Branch: ${projectInfo.defaultBranch}` : ""}${worktreeInfo}`;
|
|
|
6717
8829
|
{
|
|
6718
8830
|
name: "get_project_info",
|
|
6719
8831
|
description: "Gets detailed information about a project including its path and any git worktrees.",
|
|
6720
|
-
schema:
|
|
6721
|
-
workspace:
|
|
6722
|
-
project:
|
|
8832
|
+
schema: z5.object({
|
|
8833
|
+
workspace: z5.string().describe("Name of the workspace"),
|
|
8834
|
+
project: z5.string().describe("Name of the project")
|
|
6723
8835
|
})
|
|
6724
8836
|
}
|
|
6725
8837
|
);
|
|
6726
|
-
const createWorkspace =
|
|
8838
|
+
const createWorkspace = tool2(
|
|
6727
8839
|
async ({ name }) => {
|
|
6728
8840
|
try {
|
|
6729
8841
|
const workspace = await workspaceDiscovery.createWorkspace(name);
|
|
@@ -6735,15 +8847,15 @@ Default Branch: ${projectInfo.defaultBranch}` : ""}${worktreeInfo}`;
|
|
|
6735
8847
|
{
|
|
6736
8848
|
name: "create_workspace",
|
|
6737
8849
|
description: 'Creates a new workspace directory. The name must be in lower-kebab-case (e.g., "my-company", "personal-projects").',
|
|
6738
|
-
schema:
|
|
6739
|
-
name:
|
|
8850
|
+
schema: z5.object({
|
|
8851
|
+
name: z5.string().regex(
|
|
6740
8852
|
/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/,
|
|
6741
8853
|
'Name must be in lower-kebab-case (e.g., "my-workspace")'
|
|
6742
8854
|
).describe("Name for the new workspace in lower-kebab-case")
|
|
6743
8855
|
})
|
|
6744
8856
|
}
|
|
6745
8857
|
);
|
|
6746
|
-
const createProject =
|
|
8858
|
+
const createProject = tool2(
|
|
6747
8859
|
async ({
|
|
6748
8860
|
workspace,
|
|
6749
8861
|
name,
|
|
@@ -6761,13 +8873,13 @@ Path: ${project.path}`;
|
|
|
6761
8873
|
{
|
|
6762
8874
|
name: "create_project",
|
|
6763
8875
|
description: "Creates a new project directory within a workspace. The name must be in lower-kebab-case. By default, initializes a git repository.",
|
|
6764
|
-
schema:
|
|
6765
|
-
workspace:
|
|
6766
|
-
name:
|
|
8876
|
+
schema: z5.object({
|
|
8877
|
+
workspace: z5.string().describe("Name of the workspace to create the project in"),
|
|
8878
|
+
name: z5.string().regex(
|
|
6767
8879
|
/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/,
|
|
6768
8880
|
'Name must be in lower-kebab-case (e.g., "my-project")'
|
|
6769
8881
|
).describe("Name for the new project in lower-kebab-case"),
|
|
6770
|
-
init_git:
|
|
8882
|
+
init_git: z5.boolean().optional().default(true).describe("Whether to initialize a git repository (default: true)")
|
|
6771
8883
|
})
|
|
6772
8884
|
}
|
|
6773
8885
|
);
|
|
@@ -6775,10 +8887,10 @@ Path: ${project.path}`;
|
|
|
6775
8887
|
}
|
|
6776
8888
|
|
|
6777
8889
|
// src/infrastructure/agents/supervisor/tools/worktree-tools.ts
|
|
6778
|
-
import { tool as
|
|
6779
|
-
import { z as
|
|
8890
|
+
import { tool as tool3 } from "@langchain/core/tools";
|
|
8891
|
+
import { z as z6 } from "zod";
|
|
6780
8892
|
function createWorktreeTools(workspaceDiscovery, _agentSessionManager) {
|
|
6781
|
-
const listWorktrees =
|
|
8893
|
+
const listWorktrees = tool3(
|
|
6782
8894
|
async ({ workspace, project }) => {
|
|
6783
8895
|
try {
|
|
6784
8896
|
const worktrees = await workspaceDiscovery.listWorktrees(workspace, project);
|
|
@@ -6794,13 +8906,13 @@ ${worktrees.map((w) => `- ${w.name}: ${w.branch} (${w.path})`).join("\n")}`;
|
|
|
6794
8906
|
{
|
|
6795
8907
|
name: "list_worktrees",
|
|
6796
8908
|
description: "Lists all git worktrees for a specific project. Worktrees allow working on multiple branches simultaneously.",
|
|
6797
|
-
schema:
|
|
6798
|
-
workspace:
|
|
6799
|
-
project:
|
|
8909
|
+
schema: z6.object({
|
|
8910
|
+
workspace: z6.string().describe("Name of the workspace"),
|
|
8911
|
+
project: z6.string().describe("Name of the project (git repository)")
|
|
6800
8912
|
})
|
|
6801
8913
|
}
|
|
6802
8914
|
);
|
|
6803
|
-
const createWorktree =
|
|
8915
|
+
const createWorktree = tool3(
|
|
6804
8916
|
async ({
|
|
6805
8917
|
workspace,
|
|
6806
8918
|
project,
|
|
@@ -6825,16 +8937,16 @@ ${worktrees.map((w) => `- ${w.name}: ${w.branch} (${w.path})`).join("\n")}`;
|
|
|
6825
8937
|
{
|
|
6826
8938
|
name: "create_worktree",
|
|
6827
8939
|
description: "Creates a new git worktree for a project. Use this to work on a different branch without switching. Can either checkout an existing branch or create a new branch.",
|
|
6828
|
-
schema:
|
|
6829
|
-
workspace:
|
|
6830
|
-
project:
|
|
6831
|
-
branch:
|
|
6832
|
-
createNewBranch:
|
|
6833
|
-
baseBranch:
|
|
8940
|
+
schema: z6.object({
|
|
8941
|
+
workspace: z6.string().describe("Name of the workspace"),
|
|
8942
|
+
project: z6.string().describe("Name of the project (git repository)"),
|
|
8943
|
+
branch: z6.string().describe("Branch name to use in the worktree"),
|
|
8944
|
+
createNewBranch: z6.boolean().optional().describe("If true, creates a new branch with the given name. If false or omitted, checks out an existing branch."),
|
|
8945
|
+
baseBranch: z6.string().optional().describe('When creating a new branch, specifies the starting point (e.g., "main", "develop"). Defaults to current HEAD if not specified.')
|
|
6834
8946
|
})
|
|
6835
8947
|
}
|
|
6836
8948
|
);
|
|
6837
|
-
const removeWorktree =
|
|
8949
|
+
const removeWorktree = tool3(
|
|
6838
8950
|
async ({
|
|
6839
8951
|
workspace,
|
|
6840
8952
|
project,
|
|
@@ -6850,14 +8962,14 @@ ${worktrees.map((w) => `- ${w.name}: ${w.branch} (${w.path})`).join("\n")}`;
|
|
|
6850
8962
|
{
|
|
6851
8963
|
name: "remove_worktree",
|
|
6852
8964
|
description: "Removes a git worktree from a project. This deletes the worktree directory.",
|
|
6853
|
-
schema:
|
|
6854
|
-
workspace:
|
|
6855
|
-
project:
|
|
6856
|
-
worktreeName:
|
|
8965
|
+
schema: z6.object({
|
|
8966
|
+
workspace: z6.string().describe("Name of the workspace"),
|
|
8967
|
+
project: z6.string().describe("Name of the project"),
|
|
8968
|
+
worktreeName: z6.string().describe("Name of the worktree to remove")
|
|
6857
8969
|
})
|
|
6858
8970
|
}
|
|
6859
8971
|
);
|
|
6860
|
-
const branchStatus =
|
|
8972
|
+
const branchStatus = tool3(
|
|
6861
8973
|
async ({ workspace, project }) => {
|
|
6862
8974
|
try {
|
|
6863
8975
|
const status = await workspaceDiscovery.getBranchStatus(workspace, project);
|
|
@@ -6876,13 +8988,13 @@ ${status.uncommittedChanges.map((c) => ` ${c}`).join("\n")}`
|
|
|
6876
8988
|
{
|
|
6877
8989
|
name: "branch_status",
|
|
6878
8990
|
description: "Get current branch status including uncommitted changes and commit count ahead of main",
|
|
6879
|
-
schema:
|
|
6880
|
-
workspace:
|
|
6881
|
-
project:
|
|
8991
|
+
schema: z6.object({
|
|
8992
|
+
workspace: z6.string().describe("Workspace name containing the project"),
|
|
8993
|
+
project: z6.string().describe("Project name to get branch status for")
|
|
6882
8994
|
})
|
|
6883
8995
|
}
|
|
6884
8996
|
);
|
|
6885
|
-
const mergeBranch =
|
|
8997
|
+
const mergeBranch = tool3(
|
|
6886
8998
|
async ({ workspace, project, sourceBranch, targetBranch, pushAfter, skipPreCheck }) => {
|
|
6887
8999
|
try {
|
|
6888
9000
|
const result = await workspaceDiscovery.mergeBranch(
|
|
@@ -6903,17 +9015,17 @@ ${status.uncommittedChanges.map((c) => ` ${c}`).join("\n")}`
|
|
|
6903
9015
|
{
|
|
6904
9016
|
name: "merge_branch",
|
|
6905
9017
|
description: "Merge source branch into target branch with safety checks and optional push to remote",
|
|
6906
|
-
schema:
|
|
6907
|
-
workspace:
|
|
6908
|
-
project:
|
|
6909
|
-
sourceBranch:
|
|
6910
|
-
targetBranch:
|
|
6911
|
-
pushAfter:
|
|
6912
|
-
skipPreCheck:
|
|
9018
|
+
schema: z6.object({
|
|
9019
|
+
workspace: z6.string().describe("Workspace name"),
|
|
9020
|
+
project: z6.string().describe("Project name"),
|
|
9021
|
+
sourceBranch: z6.string().describe("Source branch to merge from"),
|
|
9022
|
+
targetBranch: z6.string().optional().describe("Target branch (defaults to main)"),
|
|
9023
|
+
pushAfter: z6.boolean().optional().describe("Push to remote after successful merge"),
|
|
9024
|
+
skipPreCheck: z6.boolean().optional().describe("Skip pre-merge safety checks (not recommended)")
|
|
6913
9025
|
})
|
|
6914
9026
|
}
|
|
6915
9027
|
);
|
|
6916
|
-
const listMergeableBranches =
|
|
9028
|
+
const listMergeableBranches = tool3(
|
|
6917
9029
|
async ({ workspace, project }) => {
|
|
6918
9030
|
try {
|
|
6919
9031
|
const branches = await workspaceDiscovery.listMergeableBranches(workspace, project);
|
|
@@ -6942,13 +9054,13 @@ ${statusLines.join("\n")}`;
|
|
|
6942
9054
|
{
|
|
6943
9055
|
name: "list_mergeable_branches",
|
|
6944
9056
|
description: "List all feature branches and their merge/cleanup status",
|
|
6945
|
-
schema:
|
|
6946
|
-
workspace:
|
|
6947
|
-
project:
|
|
9057
|
+
schema: z6.object({
|
|
9058
|
+
workspace: z6.string().describe("Workspace name"),
|
|
9059
|
+
project: z6.string().describe("Project name")
|
|
6948
9060
|
})
|
|
6949
9061
|
}
|
|
6950
9062
|
);
|
|
6951
|
-
const cleanupWorktree =
|
|
9063
|
+
const cleanupWorktree = tool3(
|
|
6952
9064
|
async ({ workspace, project, branch, force }) => {
|
|
6953
9065
|
try {
|
|
6954
9066
|
if (!force) {
|
|
@@ -6978,15 +9090,15 @@ ${statusLines.join("\n")}`;
|
|
|
6978
9090
|
{
|
|
6979
9091
|
name: "cleanup_worktree",
|
|
6980
9092
|
description: "Remove worktree and optionally delete the branch if merged",
|
|
6981
|
-
schema:
|
|
6982
|
-
workspace:
|
|
6983
|
-
project:
|
|
6984
|
-
branch:
|
|
6985
|
-
force:
|
|
9093
|
+
schema: z6.object({
|
|
9094
|
+
workspace: z6.string().describe("Workspace name"),
|
|
9095
|
+
project: z6.string().describe("Project name"),
|
|
9096
|
+
branch: z6.string().describe("Branch/worktree name to cleanup"),
|
|
9097
|
+
force: z6.boolean().optional().describe("Force cleanup even with uncommitted changes")
|
|
6986
9098
|
})
|
|
6987
9099
|
}
|
|
6988
9100
|
);
|
|
6989
|
-
const completeFeature =
|
|
9101
|
+
const completeFeature = tool3(
|
|
6990
9102
|
async ({ workspace, project, featureBranch, targetBranch, skipConfirmation }) => {
|
|
6991
9103
|
try {
|
|
6992
9104
|
const results = [];
|
|
@@ -7055,12 +9167,12 @@ Steps executed:
|
|
|
7055
9167
|
{
|
|
7056
9168
|
name: "complete_feature",
|
|
7057
9169
|
description: "Complete workflow: merge feature branch into main and cleanup worktree/branch",
|
|
7058
|
-
schema:
|
|
7059
|
-
workspace:
|
|
7060
|
-
project:
|
|
7061
|
-
featureBranch:
|
|
7062
|
-
targetBranch:
|
|
7063
|
-
skipConfirmation:
|
|
9170
|
+
schema: z6.object({
|
|
9171
|
+
workspace: z6.string().describe("Workspace name"),
|
|
9172
|
+
project: z6.string().describe("Project name"),
|
|
9173
|
+
featureBranch: z6.string().describe("Feature branch name to complete"),
|
|
9174
|
+
targetBranch: z6.string().optional().describe("Target branch (defaults to main)"),
|
|
9175
|
+
skipConfirmation: z6.boolean().optional().describe("Skip safety checks (not recommended)")
|
|
7064
9176
|
})
|
|
7065
9177
|
}
|
|
7066
9178
|
);
|
|
@@ -7077,10 +9189,10 @@ Steps executed:
|
|
|
7077
9189
|
}
|
|
7078
9190
|
|
|
7079
9191
|
// src/infrastructure/agents/supervisor/tools/session-tools.ts
|
|
7080
|
-
import { tool as
|
|
7081
|
-
import { z as
|
|
9192
|
+
import { tool as tool4 } from "@langchain/core/tools";
|
|
9193
|
+
import { z as z7 } from "zod";
|
|
7082
9194
|
function createSessionTools(sessionManager, agentSessionManager, workspaceDiscovery, workspacesRoot, _getMessageBroadcaster, getChatHistoryService, clearSupervisorContext, terminateSessionCallback) {
|
|
7083
|
-
const listSessions =
|
|
9195
|
+
const listSessions = tool4(
|
|
7084
9196
|
() => {
|
|
7085
9197
|
const chatHistoryService = getChatHistoryService?.();
|
|
7086
9198
|
const inMemorySessions = sessionManager.getAllSessions();
|
|
@@ -7108,10 +9220,10 @@ ${allSessions.map((s) => `- [${s.type}] ${s.id} (${s.status}) - ${s.workingDir}`
|
|
|
7108
9220
|
{
|
|
7109
9221
|
name: "list_sessions",
|
|
7110
9222
|
description: "Lists all active sessions (terminals, agents). Use this to see what sessions are running.",
|
|
7111
|
-
schema:
|
|
9223
|
+
schema: z7.object({})
|
|
7112
9224
|
}
|
|
7113
9225
|
);
|
|
7114
|
-
const listAvailableAgents =
|
|
9226
|
+
const listAvailableAgents = tool4(
|
|
7115
9227
|
() => {
|
|
7116
9228
|
const agents = getAvailableAgents();
|
|
7117
9229
|
if (agents.size === 0) {
|
|
@@ -7127,10 +9239,10 @@ ${allSessions.map((s) => `- [${s.type}] ${s.id} (${s.status}) - ${s.workingDir}`
|
|
|
7127
9239
|
{
|
|
7128
9240
|
name: "list_available_agents",
|
|
7129
9241
|
description: "Lists all available AI agent types, including base agents (cursor, claude, opencode) and custom aliases configured via AGENT_ALIAS_* environment variables.",
|
|
7130
|
-
schema:
|
|
9242
|
+
schema: z7.object({})
|
|
7131
9243
|
}
|
|
7132
9244
|
);
|
|
7133
|
-
const createAgentSession =
|
|
9245
|
+
const createAgentSession = tool4(
|
|
7134
9246
|
async ({
|
|
7135
9247
|
agentName,
|
|
7136
9248
|
workspace,
|
|
@@ -7143,7 +9255,10 @@ ${allSessions.map((s) => `- [${s.type}] ${s.id} (${s.status}) - ${s.workingDir}`
|
|
|
7143
9255
|
const available = Array.from(getAvailableAgents().keys()).join(", ");
|
|
7144
9256
|
return `Error: Unknown agent "${agentName}". Available agents: ${available}`;
|
|
7145
9257
|
}
|
|
7146
|
-
|
|
9258
|
+
let normalizedWorktree = worktree;
|
|
9259
|
+
if (normalizedWorktree && (normalizedWorktree.toLowerCase() === "main" || normalizedWorktree.toLowerCase() === "master")) {
|
|
9260
|
+
normalizedWorktree = void 0;
|
|
9261
|
+
}
|
|
7147
9262
|
const workingDir = workspaceDiscovery.resolvePath(workspace, project, normalizedWorktree);
|
|
7148
9263
|
const exists = await workspaceDiscovery.pathExists(workingDir);
|
|
7149
9264
|
if (!exists) {
|
|
@@ -7164,22 +9279,25 @@ Working directory: ${session.workingDir}`;
|
|
|
7164
9279
|
{
|
|
7165
9280
|
name: "create_agent_session",
|
|
7166
9281
|
description: "Creates a new AI agent session in a specific project. Supports base agents (cursor, claude, opencode) and custom aliases configured via AGENT_ALIAS_* environment variables. Use list_available_agents to see all available options.",
|
|
7167
|
-
schema:
|
|
7168
|
-
agentName:
|
|
7169
|
-
workspace:
|
|
7170
|
-
project:
|
|
7171
|
-
worktree:
|
|
9282
|
+
schema: z7.object({
|
|
9283
|
+
agentName: z7.string().describe('Name of the agent to start (e.g., "claude", "cursor", "opencode", or a custom alias like "zai")'),
|
|
9284
|
+
workspace: z7.string().describe("Name of the workspace"),
|
|
9285
|
+
project: z7.string().describe("Name of the project"),
|
|
9286
|
+
worktree: z7.string().optional().describe("Optional worktree name")
|
|
7172
9287
|
})
|
|
7173
9288
|
}
|
|
7174
9289
|
);
|
|
7175
|
-
const createTerminalSession =
|
|
9290
|
+
const createTerminalSession = tool4(
|
|
7176
9291
|
async ({
|
|
7177
9292
|
workspace,
|
|
7178
9293
|
project,
|
|
7179
9294
|
worktree
|
|
7180
9295
|
}) => {
|
|
7181
9296
|
try {
|
|
7182
|
-
|
|
9297
|
+
let normalizedWorktree = worktree;
|
|
9298
|
+
if (normalizedWorktree && (normalizedWorktree.toLowerCase() === "main" || normalizedWorktree.toLowerCase() === "master")) {
|
|
9299
|
+
normalizedWorktree = void 0;
|
|
9300
|
+
}
|
|
7183
9301
|
let workingDir;
|
|
7184
9302
|
if (workspace && project) {
|
|
7185
9303
|
workingDir = workspaceDiscovery.resolvePath(workspace, project, normalizedWorktree);
|
|
@@ -7211,14 +9329,14 @@ Working directory: ${session.workingDir}`;
|
|
|
7211
9329
|
- workspace + project + worktree: opens terminal in that worktree directory
|
|
7212
9330
|
|
|
7213
9331
|
Use this tool when user asks to "open terminal", "create terminal", or similar requests.`,
|
|
7214
|
-
schema:
|
|
7215
|
-
workspace:
|
|
7216
|
-
project:
|
|
7217
|
-
worktree:
|
|
9332
|
+
schema: z7.object({
|
|
9333
|
+
workspace: z7.string().optional().describe("Name of the workspace. Omit to open in workspaces root."),
|
|
9334
|
+
project: z7.string().optional().describe("Name of the project within the workspace. Requires workspace."),
|
|
9335
|
+
worktree: z7.string().optional().describe("Worktree name. Requires workspace and project.")
|
|
7218
9336
|
})
|
|
7219
9337
|
}
|
|
7220
9338
|
);
|
|
7221
|
-
const terminateSession =
|
|
9339
|
+
const terminateSession = tool4(
|
|
7222
9340
|
async ({ sessionId }) => {
|
|
7223
9341
|
try {
|
|
7224
9342
|
if (!terminateSessionCallback) {
|
|
@@ -7236,12 +9354,12 @@ Use this tool when user asks to "open terminal", "create terminal", or similar r
|
|
|
7236
9354
|
{
|
|
7237
9355
|
name: "terminate_session",
|
|
7238
9356
|
description: "Terminates an active session by its ID.",
|
|
7239
|
-
schema:
|
|
7240
|
-
sessionId:
|
|
9357
|
+
schema: z7.object({
|
|
9358
|
+
sessionId: z7.string().describe("ID of the session to terminate")
|
|
7241
9359
|
})
|
|
7242
9360
|
}
|
|
7243
9361
|
);
|
|
7244
|
-
const terminateAllSessions =
|
|
9362
|
+
const terminateAllSessions = tool4(
|
|
7245
9363
|
async ({ sessionType }) => {
|
|
7246
9364
|
try {
|
|
7247
9365
|
if (!terminateSessionCallback) {
|
|
@@ -7294,12 +9412,12 @@ ${errors.map((e) => ` - ${e}`).join("\n")}`;
|
|
|
7294
9412
|
{
|
|
7295
9413
|
name: "terminate_all_sessions",
|
|
7296
9414
|
description: "Terminates all active sessions, or all sessions of a specific type (terminal, cursor, claude, opencode).",
|
|
7297
|
-
schema:
|
|
7298
|
-
sessionType:
|
|
9415
|
+
schema: z7.object({
|
|
9416
|
+
sessionType: z7.enum(["terminal", "cursor", "claude", "opencode", "all"]).optional().describe('Type of sessions to terminate. Omit or use "all" to terminate all sessions.')
|
|
7299
9417
|
})
|
|
7300
9418
|
}
|
|
7301
9419
|
);
|
|
7302
|
-
const getSessionInfo =
|
|
9420
|
+
const getSessionInfo = tool4(
|
|
7303
9421
|
async ({ sessionId }) => {
|
|
7304
9422
|
try {
|
|
7305
9423
|
const { SessionId: SessionId2 } = await import("./session-id-VKOYWZAK.js");
|
|
@@ -7334,12 +9452,12 @@ Messages: ${agentState.messages.length}`;
|
|
|
7334
9452
|
{
|
|
7335
9453
|
name: "get_session_info",
|
|
7336
9454
|
description: "Gets detailed information about a specific session.",
|
|
7337
|
-
schema:
|
|
7338
|
-
sessionId:
|
|
9455
|
+
schema: z7.object({
|
|
9456
|
+
sessionId: z7.string().describe("ID of the session")
|
|
7339
9457
|
})
|
|
7340
9458
|
}
|
|
7341
9459
|
);
|
|
7342
|
-
const listSessionsWithWorktrees =
|
|
9460
|
+
const listSessionsWithWorktrees = tool4(
|
|
7343
9461
|
() => {
|
|
7344
9462
|
try {
|
|
7345
9463
|
const sessions2 = agentSessionManager.listSessionsWithWorktreeInfo();
|
|
@@ -7361,10 +9479,10 @@ ${sessionLines.join("\n")}`;
|
|
|
7361
9479
|
{
|
|
7362
9480
|
name: "list_sessions_with_worktrees",
|
|
7363
9481
|
description: "Lists all active sessions with worktree information for branch management",
|
|
7364
|
-
schema:
|
|
9482
|
+
schema: z7.object({})
|
|
7365
9483
|
}
|
|
7366
9484
|
);
|
|
7367
|
-
const getWorktreeSessionSummary =
|
|
9485
|
+
const getWorktreeSessionSummary = tool4(
|
|
7368
9486
|
({ workspace, project, branch }) => {
|
|
7369
9487
|
try {
|
|
7370
9488
|
const summary = agentSessionManager.getWorktreeSessionSummary(workspace, project, branch);
|
|
@@ -7387,14 +9505,14 @@ Executing: ${summary.executingCount}`;
|
|
|
7387
9505
|
{
|
|
7388
9506
|
name: "get_worktree_session_summary",
|
|
7389
9507
|
description: "Gets detailed session information for a specific worktree",
|
|
7390
|
-
schema:
|
|
7391
|
-
workspace:
|
|
7392
|
-
project:
|
|
7393
|
-
branch:
|
|
9508
|
+
schema: z7.object({
|
|
9509
|
+
workspace: z7.string().describe("Workspace name"),
|
|
9510
|
+
project: z7.string().describe("Project name"),
|
|
9511
|
+
branch: z7.string().describe("Branch/worktree name")
|
|
7394
9512
|
})
|
|
7395
9513
|
}
|
|
7396
9514
|
);
|
|
7397
|
-
const terminateWorktreeSessions =
|
|
9515
|
+
const terminateWorktreeSessions = tool4(
|
|
7398
9516
|
({ workspace, project, branch }) => {
|
|
7399
9517
|
try {
|
|
7400
9518
|
const terminatedSessions = agentSessionManager.terminateWorktreeSessions(workspace, project, branch);
|
|
@@ -7410,14 +9528,14 @@ ${terminatedSessions.map((id) => ` - ${id}`).join("\n")}`;
|
|
|
7410
9528
|
{
|
|
7411
9529
|
name: "terminate_worktree_sessions",
|
|
7412
9530
|
description: "Terminates all active sessions in a specific worktree",
|
|
7413
|
-
schema:
|
|
7414
|
-
workspace:
|
|
7415
|
-
project:
|
|
7416
|
-
branch:
|
|
9531
|
+
schema: z7.object({
|
|
9532
|
+
workspace: z7.string().describe("Workspace name"),
|
|
9533
|
+
project: z7.string().describe("Project name"),
|
|
9534
|
+
branch: z7.string().describe("Branch/worktree name")
|
|
7417
9535
|
})
|
|
7418
9536
|
}
|
|
7419
9537
|
);
|
|
7420
|
-
const clearContext =
|
|
9538
|
+
const clearContext = tool4(
|
|
7421
9539
|
() => {
|
|
7422
9540
|
try {
|
|
7423
9541
|
if (clearSupervisorContext) {
|
|
@@ -7431,7 +9549,7 @@ ${terminatedSessions.map((id) => ` - ${id}`).join("\n")}`;
|
|
|
7431
9549
|
{
|
|
7432
9550
|
name: "clear_supervisor_context",
|
|
7433
9551
|
description: 'Clears the supervisor agent conversation context and history. Use when user asks to "clear context", "reset conversation", "start fresh", or similar requests.',
|
|
7434
|
-
schema:
|
|
9552
|
+
schema: z7.object({})
|
|
7435
9553
|
}
|
|
7436
9554
|
);
|
|
7437
9555
|
return [
|
|
@@ -7450,12 +9568,12 @@ ${terminatedSessions.map((id) => ` - ${id}`).join("\n")}`;
|
|
|
7450
9568
|
}
|
|
7451
9569
|
|
|
7452
9570
|
// src/infrastructure/agents/supervisor/tools/filesystem-tools.ts
|
|
7453
|
-
import { tool as
|
|
7454
|
-
import { z as
|
|
9571
|
+
import { tool as tool5 } from "@langchain/core/tools";
|
|
9572
|
+
import { z as z8 } from "zod";
|
|
7455
9573
|
import { readdir as readdir3, readFile as readFile2, stat as stat2 } from "fs/promises";
|
|
7456
|
-
import { join as
|
|
9574
|
+
import { join as join10, resolve } from "path";
|
|
7457
9575
|
function createFilesystemTools(workspacesRoot) {
|
|
7458
|
-
const listDirectory =
|
|
9576
|
+
const listDirectory = tool5(
|
|
7459
9577
|
async ({ path, showHidden }) => {
|
|
7460
9578
|
try {
|
|
7461
9579
|
const resolvedPath = resolveSafePath(path, workspacesRoot);
|
|
@@ -7477,432 +9595,324 @@ ${formatted.join("\n")}`;
|
|
|
7477
9595
|
{
|
|
7478
9596
|
name: "list_directory",
|
|
7479
9597
|
description: "Lists the contents of a directory. Shows files and subdirectories.",
|
|
7480
|
-
schema:
|
|
7481
|
-
path:
|
|
7482
|
-
showHidden:
|
|
9598
|
+
schema: z8.object({
|
|
9599
|
+
path: z8.string().describe("Path to the directory (relative to workspaces root or absolute)"),
|
|
9600
|
+
showHidden: z8.boolean().optional().describe("Whether to show hidden files (starting with .)")
|
|
7483
9601
|
})
|
|
7484
9602
|
}
|
|
7485
9603
|
);
|
|
7486
|
-
const readFileContent =
|
|
9604
|
+
const readFileContent = tool5(
|
|
7487
9605
|
async ({ path, maxLines }) => {
|
|
7488
9606
|
try {
|
|
7489
9607
|
const resolvedPath = resolveSafePath(path, workspacesRoot);
|
|
7490
9608
|
const fileStat = await stat2(resolvedPath);
|
|
7491
|
-
if (fileStat.size > 100 * 1024) {
|
|
7492
|
-
return `File is too large (${Math.round(fileStat.size / 1024)}KB). Maximum is 100KB.`;
|
|
7493
|
-
}
|
|
7494
|
-
const content = await readFile2(resolvedPath, "utf-8");
|
|
7495
|
-
const lines = content.split("\n");
|
|
7496
|
-
if (maxLines && lines.length > maxLines) {
|
|
7497
|
-
return `File: ${path} (showing first ${maxLines} of ${lines.length} lines)
|
|
7498
|
-
|
|
7499
|
-
${lines.slice(0, maxLines).join("\n")}
|
|
7500
|
-
|
|
7501
|
-
... (${lines.length - maxLines} more lines)`;
|
|
7502
|
-
}
|
|
7503
|
-
return `File: ${path}
|
|
7504
|
-
|
|
7505
|
-
${content}`;
|
|
7506
|
-
} catch (error) {
|
|
7507
|
-
return `Error reading file: ${error instanceof Error ? error.message : String(error)}`;
|
|
7508
|
-
}
|
|
7509
|
-
},
|
|
7510
|
-
{
|
|
7511
|
-
name: "read_file",
|
|
7512
|
-
description: "Reads the contents of a file. Limited to 100KB files for safety.",
|
|
7513
|
-
schema: z6.object({
|
|
7514
|
-
path: z6.string().describe("Path to the file"),
|
|
7515
|
-
maxLines: z6.number().optional().describe("Maximum number of lines to return")
|
|
7516
|
-
})
|
|
7517
|
-
}
|
|
7518
|
-
);
|
|
7519
|
-
const getFileInfo = tool4(
|
|
7520
|
-
async ({ path }) => {
|
|
7521
|
-
try {
|
|
7522
|
-
const resolvedPath = resolveSafePath(path, workspacesRoot);
|
|
7523
|
-
const fileStat = await stat2(resolvedPath);
|
|
7524
|
-
const info = [
|
|
7525
|
-
`Path: ${path}`,
|
|
7526
|
-
`Type: ${fileStat.isDirectory() ? "Directory" : "File"}`,
|
|
7527
|
-
`Size: ${formatSize(fileStat.size)}`,
|
|
7528
|
-
`Modified: ${fileStat.mtime.toISOString()}`,
|
|
7529
|
-
`Created: ${fileStat.birthtime.toISOString()}`
|
|
7530
|
-
];
|
|
7531
|
-
return info.join("\n");
|
|
7532
|
-
} catch (error) {
|
|
7533
|
-
return `Error getting file info: ${error instanceof Error ? error.message : String(error)}`;
|
|
7534
|
-
}
|
|
7535
|
-
},
|
|
7536
|
-
{
|
|
7537
|
-
name: "get_file_info",
|
|
7538
|
-
description: "Gets information about a file or directory (size, modified date, etc.).",
|
|
7539
|
-
schema: z6.object({
|
|
7540
|
-
path: z6.string().describe("Path to the file or directory")
|
|
7541
|
-
})
|
|
7542
|
-
}
|
|
7543
|
-
);
|
|
7544
|
-
return [listDirectory, readFileContent, getFileInfo];
|
|
7545
|
-
}
|
|
7546
|
-
function resolveSafePath(path, workspacesRoot) {
|
|
7547
|
-
const resolved = path.startsWith("/") ? path : join6(workspacesRoot, path);
|
|
7548
|
-
const normalized = resolve(resolved);
|
|
7549
|
-
const homeDir = process.env.HOME ?? "/";
|
|
7550
|
-
if (!normalized.startsWith(workspacesRoot) && !normalized.startsWith(homeDir)) {
|
|
7551
|
-
throw new Error(`Access denied: Path must be within ${workspacesRoot} or home directory`);
|
|
7552
|
-
}
|
|
7553
|
-
return normalized;
|
|
7554
|
-
}
|
|
7555
|
-
function formatSize(bytes) {
|
|
7556
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
7557
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
7558
|
-
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
7559
|
-
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
|
|
7560
|
-
}
|
|
7561
|
-
|
|
7562
|
-
// src/infrastructure/agents/supervisor/supervisor-agent.ts
|
|
7563
|
-
var SupervisorAgent = class extends EventEmitter4 {
|
|
7564
|
-
logger;
|
|
7565
|
-
agent;
|
|
7566
|
-
getMessageBroadcaster;
|
|
7567
|
-
getChatHistoryService;
|
|
7568
|
-
conversationHistory = [];
|
|
7569
|
-
abortController = null;
|
|
7570
|
-
isExecuting = false;
|
|
7571
|
-
isCancelled = false;
|
|
7572
|
-
/** Tracks if we're processing a command (including STT, before LLM execution) */
|
|
7573
|
-
isProcessingCommand = false;
|
|
7574
|
-
/** Timestamp when current execution started (for race condition protection) */
|
|
7575
|
-
executionStartedAt = 0;
|
|
7576
|
-
constructor(config2) {
|
|
7577
|
-
super();
|
|
7578
|
-
this.logger = config2.logger.child({ component: "SupervisorAgent" });
|
|
7579
|
-
this.getMessageBroadcaster = config2.getMessageBroadcaster;
|
|
7580
|
-
this.getChatHistoryService = config2.getChatHistoryService;
|
|
7581
|
-
const env = getEnv();
|
|
7582
|
-
const llm = this.createLLM(env);
|
|
7583
|
-
const terminateSessionCallback = async (sessionId) => {
|
|
7584
|
-
const terminate = config2.getTerminateSession?.();
|
|
7585
|
-
if (!terminate) {
|
|
7586
|
-
this.logger.warn("Terminate session callback not available");
|
|
7587
|
-
return false;
|
|
7588
|
-
}
|
|
7589
|
-
return terminate(sessionId);
|
|
7590
|
-
};
|
|
7591
|
-
const tools = [
|
|
7592
|
-
...createWorkspaceTools(config2.workspaceDiscovery),
|
|
7593
|
-
...createWorktreeTools(config2.workspaceDiscovery, config2.agentSessionManager),
|
|
7594
|
-
...createSessionTools(
|
|
7595
|
-
config2.sessionManager,
|
|
7596
|
-
config2.agentSessionManager,
|
|
7597
|
-
config2.workspaceDiscovery,
|
|
7598
|
-
config2.workspacesRoot,
|
|
7599
|
-
config2.getMessageBroadcaster,
|
|
7600
|
-
config2.getChatHistoryService,
|
|
7601
|
-
() => this.clearContext(),
|
|
7602
|
-
terminateSessionCallback
|
|
7603
|
-
),
|
|
7604
|
-
...createFilesystemTools(config2.workspacesRoot)
|
|
7605
|
-
];
|
|
7606
|
-
this.logger.info({ toolCount: tools.length }, "Creating Supervisor Agent with tools");
|
|
7607
|
-
this.agent = createReactAgent({
|
|
7608
|
-
llm,
|
|
7609
|
-
tools
|
|
7610
|
-
});
|
|
7611
|
-
}
|
|
7612
|
-
/**
|
|
7613
|
-
* Creates the LLM instance based on configuration.
|
|
7614
|
-
*/
|
|
7615
|
-
createLLM(env) {
|
|
7616
|
-
const provider = env.AGENT_PROVIDER;
|
|
7617
|
-
const apiKey = env.AGENT_API_KEY;
|
|
7618
|
-
const modelName = env.AGENT_MODEL_NAME;
|
|
7619
|
-
const baseUrl = env.AGENT_BASE_URL;
|
|
7620
|
-
const temperature = env.AGENT_TEMPERATURE;
|
|
7621
|
-
if (!apiKey) {
|
|
7622
|
-
throw new Error("AGENT_API_KEY is required for Supervisor Agent");
|
|
7623
|
-
}
|
|
7624
|
-
this.logger.info({ provider, model: modelName }, "Initializing LLM");
|
|
7625
|
-
return new ChatOpenAI({
|
|
7626
|
-
openAIApiKey: apiKey,
|
|
7627
|
-
modelName,
|
|
7628
|
-
temperature,
|
|
7629
|
-
configuration: baseUrl ? {
|
|
7630
|
-
baseURL: baseUrl
|
|
7631
|
-
} : void 0
|
|
7632
|
-
});
|
|
7633
|
-
}
|
|
7634
|
-
/**
|
|
7635
|
-
* Executes a command through the supervisor agent.
|
|
7636
|
-
* Note: deviceId is used for routing responses, not for history (history is global).
|
|
7637
|
-
*/
|
|
7638
|
-
async execute(command, deviceId, currentSessionId) {
|
|
7639
|
-
this.logger.info({ command, deviceId, currentSessionId }, "Executing supervisor command");
|
|
7640
|
-
try {
|
|
7641
|
-
const history = this.getConversationHistory();
|
|
7642
|
-
const messages2 = [
|
|
7643
|
-
...this.buildSystemMessage(),
|
|
7644
|
-
...this.buildHistoryMessages(history),
|
|
7645
|
-
new HumanMessage(command)
|
|
7646
|
-
];
|
|
7647
|
-
const result = await this.agent.invoke({
|
|
7648
|
-
messages: messages2
|
|
7649
|
-
});
|
|
7650
|
-
const agentMessages = result.messages;
|
|
7651
|
-
const lastMessage = agentMessages[agentMessages.length - 1];
|
|
7652
|
-
const content = lastMessage?.content;
|
|
7653
|
-
const output = typeof content === "string" ? content : JSON.stringify(content);
|
|
7654
|
-
this.addToHistory("user", command);
|
|
7655
|
-
this.addToHistory("assistant", output);
|
|
7656
|
-
this.logger.debug({ output: output.slice(0, 200) }, "Supervisor command completed");
|
|
7657
|
-
return {
|
|
7658
|
-
output,
|
|
7659
|
-
sessionId: currentSessionId
|
|
7660
|
-
};
|
|
7661
|
-
} catch (error) {
|
|
7662
|
-
this.logger.error({ error, command }, "Supervisor command failed");
|
|
7663
|
-
const errorMessage = error instanceof Error ? error.message : "An unexpected error occurred";
|
|
7664
|
-
return {
|
|
7665
|
-
output: `Error: ${errorMessage}`
|
|
7666
|
-
};
|
|
7667
|
-
}
|
|
7668
|
-
}
|
|
7669
|
-
/**
|
|
7670
|
-
* Executes a command with streaming output.
|
|
7671
|
-
* Emits 'blocks' events as content is generated.
|
|
7672
|
-
* Note: deviceId is used for routing responses, history is global.
|
|
7673
|
-
*/
|
|
7674
|
-
async executeWithStream(command, deviceId) {
|
|
7675
|
-
this.logger.info({ command, deviceId, wasCancelledBefore: this.isCancelled }, "Executing supervisor command with streaming");
|
|
7676
|
-
this.abortController = new AbortController();
|
|
7677
|
-
this.isExecuting = true;
|
|
7678
|
-
this.isCancelled = false;
|
|
7679
|
-
this.executionStartedAt = Date.now();
|
|
7680
|
-
this.logger.debug({ isCancelled: this.isCancelled, isExecuting: this.isExecuting }, "Flags reset for new execution");
|
|
7681
|
-
try {
|
|
7682
|
-
const history = this.getConversationHistory();
|
|
7683
|
-
const messages2 = [
|
|
7684
|
-
...this.buildSystemMessage(),
|
|
7685
|
-
...this.buildHistoryMessages(history),
|
|
7686
|
-
new HumanMessage(command)
|
|
7687
|
-
];
|
|
7688
|
-
if (this.isExecuting && !this.isCancelled) {
|
|
7689
|
-
const statusBlock = createStatusBlock("Processing...");
|
|
7690
|
-
this.logger.debug({ deviceId, blockType: "status" }, "Emitting status block");
|
|
7691
|
-
this.emit("blocks", deviceId, [statusBlock], false);
|
|
7692
|
-
}
|
|
7693
|
-
this.logger.info({ deviceId }, "Starting LangGraph agent stream");
|
|
7694
|
-
const stream = await this.agent.stream(
|
|
7695
|
-
{ messages: messages2 },
|
|
7696
|
-
{
|
|
7697
|
-
streamMode: "values",
|
|
7698
|
-
signal: this.abortController.signal
|
|
7699
|
-
}
|
|
7700
|
-
);
|
|
7701
|
-
let finalOutput = "";
|
|
7702
|
-
const allBlocks = [];
|
|
7703
|
-
for await (const chunk of stream) {
|
|
7704
|
-
if (this.isCancelled || !this.isExecuting) {
|
|
7705
|
-
this.logger.info({ deviceId, isCancelled: this.isCancelled, isExecuting: this.isExecuting }, "Supervisor execution cancelled, stopping stream processing");
|
|
7706
|
-
return;
|
|
7707
|
-
}
|
|
7708
|
-
const chunkData = chunk;
|
|
7709
|
-
const chunkMessages = chunkData.messages;
|
|
7710
|
-
if (!chunkMessages || chunkMessages.length === 0) continue;
|
|
7711
|
-
const lastMessage = chunkMessages[chunkMessages.length - 1];
|
|
7712
|
-
if (!lastMessage) continue;
|
|
7713
|
-
if (isAIMessage(lastMessage)) {
|
|
7714
|
-
const content = lastMessage.content;
|
|
7715
|
-
if (typeof content === "string" && content.length > 0) {
|
|
7716
|
-
finalOutput = content;
|
|
7717
|
-
if (this.isExecuting && !this.isCancelled) {
|
|
7718
|
-
const textBlock = createTextBlock(content);
|
|
7719
|
-
this.emit("blocks", deviceId, [textBlock], false);
|
|
7720
|
-
accumulateBlocks(allBlocks, [textBlock]);
|
|
7721
|
-
}
|
|
7722
|
-
} else if (Array.isArray(content)) {
|
|
7723
|
-
for (const item of content) {
|
|
7724
|
-
if (typeof item === "object" && this.isExecuting && !this.isCancelled) {
|
|
7725
|
-
const block = this.parseContentItem(item);
|
|
7726
|
-
if (block) {
|
|
7727
|
-
this.emit("blocks", deviceId, [block], false);
|
|
7728
|
-
accumulateBlocks(allBlocks, [block]);
|
|
7729
|
-
}
|
|
7730
|
-
}
|
|
7731
|
-
}
|
|
7732
|
-
}
|
|
7733
|
-
}
|
|
7734
|
-
if (lastMessage.getType() === "tool" && this.isExecuting && !this.isCancelled) {
|
|
7735
|
-
const toolContent = lastMessage.content;
|
|
7736
|
-
const toolName = lastMessage.name ?? "tool";
|
|
7737
|
-
const toolCallId = lastMessage.tool_call_id;
|
|
7738
|
-
const toolBlock = createToolBlock(
|
|
7739
|
-
toolName,
|
|
7740
|
-
"completed",
|
|
7741
|
-
void 0,
|
|
7742
|
-
typeof toolContent === "string" ? toolContent : JSON.stringify(toolContent),
|
|
7743
|
-
toolCallId
|
|
7744
|
-
);
|
|
7745
|
-
this.emit("blocks", deviceId, [toolBlock], false);
|
|
7746
|
-
accumulateBlocks(allBlocks, [toolBlock]);
|
|
9609
|
+
if (fileStat.size > 100 * 1024) {
|
|
9610
|
+
return `File is too large (${Math.round(fileStat.size / 1024)}KB). Maximum is 100KB.`;
|
|
7747
9611
|
}
|
|
9612
|
+
const content = await readFile2(resolvedPath, "utf-8");
|
|
9613
|
+
const lines = content.split("\n");
|
|
9614
|
+
if (maxLines && lines.length > maxLines) {
|
|
9615
|
+
return `File: ${path} (showing first ${maxLines} of ${lines.length} lines)
|
|
9616
|
+
|
|
9617
|
+
${lines.slice(0, maxLines).join("\n")}
|
|
9618
|
+
|
|
9619
|
+
... (${lines.length - maxLines} more lines)`;
|
|
9620
|
+
}
|
|
9621
|
+
return `File: ${path}
|
|
9622
|
+
|
|
9623
|
+
${content}`;
|
|
9624
|
+
} catch (error) {
|
|
9625
|
+
return `Error reading file: ${error instanceof Error ? error.message : String(error)}`;
|
|
7748
9626
|
}
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
}
|
|
7757
|
-
|
|
7758
|
-
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
9627
|
+
},
|
|
9628
|
+
{
|
|
9629
|
+
name: "read_file",
|
|
9630
|
+
description: "Reads the contents of a file. Limited to 100KB files for safety.",
|
|
9631
|
+
schema: z8.object({
|
|
9632
|
+
path: z8.string().describe("Path to the file"),
|
|
9633
|
+
maxLines: z8.number().optional().describe("Maximum number of lines to return")
|
|
9634
|
+
})
|
|
9635
|
+
}
|
|
9636
|
+
);
|
|
9637
|
+
const getFileInfo = tool5(
|
|
9638
|
+
async ({ path }) => {
|
|
9639
|
+
try {
|
|
9640
|
+
const resolvedPath = resolveSafePath(path, workspacesRoot);
|
|
9641
|
+
const fileStat = await stat2(resolvedPath);
|
|
9642
|
+
const info = [
|
|
9643
|
+
`Path: ${path}`,
|
|
9644
|
+
`Type: ${fileStat.isDirectory() ? "Directory" : "File"}`,
|
|
9645
|
+
`Size: ${formatSize(fileStat.size)}`,
|
|
9646
|
+
`Modified: ${fileStat.mtime.toISOString()}`,
|
|
9647
|
+
`Created: ${fileStat.birthtime.toISOString()}`
|
|
9648
|
+
];
|
|
9649
|
+
return info.join("\n");
|
|
9650
|
+
} catch (error) {
|
|
9651
|
+
return `Error getting file info: ${error instanceof Error ? error.message : String(error)}`;
|
|
7763
9652
|
}
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
9653
|
+
},
|
|
9654
|
+
{
|
|
9655
|
+
name: "get_file_info",
|
|
9656
|
+
description: "Gets information about a file or directory (size, modified date, etc.).",
|
|
9657
|
+
schema: z8.object({
|
|
9658
|
+
path: z8.string().describe("Path to the file or directory")
|
|
9659
|
+
})
|
|
7771
9660
|
}
|
|
9661
|
+
);
|
|
9662
|
+
return [listDirectory, readFileContent, getFileInfo];
|
|
9663
|
+
}
|
|
9664
|
+
function resolveSafePath(path, workspacesRoot) {
|
|
9665
|
+
const resolved = path.startsWith("/") ? path : join10(workspacesRoot, path);
|
|
9666
|
+
const normalized = resolve(resolved);
|
|
9667
|
+
const homeDir = process.env.HOME ?? "/";
|
|
9668
|
+
if (!normalized.startsWith(workspacesRoot) && !normalized.startsWith(homeDir)) {
|
|
9669
|
+
throw new Error(`Access denied: Path must be within ${workspacesRoot} or home directory`);
|
|
7772
9670
|
}
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
9671
|
+
return normalized;
|
|
9672
|
+
}
|
|
9673
|
+
function formatSize(bytes) {
|
|
9674
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
9675
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
9676
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
9677
|
+
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
|
|
9678
|
+
}
|
|
9679
|
+
|
|
9680
|
+
// src/infrastructure/agents/supervisor/tools/backlog-tools.ts
|
|
9681
|
+
import { join as join11 } from "path";
|
|
9682
|
+
import { existsSync as existsSync8 } from "fs";
|
|
9683
|
+
import { tool as tool6 } from "@langchain/core/tools";
|
|
9684
|
+
import { z as z9 } from "zod";
|
|
9685
|
+
function createBacklogTools(sessionManager, agentSessionManager, backlogManagers, workspacesRoot, getMessageBroadcaster, logger) {
|
|
9686
|
+
const createBacklogSession = tool6(
|
|
9687
|
+
async (args) => {
|
|
9688
|
+
const finalBacklogId = args.backlogId ?? `${args.project}-${Date.now()}`;
|
|
9689
|
+
let normalizedWorktree = args.worktree;
|
|
9690
|
+
if (normalizedWorktree && (normalizedWorktree.toLowerCase() === "main" || normalizedWorktree.toLowerCase() === "master")) {
|
|
9691
|
+
normalizedWorktree = void 0;
|
|
9692
|
+
}
|
|
9693
|
+
const root = workspacesRoot ?? "/workspaces";
|
|
9694
|
+
let workingDir;
|
|
9695
|
+
if (!normalizedWorktree) {
|
|
9696
|
+
workingDir = join11(root, args.workspace, args.project);
|
|
9697
|
+
} else {
|
|
9698
|
+
workingDir = join11(root, args.workspace, `${args.project}--${normalizedWorktree}`);
|
|
9699
|
+
}
|
|
9700
|
+
const workspacePath = new WorkspacePath(
|
|
9701
|
+
args.workspace,
|
|
9702
|
+
args.project,
|
|
9703
|
+
normalizedWorktree
|
|
9704
|
+
);
|
|
9705
|
+
if (!existsSync8(workingDir)) {
|
|
9706
|
+
const details = normalizedWorktree ? `The worktree/branch "${normalizedWorktree}" is checked out` : `The main/master branch is checked out`;
|
|
9707
|
+
return `\u274C ERROR: Project path does not exist: ${workingDir}
|
|
9708
|
+
|
|
9709
|
+
Please make sure:
|
|
9710
|
+
1. The workspace "${args.workspace}" exists
|
|
9711
|
+
2. The project "${args.project}" exists
|
|
9712
|
+
3. ${details}
|
|
9713
|
+
|
|
9714
|
+
Use list_worktrees to see available branches for the project.`;
|
|
9715
|
+
}
|
|
9716
|
+
const session = await sessionManager.createSession({
|
|
9717
|
+
sessionType: "backlog-agent",
|
|
9718
|
+
workspacePath,
|
|
9719
|
+
workingDir,
|
|
9720
|
+
backlogId: finalBacklogId
|
|
9721
|
+
});
|
|
9722
|
+
if (session.type !== "backlog-agent") {
|
|
9723
|
+
return `Failed to create backlog session`;
|
|
9724
|
+
}
|
|
9725
|
+
const backlogSession = session;
|
|
9726
|
+
const noopFn = () => {
|
|
9727
|
+
};
|
|
9728
|
+
const noopLogger = {
|
|
9729
|
+
level: "silent",
|
|
9730
|
+
debug: noopFn,
|
|
9731
|
+
info: noopFn,
|
|
9732
|
+
warn: noopFn,
|
|
9733
|
+
error: noopFn,
|
|
9734
|
+
fatal: noopFn,
|
|
9735
|
+
trace: noopFn,
|
|
9736
|
+
silent: noopFn,
|
|
9737
|
+
child: () => noopLogger
|
|
9738
|
+
};
|
|
9739
|
+
const backlogLogger = logger ?? noopLogger;
|
|
9740
|
+
const manager = BacklogAgentManager.createEmpty(
|
|
9741
|
+
backlogSession,
|
|
9742
|
+
workingDir,
|
|
9743
|
+
agentSessionManager,
|
|
9744
|
+
backlogLogger
|
|
9745
|
+
);
|
|
9746
|
+
backlogManagers.set(session.id.value, manager);
|
|
9747
|
+
const broadcaster = getMessageBroadcaster?.();
|
|
9748
|
+
if (broadcaster) {
|
|
9749
|
+
broadcaster.broadcastToAll(JSON.stringify({
|
|
9750
|
+
type: "session.created",
|
|
9751
|
+
session_id: session.id.value,
|
|
9752
|
+
payload: {
|
|
9753
|
+
session_type: "backlog-agent",
|
|
9754
|
+
workspace: args.workspace,
|
|
9755
|
+
project: args.project,
|
|
9756
|
+
worktree: normalizedWorktree,
|
|
9757
|
+
working_dir: workingDir
|
|
9758
|
+
}
|
|
9759
|
+
}));
|
|
9760
|
+
}
|
|
9761
|
+
return `\u2705 Created backlog session "${finalBacklogId}" at ${workingDir}
|
|
9762
|
+
Session ID: ${session.id.value}`;
|
|
9763
|
+
},
|
|
9764
|
+
{
|
|
9765
|
+
name: "create_backlog_session",
|
|
9766
|
+
description: "Create a new Backlog Agent session for autonomous development (uses system default model)",
|
|
9767
|
+
schema: z9.object({
|
|
9768
|
+
workspace: z9.string().describe("Workspace name"),
|
|
9769
|
+
project: z9.string().describe("Project name"),
|
|
9770
|
+
worktree: z9.string().optional().describe("Git worktree/branch name. Omit for main/master branch"),
|
|
9771
|
+
backlogId: z9.string().optional().describe("Custom backlog identifier")
|
|
9772
|
+
})
|
|
7780
9773
|
}
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
const
|
|
7785
|
-
|
|
9774
|
+
);
|
|
9775
|
+
const listBacklogSessions = tool6(
|
|
9776
|
+
() => {
|
|
9777
|
+
const sessions2 = Array.from(backlogManagers.entries()).map(
|
|
9778
|
+
([sessionId, manager]) => `- ${sessionId}: ${manager.getBacklog().id} (${manager.getSession().agentName})`
|
|
9779
|
+
).join("\n");
|
|
9780
|
+
if (sessions2.length === 0) {
|
|
9781
|
+
return "No active backlog sessions";
|
|
9782
|
+
}
|
|
9783
|
+
return `Active Backlog Sessions:
|
|
9784
|
+
${sessions2}`;
|
|
9785
|
+
},
|
|
9786
|
+
{
|
|
9787
|
+
name: "list_backlog_sessions",
|
|
9788
|
+
description: "List all active Backlog Agent sessions",
|
|
9789
|
+
schema: z9.object({})
|
|
7786
9790
|
}
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
|
|
9791
|
+
);
|
|
9792
|
+
const getBacklogStatus = tool6(
|
|
9793
|
+
(args) => {
|
|
9794
|
+
const manager = backlogManagers.get(args.sessionId);
|
|
9795
|
+
if (!manager) {
|
|
9796
|
+
return `Backlog session "${args.sessionId}" not found`;
|
|
9797
|
+
}
|
|
9798
|
+
const backlog = manager.getBacklog();
|
|
9799
|
+
const summary = backlog.summary ?? {
|
|
9800
|
+
total: backlog.tasks.length,
|
|
9801
|
+
completed: 0,
|
|
9802
|
+
failed: 0,
|
|
9803
|
+
in_progress: 0,
|
|
9804
|
+
pending: backlog.tasks.length
|
|
9805
|
+
};
|
|
9806
|
+
const percentage = summary.total > 0 ? summary.completed / summary.total * 100 : 0;
|
|
9807
|
+
const worktreeDisplay = backlog.worktree ?? "main";
|
|
9808
|
+
return `
|
|
9809
|
+
\u{1F4CA} Backlog: ${backlog.id}
|
|
9810
|
+
Progress: ${summary.completed}/${summary.total} (${percentage.toFixed(0)}%)
|
|
9811
|
+
- Completed: ${summary.completed}
|
|
9812
|
+
- In Progress: ${summary.in_progress}
|
|
9813
|
+
- Pending: ${summary.pending}
|
|
9814
|
+
- Failed: ${summary.failed}
|
|
9815
|
+
|
|
9816
|
+
Agent: ${manager.getSession().agentName}
|
|
9817
|
+
Worktree: ${worktreeDisplay}
|
|
9818
|
+
`.trim();
|
|
9819
|
+
},
|
|
9820
|
+
{
|
|
9821
|
+
name: "get_backlog_status",
|
|
9822
|
+
description: "Get status of a backlog session",
|
|
9823
|
+
schema: z9.object({
|
|
9824
|
+
sessionId: z9.string().describe("Backlog session ID")
|
|
9825
|
+
})
|
|
7800
9826
|
}
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
|
|
9827
|
+
);
|
|
9828
|
+
const addTaskToBacklog = tool6(
|
|
9829
|
+
async (args) => {
|
|
9830
|
+
const manager = backlogManagers.get(args.sessionId);
|
|
9831
|
+
if (!manager) {
|
|
9832
|
+
return `Backlog session "${args.sessionId}" not found`;
|
|
9833
|
+
}
|
|
9834
|
+
const blocks = await manager.executeCommand(
|
|
9835
|
+
`add task: "${args.title}" - ${args.description}${args.criteria ? ` (criteria: ${args.criteria})` : ""}`
|
|
7806
9836
|
);
|
|
7807
|
-
return
|
|
7808
|
-
}
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
"
|
|
7812
|
-
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
|
|
7817
|
-
|
|
9837
|
+
return blocks.map((b) => b.block_type === "text" ? b.content : "").join("\n");
|
|
9838
|
+
},
|
|
9839
|
+
{
|
|
9840
|
+
name: "add_task_to_backlog",
|
|
9841
|
+
description: "Add a task to a backlog",
|
|
9842
|
+
schema: z9.object({
|
|
9843
|
+
sessionId: z9.string().describe("Backlog session ID"),
|
|
9844
|
+
title: z9.string().describe("Task title"),
|
|
9845
|
+
description: z9.string().describe("Task description"),
|
|
9846
|
+
criteria: z9.string().optional().describe("Acceptance criteria")
|
|
9847
|
+
})
|
|
7818
9848
|
}
|
|
7819
|
-
|
|
7820
|
-
|
|
7821
|
-
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
7835
|
-
this.isCancelled = false;
|
|
7836
|
-
this.logger.debug("Started command processing");
|
|
7837
|
-
return this.abortController;
|
|
7838
|
-
}
|
|
7839
|
-
/**
|
|
7840
|
-
* Checks if command processing is active (STT or LLM execution).
|
|
7841
|
-
*/
|
|
7842
|
-
isProcessing() {
|
|
7843
|
-
return this.isProcessingCommand || this.isExecuting;
|
|
7844
|
-
}
|
|
7845
|
-
/**
|
|
7846
|
-
* Ends command processing (called after completion or error, not after cancel).
|
|
7847
|
-
*/
|
|
7848
|
-
endProcessing() {
|
|
7849
|
-
this.isProcessingCommand = false;
|
|
7850
|
-
this.logger.debug("Ended command processing");
|
|
7851
|
-
}
|
|
7852
|
-
/**
|
|
7853
|
-
* Clears global conversation history (in-memory only).
|
|
7854
|
-
* Also resets cancellation state to allow new commands.
|
|
7855
|
-
* @deprecated Use clearContext() for full context clearing with persistence and broadcast.
|
|
7856
|
-
*/
|
|
7857
|
-
clearHistory() {
|
|
7858
|
-
this.conversationHistory = [];
|
|
7859
|
-
this.isCancelled = false;
|
|
7860
|
-
this.logger.info("Global conversation history cleared");
|
|
7861
|
-
}
|
|
7862
|
-
/**
|
|
7863
|
-
* Clears supervisor context completely:
|
|
7864
|
-
* - In-memory conversation history
|
|
7865
|
-
* - Persistent history in database
|
|
7866
|
-
* - Notifies all connected clients
|
|
7867
|
-
*/
|
|
7868
|
-
clearContext() {
|
|
7869
|
-
this.conversationHistory = [];
|
|
7870
|
-
this.isCancelled = false;
|
|
7871
|
-
const chatHistoryService = this.getChatHistoryService?.();
|
|
7872
|
-
if (chatHistoryService) {
|
|
7873
|
-
chatHistoryService.clearSupervisorHistory();
|
|
9849
|
+
);
|
|
9850
|
+
const startBacklogHarness = tool6(
|
|
9851
|
+
async (args) => {
|
|
9852
|
+
const manager = backlogManagers.get(args.sessionId);
|
|
9853
|
+
if (!manager) {
|
|
9854
|
+
return `Backlog session "${args.sessionId}" not found`;
|
|
9855
|
+
}
|
|
9856
|
+
const blocks = await manager.executeCommand("start");
|
|
9857
|
+
return blocks.map((b) => b.block_type === "text" ? b.content : "").join("\n");
|
|
9858
|
+
},
|
|
9859
|
+
{
|
|
9860
|
+
name: "start_backlog_harness",
|
|
9861
|
+
description: "Start autonomous execution of a backlog",
|
|
9862
|
+
schema: z9.object({
|
|
9863
|
+
sessionId: z9.string().describe("Backlog session ID")
|
|
9864
|
+
})
|
|
7874
9865
|
}
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
9866
|
+
);
|
|
9867
|
+
const stopBacklogHarness = tool6(
|
|
9868
|
+
async (args) => {
|
|
9869
|
+
const manager = backlogManagers.get(args.sessionId);
|
|
9870
|
+
if (!manager) {
|
|
9871
|
+
return `Backlog session "${args.sessionId}" not found`;
|
|
9872
|
+
}
|
|
9873
|
+
const blocks = await manager.executeCommand("stop");
|
|
9874
|
+
return blocks.map((b) => b.block_type === "text" ? b.content : "").join("\n");
|
|
9875
|
+
},
|
|
9876
|
+
{
|
|
9877
|
+
name: "stop_backlog_harness",
|
|
9878
|
+
description: "Stop autonomous execution of a backlog",
|
|
9879
|
+
schema: z9.object({
|
|
9880
|
+
sessionId: z9.string().describe("Backlog session ID")
|
|
9881
|
+
})
|
|
7882
9882
|
}
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
}
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
9883
|
+
);
|
|
9884
|
+
return {
|
|
9885
|
+
createBacklogSession,
|
|
9886
|
+
listBacklogSessions,
|
|
9887
|
+
getBacklogStatus,
|
|
9888
|
+
addTaskToBacklog,
|
|
9889
|
+
startBacklogHarness,
|
|
9890
|
+
stopBacklogHarness
|
|
9891
|
+
};
|
|
9892
|
+
}
|
|
9893
|
+
|
|
9894
|
+
// src/infrastructure/agents/supervisor/supervisor-agent.ts
|
|
9895
|
+
var SupervisorAgent = class extends LangGraphAgent {
|
|
9896
|
+
getMessageBroadcaster;
|
|
9897
|
+
getChatHistoryService;
|
|
9898
|
+
sessionManager;
|
|
9899
|
+
agentSessionManager;
|
|
9900
|
+
workspaceDiscovery;
|
|
9901
|
+
workspacesRoot;
|
|
9902
|
+
getTerminateSession;
|
|
9903
|
+
constructor(config2) {
|
|
9904
|
+
super(config2.logger);
|
|
9905
|
+
this.getMessageBroadcaster = config2.getMessageBroadcaster;
|
|
9906
|
+
this.getChatHistoryService = config2.getChatHistoryService;
|
|
9907
|
+
this.sessionManager = config2.sessionManager;
|
|
9908
|
+
this.agentSessionManager = config2.agentSessionManager;
|
|
9909
|
+
this.workspaceDiscovery = config2.workspaceDiscovery;
|
|
9910
|
+
this.workspacesRoot = config2.workspacesRoot;
|
|
9911
|
+
this.getTerminateSession = config2.getTerminateSession;
|
|
9912
|
+
this.initializeAgent();
|
|
7900
9913
|
}
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
*/
|
|
7904
|
-
buildSystemMessage() {
|
|
7905
|
-
const systemPrompt = `## MANDATORY RULES (STRICTLY ENFORCED)
|
|
9914
|
+
buildSystemPrompt() {
|
|
9915
|
+
return `## MANDATORY RULES (STRICTLY ENFORCED)
|
|
7906
9916
|
|
|
7907
9917
|
You MUST always respond in English.
|
|
7908
9918
|
|
|
@@ -8047,6 +10057,59 @@ IMPORTANT: When \`list_worktrees\` shows a worktree named "main" with \`isMain:
|
|
|
8047
10057
|
|
|
8048
10058
|
---
|
|
8049
10059
|
|
|
10060
|
+
## BACKLOG SESSIONS (Autonomous Development)
|
|
10061
|
+
|
|
10062
|
+
Backlog sessions are special autonomous coding sessions that use the default system LLM model to execute a series of tasks.
|
|
10063
|
+
|
|
10064
|
+
### Creating Backlog Sessions (CRITICAL PATH VALIDATION):
|
|
10065
|
+
|
|
10066
|
+
IMPORTANT: You MUST validate the project path EXISTS before creating a backlog session!
|
|
10067
|
+
|
|
10068
|
+
Step 1: Call \`list_worktrees\` for the project to see all available branches/worktrees
|
|
10069
|
+
Step 2: PARSE THE OUTPUT CAREFULLY:
|
|
10070
|
+
- Output format: "Worktrees for 'workspace/project':
|
|
10071
|
+
- worktree-name: branch-name (/path/to/directory)"
|
|
10072
|
+
- Example: "- main: main (/Users/roman/tiflis-code-work/roman/eva)"
|
|
10073
|
+
- This means the main branch is at /Users/roman/tiflis-code-work/roman/eva (NO --main suffix!)
|
|
10074
|
+
Step 3: Determine the correct worktree parameter:
|
|
10075
|
+
- If worktree is "main" or "master" (the default/primary branch):
|
|
10076
|
+
* Check the path shown: /Users/roman/tiflis-code-work/roman/eva
|
|
10077
|
+
* The path has NO worktree suffix (no --main, no --master)
|
|
10078
|
+
* DO NOT PASS WORKTREE PARAMETER - omit it entirely
|
|
10079
|
+
- If worktree is a feature branch (e.g., "feature-auth"):
|
|
10080
|
+
* The path would be: /Users/roman/tiflis-code-work/roman/eva--feature-auth
|
|
10081
|
+
* PASS worktree="feature-auth" parameter
|
|
10082
|
+
Step 4: Call \`create_backlog_session\` with workspace, project, and worktree (only if non-main)
|
|
10083
|
+
Step 5: Confirm the session was created successfully
|
|
10084
|
+
|
|
10085
|
+
### Path Construction Rules (CRITICAL):
|
|
10086
|
+
- Worktree parameter controls the directory name pattern:
|
|
10087
|
+
* Omit worktree \u2192 uses: /workspaces/{workspace}/{project}
|
|
10088
|
+
* Pass worktree="feature-x" \u2192 uses: /workspaces/{workspace}/{project}--feature-x
|
|
10089
|
+
- NEVER pass worktree="main" or worktree="master" - just omit the parameter instead
|
|
10090
|
+
- ALWAYS check list_worktrees output to see actual paths
|
|
10091
|
+
- Only pass worktree when the branch name appears in the path AFTER the project name with -- separator
|
|
10092
|
+
|
|
10093
|
+
### Example Flows:
|
|
10094
|
+
|
|
10095
|
+
User: "Create a backlog for eva on the main branch"
|
|
10096
|
+
Step 1: Call list_worktrees(roman, eva)
|
|
10097
|
+
Step 2: Output shows: "- main: main (/Users/roman/tiflis-code-work/roman/eva)"
|
|
10098
|
+
\u2192 Path is /roman/eva (no --main suffix)
|
|
10099
|
+
\u2192 This is the main branch, omit worktree parameter
|
|
10100
|
+
Step 3: Call create_backlog_session(workspace="roman", project="eva") [NO worktree parameter!]
|
|
10101
|
+
Step 4: \u2705 Created at /Users/roman/tiflis-code-work/roman/eva
|
|
10102
|
+
|
|
10103
|
+
User: "Create a backlog for tiflis-code on the feature-auth branch"
|
|
10104
|
+
Step 1: Call list_worktrees(tiflis, tiflis-code)
|
|
10105
|
+
Step 2: Output shows: "- feature-auth: feature/auth (/Users/roman/tiflis-code-work/tiflis/tiflis-code--feature-auth)"
|
|
10106
|
+
\u2192 Path has --feature-auth suffix
|
|
10107
|
+
\u2192 This is a feature branch, pass worktree parameter
|
|
10108
|
+
Step 3: Call create_backlog_session(workspace="tiflis", project="tiflis-code", worktree="feature-auth")
|
|
10109
|
+
Step 4: \u2705 Created at /Users/roman/tiflis-code-work/tiflis/tiflis-code--feature-auth
|
|
10110
|
+
|
|
10111
|
+
---
|
|
10112
|
+
|
|
8050
10113
|
## WORKTREE MANAGEMENT
|
|
8051
10114
|
|
|
8052
10115
|
Worktrees allow working on multiple branches simultaneously in separate directories.
|
|
@@ -8075,45 +10138,117 @@ Creating worktrees with \`create_worktree\`:
|
|
|
8075
10138
|
- ALWAYS use bullet lists or numbered lists instead of tables
|
|
8076
10139
|
- Keep list items short and scannable for mobile reading
|
|
8077
10140
|
- ALWAYS prioritize safety - check before deleting/merging`;
|
|
8078
|
-
return [new HumanMessage(`[System Instructions]
|
|
8079
|
-
${systemPrompt}
|
|
8080
|
-
[End Instructions]`)];
|
|
8081
10141
|
}
|
|
8082
10142
|
/**
|
|
8083
|
-
*
|
|
10143
|
+
* Implements abstract method: create tools for Supervisor.
|
|
10144
|
+
*/
|
|
10145
|
+
createTools() {
|
|
10146
|
+
const terminateSessionCallback = async (sessionId) => {
|
|
10147
|
+
const terminate = this.getTerminateSession?.();
|
|
10148
|
+
if (!terminate) {
|
|
10149
|
+
this.logger.warn("Terminate session callback not available");
|
|
10150
|
+
return false;
|
|
10151
|
+
}
|
|
10152
|
+
return terminate(sessionId);
|
|
10153
|
+
};
|
|
10154
|
+
return [
|
|
10155
|
+
...createWorkspaceTools(this.workspaceDiscovery),
|
|
10156
|
+
...createWorktreeTools(this.workspaceDiscovery, this.agentSessionManager),
|
|
10157
|
+
...createSessionTools(
|
|
10158
|
+
this.sessionManager,
|
|
10159
|
+
this.agentSessionManager,
|
|
10160
|
+
this.workspaceDiscovery,
|
|
10161
|
+
this.workspacesRoot,
|
|
10162
|
+
this.getMessageBroadcaster,
|
|
10163
|
+
this.getChatHistoryService,
|
|
10164
|
+
() => this.clearContext(),
|
|
10165
|
+
terminateSessionCallback
|
|
10166
|
+
),
|
|
10167
|
+
...createFilesystemTools(this.workspacesRoot),
|
|
10168
|
+
...Object.values(createBacklogTools(
|
|
10169
|
+
this.sessionManager,
|
|
10170
|
+
this.agentSessionManager,
|
|
10171
|
+
this.sessionManager.getBacklogManagers?.() ?? /* @__PURE__ */ new Map(),
|
|
10172
|
+
this.workspacesRoot,
|
|
10173
|
+
this.getMessageBroadcaster,
|
|
10174
|
+
this.logger
|
|
10175
|
+
))
|
|
10176
|
+
];
|
|
10177
|
+
}
|
|
10178
|
+
/**
|
|
10179
|
+
* Implements abstract method: create state manager for Supervisor.
|
|
8084
10180
|
*/
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
10181
|
+
createStateManager() {
|
|
10182
|
+
const chatHistoryService = this.getChatHistoryService?.();
|
|
10183
|
+
if (!chatHistoryService) {
|
|
10184
|
+
throw new Error("ChatHistoryService is required for SupervisorAgent");
|
|
10185
|
+
}
|
|
10186
|
+
return new SupervisorStateManager(chatHistoryService);
|
|
8089
10187
|
}
|
|
8090
10188
|
/**
|
|
8091
|
-
*
|
|
10189
|
+
* Clears supervisor context completely:
|
|
10190
|
+
* - In-memory conversation history
|
|
10191
|
+
* - Persistent history in database
|
|
10192
|
+
* - Notifies all connected clients
|
|
8092
10193
|
*/
|
|
8093
|
-
|
|
8094
|
-
|
|
10194
|
+
clearContext() {
|
|
10195
|
+
this.conversationHistory = [];
|
|
10196
|
+
this.isCancelled = false;
|
|
10197
|
+
const chatHistoryService = this.getChatHistoryService?.();
|
|
10198
|
+
if (chatHistoryService) {
|
|
10199
|
+
chatHistoryService.clearSupervisorHistory();
|
|
10200
|
+
}
|
|
10201
|
+
const broadcaster = this.getMessageBroadcaster?.();
|
|
10202
|
+
if (broadcaster) {
|
|
10203
|
+
const clearNotification = JSON.stringify({
|
|
10204
|
+
type: "supervisor.context_cleared",
|
|
10205
|
+
payload: { timestamp: Date.now() }
|
|
10206
|
+
});
|
|
10207
|
+
broadcaster.broadcastToAll(clearNotification);
|
|
10208
|
+
}
|
|
10209
|
+
this.logger.info("Supervisor context cleared (in-memory, persistent, and clients notified)");
|
|
8095
10210
|
}
|
|
8096
10211
|
/**
|
|
8097
|
-
*
|
|
10212
|
+
* Synchronous execute method for ISupervisorAgent interface compatibility.
|
|
10213
|
+
* Internally calls executeWithStream and returns a result.
|
|
8098
10214
|
*/
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
|
|
10215
|
+
async execute(command, deviceId, _currentSessionId) {
|
|
10216
|
+
let output = "";
|
|
10217
|
+
const blockHandler = (_deviceId, _blocks, isComplete, finalOutput) => {
|
|
10218
|
+
if (isComplete && finalOutput) {
|
|
10219
|
+
output = finalOutput;
|
|
10220
|
+
}
|
|
10221
|
+
};
|
|
10222
|
+
this.on("blocks", blockHandler);
|
|
10223
|
+
try {
|
|
10224
|
+
await this.executeWithStream(command, deviceId);
|
|
10225
|
+
} finally {
|
|
10226
|
+
this.removeListener("blocks", blockHandler);
|
|
8103
10227
|
}
|
|
10228
|
+
return { output };
|
|
10229
|
+
}
|
|
10230
|
+
clearHistory() {
|
|
10231
|
+
this.clearContext();
|
|
10232
|
+
}
|
|
10233
|
+
/**
|
|
10234
|
+
* Records acknowledgment of context clear from a device.
|
|
10235
|
+
* Used for multi-device synchronization.
|
|
10236
|
+
*/
|
|
10237
|
+
recordClearAck(_broadcastId, _deviceId) {
|
|
10238
|
+
this.logger.debug({ _broadcastId, _deviceId }, "Context clear acknowledgment received");
|
|
8104
10239
|
}
|
|
8105
10240
|
};
|
|
8106
10241
|
|
|
8107
10242
|
// src/infrastructure/mock/mock-supervisor-agent.ts
|
|
8108
|
-
import { EventEmitter as
|
|
10243
|
+
import { EventEmitter as EventEmitter7 } from "events";
|
|
8109
10244
|
|
|
8110
10245
|
// src/infrastructure/mock/fixture-loader.ts
|
|
8111
|
-
import { readFileSync as
|
|
8112
|
-
import { join as
|
|
10246
|
+
import { readFileSync as readFileSync6, existsSync as existsSync9 } from "fs";
|
|
10247
|
+
import { join as join12, dirname as dirname2 } from "path";
|
|
8113
10248
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8114
10249
|
var __filename = fileURLToPath2(import.meta.url);
|
|
8115
10250
|
var __dirname = dirname2(__filename);
|
|
8116
|
-
var DEFAULT_FIXTURES_PATH =
|
|
10251
|
+
var DEFAULT_FIXTURES_PATH = join12(__dirname, "fixtures");
|
|
8117
10252
|
var fixtureCache = /* @__PURE__ */ new Map();
|
|
8118
10253
|
function loadFixture(name, customPath) {
|
|
8119
10254
|
const cacheKey = `${customPath ?? "default"}:${name}`;
|
|
@@ -8122,13 +10257,13 @@ function loadFixture(name, customPath) {
|
|
|
8122
10257
|
return cached;
|
|
8123
10258
|
}
|
|
8124
10259
|
const fixturesDir = customPath ?? DEFAULT_FIXTURES_PATH;
|
|
8125
|
-
const filePath =
|
|
8126
|
-
if (!
|
|
10260
|
+
const filePath = join12(fixturesDir, `${name}.json`);
|
|
10261
|
+
if (!existsSync9(filePath)) {
|
|
8127
10262
|
console.warn(`[MockMode] Fixture not found: ${filePath}`);
|
|
8128
10263
|
return null;
|
|
8129
10264
|
}
|
|
8130
10265
|
try {
|
|
8131
|
-
const content =
|
|
10266
|
+
const content = readFileSync6(filePath, "utf-8");
|
|
8132
10267
|
const fixture = JSON.parse(content);
|
|
8133
10268
|
fixtureCache.set(cacheKey, fixture);
|
|
8134
10269
|
return fixture;
|
|
@@ -8196,7 +10331,7 @@ function sleep(ms) {
|
|
|
8196
10331
|
}
|
|
8197
10332
|
|
|
8198
10333
|
// src/infrastructure/mock/mock-supervisor-agent.ts
|
|
8199
|
-
var MockSupervisorAgent = class extends
|
|
10334
|
+
var MockSupervisorAgent = class extends EventEmitter7 {
|
|
8200
10335
|
logger;
|
|
8201
10336
|
fixturesPath;
|
|
8202
10337
|
fixture;
|
|
@@ -8375,18 +10510,23 @@ var MockSupervisorAgent = class extends EventEmitter5 {
|
|
|
8375
10510
|
getConversationHistory() {
|
|
8376
10511
|
return [...this.conversationHistory];
|
|
8377
10512
|
}
|
|
8378
|
-
|
|
8379
|
-
|
|
8380
|
-
|
|
10513
|
+
clearContext() {
|
|
10514
|
+
this.conversationHistory = [];
|
|
10515
|
+
this.isCancelled = false;
|
|
10516
|
+
this.logger.info("Mock context cleared");
|
|
10517
|
+
}
|
|
10518
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
10519
|
+
recordClearAck(_broadcastId, _deviceId) {
|
|
10520
|
+
}
|
|
8381
10521
|
sleep(ms) {
|
|
8382
10522
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
8383
10523
|
}
|
|
8384
10524
|
};
|
|
8385
10525
|
|
|
8386
10526
|
// src/infrastructure/mock/mock-agent-session-manager.ts
|
|
8387
|
-
import { EventEmitter as
|
|
10527
|
+
import { EventEmitter as EventEmitter8 } from "events";
|
|
8388
10528
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
8389
|
-
var MockAgentSessionManager = class extends
|
|
10529
|
+
var MockAgentSessionManager = class extends EventEmitter8 {
|
|
8390
10530
|
sessions = /* @__PURE__ */ new Map();
|
|
8391
10531
|
fixtures = /* @__PURE__ */ new Map();
|
|
8392
10532
|
logger;
|
|
@@ -8607,7 +10747,7 @@ var MockAgentSessionManager = class extends EventEmitter6 {
|
|
|
8607
10747
|
|
|
8608
10748
|
// src/infrastructure/speech/stt-service.ts
|
|
8609
10749
|
import { writeFile as writeFile2, unlink as unlink2 } from "fs/promises";
|
|
8610
|
-
import { join as
|
|
10750
|
+
import { join as join13 } from "path";
|
|
8611
10751
|
import { tmpdir } from "os";
|
|
8612
10752
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
8613
10753
|
var STTService = class {
|
|
@@ -8661,7 +10801,7 @@ var STTService = class {
|
|
|
8661
10801
|
async transcribeOpenAI(audioBuffer, format, signal) {
|
|
8662
10802
|
const baseUrl = this.config.baseUrl ?? "https://api.openai.com/v1";
|
|
8663
10803
|
const endpoint = `${baseUrl}/audio/transcriptions`;
|
|
8664
|
-
const tempPath =
|
|
10804
|
+
const tempPath = join13(tmpdir(), `stt-${randomUUID4()}.${format}`);
|
|
8665
10805
|
try {
|
|
8666
10806
|
if (signal?.aborted) {
|
|
8667
10807
|
throw new Error("Transcription cancelled");
|
|
@@ -8709,7 +10849,7 @@ var STTService = class {
|
|
|
8709
10849
|
async transcribeElevenLabs(audioBuffer, format, signal) {
|
|
8710
10850
|
const baseUrl = this.config.baseUrl ?? "https://api.elevenlabs.io/v1";
|
|
8711
10851
|
const endpoint = `${baseUrl}/speech-to-text`;
|
|
8712
|
-
const tempPath =
|
|
10852
|
+
const tempPath = join13(tmpdir(), `stt-${randomUUID4()}.${format}`);
|
|
8713
10853
|
try {
|
|
8714
10854
|
if (signal?.aborted) {
|
|
8715
10855
|
throw new Error("Transcription cancelled");
|
|
@@ -9088,6 +11228,27 @@ function createSummarizationService(env, logger) {
|
|
|
9088
11228
|
return new SummarizationService(config2, logger);
|
|
9089
11229
|
}
|
|
9090
11230
|
|
|
11231
|
+
// src/domain/value-objects/chat-message.ts
|
|
11232
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
11233
|
+
function createChatMessage(type, content, metadata) {
|
|
11234
|
+
return {
|
|
11235
|
+
id: randomUUID5(),
|
|
11236
|
+
timestamp: Date.now(),
|
|
11237
|
+
type,
|
|
11238
|
+
content,
|
|
11239
|
+
metadata
|
|
11240
|
+
};
|
|
11241
|
+
}
|
|
11242
|
+
function createUserMessage(content) {
|
|
11243
|
+
return createChatMessage("user", content);
|
|
11244
|
+
}
|
|
11245
|
+
function createAssistantMessage(content, metadata) {
|
|
11246
|
+
return createChatMessage("assistant", content, metadata);
|
|
11247
|
+
}
|
|
11248
|
+
function createErrorMessage(content, metadata) {
|
|
11249
|
+
return createChatMessage("error", content, metadata);
|
|
11250
|
+
}
|
|
11251
|
+
|
|
9091
11252
|
// src/main.ts
|
|
9092
11253
|
function printBanner(version) {
|
|
9093
11254
|
const dim = "\x1B[2m";
|
|
@@ -9181,7 +11342,7 @@ function handleTunnelMessage(rawMessage, tunnelClient, messageBroadcaster, creat
|
|
|
9181
11342
|
);
|
|
9182
11343
|
}
|
|
9183
11344
|
}
|
|
9184
|
-
function handleAuthMessageViaTunnel(data, tunnelClient, authenticateClient, logger, subscriptionService) {
|
|
11345
|
+
function handleAuthMessageViaTunnel(data, tunnelClient, authenticateClient, logger, subscriptionService, messageBroadcaster) {
|
|
9185
11346
|
const authResult = AuthMessageSchema.safeParse(data);
|
|
9186
11347
|
if (!authResult.success) {
|
|
9187
11348
|
logger.warn(
|
|
@@ -9211,6 +11372,32 @@ function handleAuthMessageViaTunnel(data, tunnelClient, authenticateClient, logg
|
|
|
9211
11372
|
);
|
|
9212
11373
|
}
|
|
9213
11374
|
}
|
|
11375
|
+
if (messageBroadcaster) {
|
|
11376
|
+
const bufferedMessages = messageBroadcaster.flushAuthBuffer(deviceId);
|
|
11377
|
+
if (bufferedMessages.length > 0) {
|
|
11378
|
+
logger.info(
|
|
11379
|
+
{ deviceId, messageCount: bufferedMessages.length },
|
|
11380
|
+
"Flushing buffered messages after subscription restore"
|
|
11381
|
+
);
|
|
11382
|
+
(async () => {
|
|
11383
|
+
for (const bufferedMsg of bufferedMessages) {
|
|
11384
|
+
try {
|
|
11385
|
+
await messageBroadcaster.broadcastToSubscribers(
|
|
11386
|
+
bufferedMsg.sessionId,
|
|
11387
|
+
bufferedMsg.message
|
|
11388
|
+
);
|
|
11389
|
+
} catch (error) {
|
|
11390
|
+
logger.error(
|
|
11391
|
+
{ error, deviceId, sessionId: bufferedMsg.sessionId },
|
|
11392
|
+
"Failed to send buffered message"
|
|
11393
|
+
);
|
|
11394
|
+
}
|
|
11395
|
+
}
|
|
11396
|
+
})().catch((error) => {
|
|
11397
|
+
logger.error({ error, deviceId }, "Error flushing buffered messages");
|
|
11398
|
+
});
|
|
11399
|
+
}
|
|
11400
|
+
}
|
|
9214
11401
|
const responseJson = JSON.stringify(result);
|
|
9215
11402
|
if (tunnelClient.send(responseJson)) {
|
|
9216
11403
|
logger.info(
|
|
@@ -9292,6 +11479,7 @@ async function bootstrap() {
|
|
|
9292
11479
|
chatHistoryService.ensureSupervisorSession();
|
|
9293
11480
|
const workstationMetadataRepository = new WorkstationMetadataRepository();
|
|
9294
11481
|
const subscriptionRepository = new SubscriptionRepository();
|
|
11482
|
+
const sessionRepository = new SessionRepository();
|
|
9295
11483
|
const clientRegistry = new InMemoryClientRegistry(logger);
|
|
9296
11484
|
const workspaceDiscovery = new FileSystemWorkspaceDiscovery({
|
|
9297
11485
|
workspacesRoot: env.WORKSPACES_ROOT
|
|
@@ -9308,8 +11496,19 @@ async function bootstrap() {
|
|
|
9308
11496
|
ptyManager,
|
|
9309
11497
|
agentSessionManager,
|
|
9310
11498
|
workspacesRoot: env.WORKSPACES_ROOT,
|
|
9311
|
-
logger
|
|
11499
|
+
logger,
|
|
11500
|
+
sessionRepository
|
|
9312
11501
|
});
|
|
11502
|
+
try {
|
|
11503
|
+
logger.info("About to call sessionManager.restoreSessions()");
|
|
11504
|
+
await sessionManager.restoreSessions();
|
|
11505
|
+
logger.info("sessionManager.restoreSessions() completed successfully");
|
|
11506
|
+
} catch (error) {
|
|
11507
|
+
logger.error(
|
|
11508
|
+
{ error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : void 0 },
|
|
11509
|
+
"Failed to restore sessions from database"
|
|
11510
|
+
);
|
|
11511
|
+
}
|
|
9313
11512
|
if (env.MOCK_MODE) {
|
|
9314
11513
|
const mockAgentManager = agentSessionManager;
|
|
9315
11514
|
await sessionManager.createSession({
|
|
@@ -9400,7 +11599,7 @@ async function bootstrap() {
|
|
|
9400
11599
|
},
|
|
9401
11600
|
accumulate(newBlocks) {
|
|
9402
11601
|
if (this.blocks.length === 0 && newBlocks.length > 0) {
|
|
9403
|
-
this.streamingMessageId =
|
|
11602
|
+
this.streamingMessageId = randomUUID6();
|
|
9404
11603
|
}
|
|
9405
11604
|
accumulateBlocks(this.blocks, newBlocks);
|
|
9406
11605
|
}
|
|
@@ -9449,7 +11648,7 @@ async function bootstrap() {
|
|
|
9449
11648
|
}
|
|
9450
11649
|
};
|
|
9451
11650
|
const createMessageHandlers = () => ({
|
|
9452
|
-
auth: (socket, message) => {
|
|
11651
|
+
auth: async (socket, message) => {
|
|
9453
11652
|
const authMessage = message;
|
|
9454
11653
|
const deviceId = authMessage.payload.device_id;
|
|
9455
11654
|
const result = authenticateClient.execute({
|
|
@@ -9467,6 +11666,21 @@ async function bootstrap() {
|
|
|
9467
11666
|
);
|
|
9468
11667
|
}
|
|
9469
11668
|
}
|
|
11669
|
+
if (broadcaster) {
|
|
11670
|
+
const bufferedMessages = broadcaster.flushAuthBuffer(deviceId);
|
|
11671
|
+
if (bufferedMessages.length > 0) {
|
|
11672
|
+
logger.info(
|
|
11673
|
+
{ deviceId, messageCount: bufferedMessages.length },
|
|
11674
|
+
"Flushing buffered messages after subscription restore"
|
|
11675
|
+
);
|
|
11676
|
+
for (const bufferedMsg of bufferedMessages) {
|
|
11677
|
+
await broadcaster.broadcastToSubscribers(
|
|
11678
|
+
bufferedMsg.sessionId,
|
|
11679
|
+
bufferedMsg.message
|
|
11680
|
+
);
|
|
11681
|
+
}
|
|
11682
|
+
}
|
|
11683
|
+
}
|
|
9470
11684
|
sendToDevice(socket, deviceId, JSON.stringify(result));
|
|
9471
11685
|
return Promise.resolve();
|
|
9472
11686
|
},
|
|
@@ -9500,7 +11714,14 @@ async function bootstrap() {
|
|
|
9500
11714
|
const inMemorySessions = sessionManager.getSessionInfos();
|
|
9501
11715
|
const persistedAgentSessions = chatHistoryService.getActiveAgentSessions();
|
|
9502
11716
|
logger.debug(
|
|
9503
|
-
{
|
|
11717
|
+
{
|
|
11718
|
+
inMemoryCount: inMemorySessions.length,
|
|
11719
|
+
inMemorySessionTypes: inMemorySessions.map((s) => ({
|
|
11720
|
+
id: s.session_id,
|
|
11721
|
+
type: s.session_type
|
|
11722
|
+
})),
|
|
11723
|
+
persistedAgentCount: persistedAgentSessions.length
|
|
11724
|
+
},
|
|
9504
11725
|
"Sync: fetched sessions"
|
|
9505
11726
|
);
|
|
9506
11727
|
const inMemorySessionIds = new Set(
|
|
@@ -9866,7 +12087,7 @@ async function bootstrap() {
|
|
|
9866
12087
|
logger.info({ wasCancelled }, "supervisorAgent.cancel() returned");
|
|
9867
12088
|
if (messageBroadcaster) {
|
|
9868
12089
|
const cancelBlock = {
|
|
9869
|
-
id:
|
|
12090
|
+
id: randomUUID6(),
|
|
9870
12091
|
block_type: "cancel",
|
|
9871
12092
|
content: "Cancelled by user"
|
|
9872
12093
|
};
|
|
@@ -9911,7 +12132,7 @@ async function bootstrap() {
|
|
|
9911
12132
|
return Promise.resolve();
|
|
9912
12133
|
},
|
|
9913
12134
|
// Clear supervisor conversation history (global)
|
|
9914
|
-
"supervisor.clear_context": (socket, message) => {
|
|
12135
|
+
"supervisor.clear_context": async (socket, message) => {
|
|
9915
12136
|
const clearMessage = message;
|
|
9916
12137
|
const directClient = clientRegistry.getBySocket(socket);
|
|
9917
12138
|
const tunnelClient2 = clearMessage.device_id ? clientRegistry.getByDeviceId(new DeviceId(clearMessage.device_id)) : void 0;
|
|
@@ -10034,18 +12255,21 @@ async function bootstrap() {
|
|
|
10034
12255
|
const sessionId = subscribeMessage.session_id;
|
|
10035
12256
|
const agentSession = agentSessionManager.getSession(sessionId);
|
|
10036
12257
|
const isPersistedAgent = !agentSession && chatHistoryService.getActiveAgentSessions().some((s) => s.sessionId === sessionId);
|
|
10037
|
-
|
|
12258
|
+
const backlogManagers = sessionManager.getBacklogManagers();
|
|
12259
|
+
const isBacklogSession = backlogManagers.has(sessionId);
|
|
12260
|
+
if (agentSession || isPersistedAgent || isBacklogSession) {
|
|
10038
12261
|
client.subscribe(new SessionId(sessionId));
|
|
10039
12262
|
logger.info(
|
|
10040
12263
|
{
|
|
10041
12264
|
deviceId: client.deviceId.value,
|
|
10042
12265
|
sessionId,
|
|
12266
|
+
sessionType: isBacklogSession ? "backlog-agent" : "agent",
|
|
10043
12267
|
allSubscriptions: client.getSubscriptions(),
|
|
10044
12268
|
clientStatus: client.status
|
|
10045
12269
|
},
|
|
10046
|
-
"Client subscribed to
|
|
12270
|
+
"Client subscribed to session"
|
|
10047
12271
|
);
|
|
10048
|
-
const isExecuting = agentSessionManager.isExecuting(sessionId);
|
|
12272
|
+
const isExecuting = isBacklogSession ? false : agentSessionManager.isExecuting(sessionId);
|
|
10049
12273
|
const currentStreamingBlocks = agentMessageAccumulator.get(sessionId) ?? [];
|
|
10050
12274
|
const streamingMessageId = currentStreamingBlocks.length > 0 ? agentStreamingMessageIds.get(sessionId) : void 0;
|
|
10051
12275
|
sendToDevice(
|
|
@@ -10063,11 +12287,12 @@ async function bootstrap() {
|
|
|
10063
12287
|
{
|
|
10064
12288
|
deviceId: client.deviceId.value,
|
|
10065
12289
|
sessionId,
|
|
12290
|
+
sessionType: isBacklogSession ? "backlog-agent" : "agent",
|
|
10066
12291
|
isExecuting,
|
|
10067
12292
|
streamingBlocksCount: currentStreamingBlocks.length,
|
|
10068
12293
|
streamingMessageId
|
|
10069
12294
|
},
|
|
10070
|
-
"
|
|
12295
|
+
"Session subscribed (use history.request for messages)"
|
|
10071
12296
|
);
|
|
10072
12297
|
} else {
|
|
10073
12298
|
const result = subscriptionService.subscribe(
|
|
@@ -10100,21 +12325,42 @@ async function bootstrap() {
|
|
|
10100
12325
|
return Promise.resolve();
|
|
10101
12326
|
},
|
|
10102
12327
|
"session.unsubscribe": (socket, message) => {
|
|
10103
|
-
if (!subscriptionService) return Promise.resolve();
|
|
10104
12328
|
const unsubscribeMessage = message;
|
|
10105
12329
|
const client = clientRegistry.getBySocket(socket) ?? (unsubscribeMessage.device_id ? clientRegistry.getByDeviceId(
|
|
10106
12330
|
new DeviceId(unsubscribeMessage.device_id)
|
|
10107
12331
|
) : void 0);
|
|
10108
12332
|
if (client?.isAuthenticated) {
|
|
10109
|
-
const
|
|
10110
|
-
|
|
10111
|
-
|
|
10112
|
-
)
|
|
10113
|
-
|
|
10114
|
-
|
|
10115
|
-
|
|
10116
|
-
|
|
10117
|
-
|
|
12333
|
+
const sessionId = unsubscribeMessage.session_id;
|
|
12334
|
+
const backlogManagers = sessionManager.getBacklogManagers();
|
|
12335
|
+
const isBacklogSession = backlogManagers.has(sessionId);
|
|
12336
|
+
if (isBacklogSession) {
|
|
12337
|
+
client.unsubscribe(new SessionId(sessionId));
|
|
12338
|
+
logger.info(
|
|
12339
|
+
{
|
|
12340
|
+
deviceId: client.deviceId.value,
|
|
12341
|
+
sessionId
|
|
12342
|
+
},
|
|
12343
|
+
"Client unsubscribed from backlog-agent session"
|
|
12344
|
+
);
|
|
12345
|
+
sendToDevice(
|
|
12346
|
+
socket,
|
|
12347
|
+
unsubscribeMessage.device_id,
|
|
12348
|
+
JSON.stringify({
|
|
12349
|
+
type: "session.unsubscribed",
|
|
12350
|
+
session_id: sessionId
|
|
12351
|
+
})
|
|
12352
|
+
);
|
|
12353
|
+
} else if (subscriptionService) {
|
|
12354
|
+
const result = subscriptionService.unsubscribe(
|
|
12355
|
+
client.deviceId.value,
|
|
12356
|
+
sessionId
|
|
12357
|
+
);
|
|
12358
|
+
sendToDevice(
|
|
12359
|
+
socket,
|
|
12360
|
+
unsubscribeMessage.device_id,
|
|
12361
|
+
JSON.stringify(result)
|
|
12362
|
+
);
|
|
12363
|
+
}
|
|
10118
12364
|
}
|
|
10119
12365
|
return Promise.resolve();
|
|
10120
12366
|
},
|
|
@@ -10140,6 +12386,153 @@ async function bootstrap() {
|
|
|
10140
12386
|
);
|
|
10141
12387
|
}
|
|
10142
12388
|
cancelledDuringTranscription.delete(sessionId);
|
|
12389
|
+
const backlogManagers = sessionManager.getBacklogManagers();
|
|
12390
|
+
const isBacklogSession = backlogManagers.has(sessionId);
|
|
12391
|
+
if (isBacklogSession) {
|
|
12392
|
+
const manager = backlogManagers.get(sessionId);
|
|
12393
|
+
if (!manager) {
|
|
12394
|
+
logger.error(
|
|
12395
|
+
{ sessionId },
|
|
12396
|
+
"Backlog manager found in map but is undefined - this should not happen"
|
|
12397
|
+
);
|
|
12398
|
+
const errorMessage = {
|
|
12399
|
+
type: "session.output",
|
|
12400
|
+
session_id: sessionId,
|
|
12401
|
+
payload: {
|
|
12402
|
+
content_blocks: [
|
|
12403
|
+
{
|
|
12404
|
+
id: "error",
|
|
12405
|
+
blockType: "error",
|
|
12406
|
+
content: "Internal error: backlog manager not properly initialized"
|
|
12407
|
+
}
|
|
12408
|
+
],
|
|
12409
|
+
content: "Internal error: backlog manager not properly initialized",
|
|
12410
|
+
content_type: "agent",
|
|
12411
|
+
is_complete: true,
|
|
12412
|
+
timestamp: Date.now(),
|
|
12413
|
+
message_id: messageId
|
|
12414
|
+
}
|
|
12415
|
+
};
|
|
12416
|
+
if (messageBroadcaster) {
|
|
12417
|
+
void messageBroadcaster.broadcastToSubscribers(
|
|
12418
|
+
sessionId,
|
|
12419
|
+
JSON.stringify(errorMessage)
|
|
12420
|
+
);
|
|
12421
|
+
}
|
|
12422
|
+
return;
|
|
12423
|
+
}
|
|
12424
|
+
const prompt = execMessage.payload.content ?? execMessage.payload.text ?? execMessage.payload.prompt ?? "";
|
|
12425
|
+
if (prompt) {
|
|
12426
|
+
chatHistoryService.saveMessage(sessionId, createUserMessage(prompt), true);
|
|
12427
|
+
}
|
|
12428
|
+
try {
|
|
12429
|
+
const blocks = await manager.executeCommand(prompt);
|
|
12430
|
+
if (blocks.length > 0) {
|
|
12431
|
+
const assistantContent = blocks.map((b) => b.content).join("\n");
|
|
12432
|
+
chatHistoryService.saveMessage(
|
|
12433
|
+
sessionId,
|
|
12434
|
+
createAssistantMessage(assistantContent),
|
|
12435
|
+
true
|
|
12436
|
+
);
|
|
12437
|
+
}
|
|
12438
|
+
if (messageBroadcaster) {
|
|
12439
|
+
const streamingMessageId = getOrCreateBacklogStreamingMessageId(sessionId);
|
|
12440
|
+
const protocolBlocks = blocks.map((block) => ({
|
|
12441
|
+
id: block.id,
|
|
12442
|
+
blockType: block.block_type,
|
|
12443
|
+
content: block.content,
|
|
12444
|
+
metadata: "metadata" in block ? block.metadata : void 0
|
|
12445
|
+
}));
|
|
12446
|
+
const contentText = protocolBlocks.map((b) => b.content).join("\n");
|
|
12447
|
+
const streamingMessage = {
|
|
12448
|
+
type: "session.output",
|
|
12449
|
+
session_id: sessionId,
|
|
12450
|
+
streaming_message_id: streamingMessageId,
|
|
12451
|
+
payload: {
|
|
12452
|
+
content_type: "agent",
|
|
12453
|
+
content: contentText,
|
|
12454
|
+
content_blocks: protocolBlocks,
|
|
12455
|
+
timestamp: Date.now(),
|
|
12456
|
+
is_complete: false
|
|
12457
|
+
}
|
|
12458
|
+
};
|
|
12459
|
+
void messageBroadcaster.broadcastToSubscribers(
|
|
12460
|
+
sessionId,
|
|
12461
|
+
JSON.stringify(streamingMessage)
|
|
12462
|
+
);
|
|
12463
|
+
const completionMessage = {
|
|
12464
|
+
type: "session.output",
|
|
12465
|
+
session_id: sessionId,
|
|
12466
|
+
streaming_message_id: streamingMessageId,
|
|
12467
|
+
payload: {
|
|
12468
|
+
content_type: "agent",
|
|
12469
|
+
content: contentText,
|
|
12470
|
+
content_blocks: protocolBlocks,
|
|
12471
|
+
timestamp: Date.now(),
|
|
12472
|
+
is_complete: true
|
|
12473
|
+
}
|
|
12474
|
+
};
|
|
12475
|
+
void messageBroadcaster.broadcastToSubscribers(
|
|
12476
|
+
sessionId,
|
|
12477
|
+
JSON.stringify(completionMessage)
|
|
12478
|
+
);
|
|
12479
|
+
clearBacklogStreamingMessageId(sessionId);
|
|
12480
|
+
}
|
|
12481
|
+
} catch (error) {
|
|
12482
|
+
const errorContent = error instanceof Error ? error.message : "Unknown error";
|
|
12483
|
+
logger.error(
|
|
12484
|
+
{ error, sessionId, errorContent, stack: error instanceof Error ? error.stack : void 0 },
|
|
12485
|
+
"Failed to execute backlog command"
|
|
12486
|
+
);
|
|
12487
|
+
chatHistoryService.saveMessage(
|
|
12488
|
+
sessionId,
|
|
12489
|
+
createErrorMessage(errorContent),
|
|
12490
|
+
true
|
|
12491
|
+
);
|
|
12492
|
+
if (messageBroadcaster) {
|
|
12493
|
+
const streamingMessageId = getOrCreateBacklogStreamingMessageId(sessionId);
|
|
12494
|
+
const errorBlock = {
|
|
12495
|
+
id: "error",
|
|
12496
|
+
blockType: "error",
|
|
12497
|
+
content: errorContent
|
|
12498
|
+
};
|
|
12499
|
+
const errorStreamingMessage = {
|
|
12500
|
+
type: "session.output",
|
|
12501
|
+
session_id: sessionId,
|
|
12502
|
+
streaming_message_id: streamingMessageId,
|
|
12503
|
+
payload: {
|
|
12504
|
+
content_type: "agent",
|
|
12505
|
+
content: errorContent,
|
|
12506
|
+
content_blocks: [errorBlock],
|
|
12507
|
+
timestamp: Date.now(),
|
|
12508
|
+
is_complete: false
|
|
12509
|
+
}
|
|
12510
|
+
};
|
|
12511
|
+
void messageBroadcaster.broadcastToSubscribers(
|
|
12512
|
+
sessionId,
|
|
12513
|
+
JSON.stringify(errorStreamingMessage)
|
|
12514
|
+
);
|
|
12515
|
+
const errorCompletionMessage = {
|
|
12516
|
+
type: "session.output",
|
|
12517
|
+
session_id: sessionId,
|
|
12518
|
+
streaming_message_id: streamingMessageId,
|
|
12519
|
+
payload: {
|
|
12520
|
+
content_type: "agent",
|
|
12521
|
+
content: errorContent,
|
|
12522
|
+
content_blocks: [errorBlock],
|
|
12523
|
+
timestamp: Date.now(),
|
|
12524
|
+
is_complete: true
|
|
12525
|
+
}
|
|
12526
|
+
};
|
|
12527
|
+
void messageBroadcaster.broadcastToSubscribers(
|
|
12528
|
+
sessionId,
|
|
12529
|
+
JSON.stringify(errorCompletionMessage)
|
|
12530
|
+
);
|
|
12531
|
+
clearBacklogStreamingMessageId(sessionId);
|
|
12532
|
+
}
|
|
12533
|
+
}
|
|
12534
|
+
return;
|
|
12535
|
+
}
|
|
10143
12536
|
if (execMessage.payload.audio) {
|
|
10144
12537
|
logger.info(
|
|
10145
12538
|
{ sessionId, hasAudio: true, messageId },
|
|
@@ -10408,7 +12801,7 @@ async function bootstrap() {
|
|
|
10408
12801
|
}
|
|
10409
12802
|
if (messageBroadcaster) {
|
|
10410
12803
|
const cancelBlock = {
|
|
10411
|
-
id:
|
|
12804
|
+
id: randomUUID6(),
|
|
10412
12805
|
block_type: "cancel",
|
|
10413
12806
|
content: "Cancelled by user"
|
|
10414
12807
|
};
|
|
@@ -10869,8 +13262,27 @@ async function bootstrap() {
|
|
|
10869
13262
|
tunnelClient,
|
|
10870
13263
|
authenticateClient,
|
|
10871
13264
|
logger,
|
|
10872
|
-
subscriptionService
|
|
13265
|
+
subscriptionService,
|
|
13266
|
+
broadcaster
|
|
10873
13267
|
);
|
|
13268
|
+
} else if (messageType === "supervisor.context_cleared.ack") {
|
|
13269
|
+
try {
|
|
13270
|
+
const ackData = data;
|
|
13271
|
+
const broadcastId = ackData.payload?.broadcast_id;
|
|
13272
|
+
const deviceId = ackData.payload?.device_id;
|
|
13273
|
+
if (broadcastId && deviceId && supervisorAgent) {
|
|
13274
|
+
supervisorAgent.recordClearAck(broadcastId, deviceId);
|
|
13275
|
+
logger.debug(
|
|
13276
|
+
{ broadcastId, deviceId },
|
|
13277
|
+
"Recorded context clear acknowledgment"
|
|
13278
|
+
);
|
|
13279
|
+
}
|
|
13280
|
+
} catch (ackError) {
|
|
13281
|
+
logger.warn(
|
|
13282
|
+
{ error: ackError, message: message.slice(0, 100) },
|
|
13283
|
+
"Failed to process context clear acknowledgment"
|
|
13284
|
+
);
|
|
13285
|
+
}
|
|
10874
13286
|
} else {
|
|
10875
13287
|
handleTunnelMessage(
|
|
10876
13288
|
message,
|
|
@@ -10932,7 +13344,7 @@ async function bootstrap() {
|
|
|
10932
13344
|
const getOrCreateAgentStreamingMessageId = (sessionId) => {
|
|
10933
13345
|
let messageId = agentStreamingMessageIds.get(sessionId);
|
|
10934
13346
|
if (!messageId) {
|
|
10935
|
-
messageId =
|
|
13347
|
+
messageId = randomUUID6();
|
|
10936
13348
|
agentStreamingMessageIds.set(sessionId, messageId);
|
|
10937
13349
|
}
|
|
10938
13350
|
return messageId;
|
|
@@ -10949,6 +13361,18 @@ async function bootstrap() {
|
|
|
10949
13361
|
}, STREAMING_STATE_GRACE_PERIOD_MS);
|
|
10950
13362
|
agentCleanupTimeouts.set(sessionId, timeout);
|
|
10951
13363
|
};
|
|
13364
|
+
const backlogStreamingMessageIds = /* @__PURE__ */ new Map();
|
|
13365
|
+
const getOrCreateBacklogStreamingMessageId = (sessionId) => {
|
|
13366
|
+
let messageId = backlogStreamingMessageIds.get(sessionId);
|
|
13367
|
+
if (!messageId) {
|
|
13368
|
+
messageId = randomUUID6();
|
|
13369
|
+
backlogStreamingMessageIds.set(sessionId, messageId);
|
|
13370
|
+
}
|
|
13371
|
+
return messageId;
|
|
13372
|
+
};
|
|
13373
|
+
const clearBacklogStreamingMessageId = (sessionId) => {
|
|
13374
|
+
backlogStreamingMessageIds.delete(sessionId);
|
|
13375
|
+
};
|
|
10952
13376
|
agentSessionManager.on(
|
|
10953
13377
|
"blocks",
|
|
10954
13378
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
@@ -11516,6 +13940,11 @@ bootstrap().catch((error) => {
|
|
|
11516
13940
|
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
11517
13941
|
* @license FSL-1.1-NC
|
|
11518
13942
|
*/
|
|
13943
|
+
/**
|
|
13944
|
+
* @file session-repository.ts
|
|
13945
|
+
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
13946
|
+
* @license FSL-1.1-NC
|
|
13947
|
+
*/
|
|
11519
13948
|
/**
|
|
11520
13949
|
* @file schemas.ts
|
|
11521
13950
|
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
@@ -11658,11 +14087,6 @@ bootstrap().catch((error) => {
|
|
|
11658
14087
|
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
11659
14088
|
* @license FSL-1.1-NC
|
|
11660
14089
|
*/
|
|
11661
|
-
/**
|
|
11662
|
-
* @file session-repository.ts
|
|
11663
|
-
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
11664
|
-
* @license FSL-1.1-NC
|
|
11665
|
-
*/
|
|
11666
14090
|
/**
|
|
11667
14091
|
* @file audio-storage.ts
|
|
11668
14092
|
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
@@ -11680,11 +14104,69 @@ bootstrap().catch((error) => {
|
|
|
11680
14104
|
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
11681
14105
|
* @license FSL-1.1-NC
|
|
11682
14106
|
*/
|
|
14107
|
+
/**
|
|
14108
|
+
* @file backlog-agent-session.ts
|
|
14109
|
+
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
14110
|
+
* @license FSL-1.1-NC
|
|
14111
|
+
*/
|
|
14112
|
+
/**
|
|
14113
|
+
* @file backlog.ts
|
|
14114
|
+
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
14115
|
+
* @license FSL-1.1-NC
|
|
14116
|
+
*/
|
|
14117
|
+
/**
|
|
14118
|
+
* @file backlog-harness.ts
|
|
14119
|
+
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
14120
|
+
* @license FSL-1.1-NC
|
|
14121
|
+
*/
|
|
14122
|
+
/**
|
|
14123
|
+
* @file lang-graph-agent.ts
|
|
14124
|
+
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
14125
|
+
* @license FSL-1.1-NC
|
|
14126
|
+
*
|
|
14127
|
+
* Abstract base class for all LangGraph-based agents.
|
|
14128
|
+
* Provides unified streaming, state management, and event emission patterns.
|
|
14129
|
+
*/
|
|
14130
|
+
/**
|
|
14131
|
+
* @file backlog-state-manager.ts
|
|
14132
|
+
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
14133
|
+
* @license FSL-1.1-NC
|
|
14134
|
+
*
|
|
14135
|
+
* AgentStateManager implementation for backlog persistence.
|
|
14136
|
+
* Persists both conversation history and backlog state to files and database.
|
|
14137
|
+
*/
|
|
14138
|
+
/**
|
|
14139
|
+
* @file backlog-agent-tools.ts
|
|
14140
|
+
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
14141
|
+
* @license FSL-1.1-NC
|
|
14142
|
+
*
|
|
14143
|
+
* LangGraph tools for backlog agent operations.
|
|
14144
|
+
*/
|
|
14145
|
+
/**
|
|
14146
|
+
* @file backlog-agent.ts
|
|
14147
|
+
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
14148
|
+
* @license FSL-1.1-NC
|
|
14149
|
+
*
|
|
14150
|
+
* LangGraph-based Backlog Agent for executing backlog commands.
|
|
14151
|
+
* Extends LangGraphAgent base class for unified streaming and state management.
|
|
14152
|
+
*/
|
|
14153
|
+
/**
|
|
14154
|
+
* @file backlog-agent-manager.ts
|
|
14155
|
+
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
14156
|
+
* @license FSL-1.1-NC
|
|
14157
|
+
*/
|
|
11683
14158
|
/**
|
|
11684
14159
|
* @file in-memory-session-manager.ts
|
|
11685
14160
|
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
11686
14161
|
* @license FSL-1.1-NC
|
|
11687
14162
|
*/
|
|
14163
|
+
/**
|
|
14164
|
+
* @file supervisor-state-manager.ts
|
|
14165
|
+
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
14166
|
+
* @license FSL-1.1-NC
|
|
14167
|
+
*
|
|
14168
|
+
* AgentStateManager implementation for SupervisorAgent database persistence.
|
|
14169
|
+
*/
|
|
11688
14170
|
/**
|
|
11689
14171
|
* @file workspace-tools.ts
|
|
11690
14172
|
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
@@ -11713,12 +14195,18 @@ bootstrap().catch((error) => {
|
|
|
11713
14195
|
*
|
|
11714
14196
|
* LangGraph tools for file system operations.
|
|
11715
14197
|
*/
|
|
14198
|
+
/**
|
|
14199
|
+
* @file backlog-tools.ts
|
|
14200
|
+
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
14201
|
+
* @license FSL-1.1-NC
|
|
14202
|
+
*/
|
|
11716
14203
|
/**
|
|
11717
14204
|
* @file supervisor-agent.ts
|
|
11718
14205
|
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
11719
14206
|
* @license FSL-1.1-NC
|
|
11720
14207
|
*
|
|
11721
14208
|
* LangGraph-based Supervisor Agent for managing workstation resources.
|
|
14209
|
+
* Extends LangGraphAgent base class for unified streaming and state management.
|
|
11722
14210
|
*/
|
|
11723
14211
|
/**
|
|
11724
14212
|
* @file fixture-loader.ts
|
|
@@ -11772,6 +14260,11 @@ bootstrap().catch((error) => {
|
|
|
11772
14260
|
* Service for summarizing long responses before TTS synthesis.
|
|
11773
14261
|
* Uses the same LLM provider as the supervisor agent.
|
|
11774
14262
|
*/
|
|
14263
|
+
/**
|
|
14264
|
+
* @file chat-message.ts
|
|
14265
|
+
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
14266
|
+
* @license FSL-1.1-NC
|
|
14267
|
+
*/
|
|
11775
14268
|
/**
|
|
11776
14269
|
* @file main.ts
|
|
11777
14270
|
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|