cyrus-edge-worker 0.2.3 → 0.2.5
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/AgentSessionManager.d.ts +35 -37
- package/dist/AgentSessionManager.d.ts.map +1 -1
- package/dist/AgentSessionManager.js +102 -402
- package/dist/AgentSessionManager.js.map +1 -1
- package/dist/EdgeWorker.d.ts +25 -7
- package/dist/EdgeWorker.d.ts.map +1 -1
- package/dist/EdgeWorker.js +280 -149
- package/dist/EdgeWorker.js.map +1 -1
- package/dist/procedures/ProcedureRouter.d.ts +5 -3
- package/dist/procedures/ProcedureRouter.d.ts.map +1 -1
- package/dist/procedures/ProcedureRouter.js +25 -11
- package/dist/procedures/ProcedureRouter.js.map +1 -1
- package/dist/procedures/registry.d.ts +5 -5
- package/dist/procedures/registry.js +5 -5
- package/dist/procedures/registry.js.map +1 -1
- package/dist/procedures/types.d.ts +3 -2
- package/dist/procedures/types.d.ts.map +1 -1
- package/package.json +8 -7
package/dist/EdgeWorker.js
CHANGED
|
@@ -7,6 +7,7 @@ import { watch as chokidarWatch } from "chokidar";
|
|
|
7
7
|
import { AbortError, ClaudeRunner, createCyrusToolsServer, createImageToolsServer, createSoraToolsServer, getAllTools, getCoordinatorTools, getReadOnlyTools, getSafeTools, } from "cyrus-claude-runner";
|
|
8
8
|
import { ConfigUpdater } from "cyrus-config-updater";
|
|
9
9
|
import { DEFAULT_PROXY_URL, isAgentSessionCreatedWebhook, isAgentSessionPromptedWebhook, isIssueAssignedWebhook, isIssueCommentMentionWebhook, isIssueNewCommentWebhook, isIssueUnassignedWebhook, PersistenceManager, resolvePath, } from "cyrus-core";
|
|
10
|
+
import { GeminiRunner } from "cyrus-gemini-runner";
|
|
10
11
|
import { LinearEventTransport, LinearIssueTrackerService, } from "cyrus-linear-event-transport";
|
|
11
12
|
import { fileTypeFromBuffer } from "file-type";
|
|
12
13
|
import { AgentSessionManager } from "./AgentSessionManager.js";
|
|
@@ -22,7 +23,7 @@ import { SharedApplicationServer } from "./SharedApplicationServer.js";
|
|
|
22
23
|
export class EdgeWorker extends EventEmitter {
|
|
23
24
|
config;
|
|
24
25
|
repositories = new Map(); // repository 'id' (internal, stored in config.json) mapped to the full repo config
|
|
25
|
-
agentSessionManagers = new Map(); // Maps repository ID to AgentSessionManager, which manages
|
|
26
|
+
agentSessionManagers = new Map(); // Maps repository ID to AgentSessionManager, which manages agent runners for a repo
|
|
26
27
|
issueTrackers = new Map(); // one issue tracker per 'repository'
|
|
27
28
|
linearEventTransport = null; // Single event transport for webhook delivery
|
|
28
29
|
configUpdater = null; // Single config updater for configuration updates
|
|
@@ -40,11 +41,13 @@ export class EdgeWorker extends EventEmitter {
|
|
|
40
41
|
this.config = config;
|
|
41
42
|
this.cyrusHome = config.cyrusHome;
|
|
42
43
|
this.persistenceManager = new PersistenceManager(join(this.cyrusHome, "state"));
|
|
43
|
-
// Initialize procedure router with haiku
|
|
44
|
+
// Initialize procedure router with haiku for fast classification
|
|
45
|
+
// Default to claude runner
|
|
44
46
|
this.procedureRouter = new ProcedureRouter({
|
|
45
47
|
cyrusHome: this.cyrusHome,
|
|
46
48
|
model: "haiku",
|
|
47
|
-
timeoutMs:
|
|
49
|
+
timeoutMs: 100000,
|
|
50
|
+
runnerType: "claude", // Use Claude by default
|
|
48
51
|
});
|
|
49
52
|
// Initialize repository router with dependencies
|
|
50
53
|
const repositoryRouterDeps = {
|
|
@@ -122,47 +125,11 @@ export class EdgeWorker extends EventEmitter {
|
|
|
122
125
|
return parentId;
|
|
123
126
|
}, async (parentSessionId, prompt, childSessionId) => {
|
|
124
127
|
await this.handleResumeParentSession(parentSessionId, prompt, childSessionId, repo, agentSessionManager);
|
|
125
|
-
}, async (linearAgentActivitySessionId) => {
|
|
126
|
-
console.log(`[Subroutine Transition] Advancing to next subroutine for session ${linearAgentActivitySessionId}`);
|
|
127
|
-
// Get the session
|
|
128
|
-
const session = agentSessionManager.getSession(linearAgentActivitySessionId);
|
|
129
|
-
if (!session) {
|
|
130
|
-
console.error(`[Subroutine Transition] Session ${linearAgentActivitySessionId} not found`);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
// Get next subroutine (advancement already handled by AgentSessionManager)
|
|
134
|
-
const nextSubroutine = this.procedureRouter.getCurrentSubroutine(session);
|
|
135
|
-
if (!nextSubroutine) {
|
|
136
|
-
console.log(`[Subroutine Transition] Procedure complete for session ${linearAgentActivitySessionId}`);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
console.log(`[Subroutine Transition] Next subroutine: ${nextSubroutine.name}`);
|
|
140
|
-
// Load subroutine prompt
|
|
141
|
-
let subroutinePrompt;
|
|
142
|
-
try {
|
|
143
|
-
subroutinePrompt = await this.loadSubroutinePrompt(nextSubroutine, this.config.linearWorkspaceSlug);
|
|
144
|
-
if (!subroutinePrompt) {
|
|
145
|
-
// Fallback if loadSubroutinePrompt returns null
|
|
146
|
-
subroutinePrompt = `Continue with: ${nextSubroutine.description}`;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
console.error(`[Subroutine Transition] Failed to load subroutine prompt:`, error);
|
|
151
|
-
// Fallback to simple prompt
|
|
152
|
-
subroutinePrompt = `Continue with: ${nextSubroutine.description}`;
|
|
153
|
-
}
|
|
154
|
-
// Resume Claude session with subroutine prompt
|
|
155
|
-
try {
|
|
156
|
-
await this.resumeClaudeSession(session, repo, linearAgentActivitySessionId, agentSessionManager, subroutinePrompt, "", // No attachment manifest
|
|
157
|
-
false, // Not a new session
|
|
158
|
-
[], // No additional allowed directories
|
|
159
|
-
nextSubroutine.maxTurns);
|
|
160
|
-
console.log(`[Subroutine Transition] Successfully resumed session for ${nextSubroutine.name} subroutine${nextSubroutine.maxTurns ? ` (maxTurns=${nextSubroutine.maxTurns})` : ""}`);
|
|
161
|
-
}
|
|
162
|
-
catch (error) {
|
|
163
|
-
console.error(`[Subroutine Transition] Failed to resume session for ${nextSubroutine.name} subroutine:`, error);
|
|
164
|
-
}
|
|
165
128
|
}, this.procedureRouter, this.sharedApplicationServer);
|
|
129
|
+
// Subscribe to subroutine completion events
|
|
130
|
+
agentSessionManager.on("subroutineComplete", async ({ linearAgentActivitySessionId, session }) => {
|
|
131
|
+
await this.handleSubroutineTransition(linearAgentActivitySessionId, session, repo, agentSessionManager);
|
|
132
|
+
});
|
|
166
133
|
this.agentSessionManagers.set(repo.id, agentSessionManager);
|
|
167
134
|
}
|
|
168
135
|
}
|
|
@@ -243,13 +210,13 @@ export class EdgeWorker extends EventEmitter {
|
|
|
243
210
|
catch (error) {
|
|
244
211
|
console.error("❌ Failed to save EdgeWorker state during shutdown:", error);
|
|
245
212
|
}
|
|
246
|
-
// get all
|
|
247
|
-
const
|
|
213
|
+
// get all agent runners
|
|
214
|
+
const agentRunners = [];
|
|
248
215
|
for (const agentSessionManager of this.agentSessionManagers.values()) {
|
|
249
|
-
|
|
216
|
+
agentRunners.push(...agentSessionManager.getAllAgentRunners());
|
|
250
217
|
}
|
|
251
|
-
// Kill all
|
|
252
|
-
for (const runner of
|
|
218
|
+
// Kill all agent processes with null checking
|
|
219
|
+
for (const runner of agentRunners) {
|
|
253
220
|
if (runner) {
|
|
254
221
|
try {
|
|
255
222
|
runner.stop();
|
|
@@ -335,6 +302,45 @@ export class EdgeWorker extends EventEmitter {
|
|
|
335
302
|
console.error(`[Parent Session Resume] Error context - Parent issue: ${parentSession.issueId}, Repository: ${repo.name}`);
|
|
336
303
|
}
|
|
337
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Handle subroutine transition when a subroutine completes
|
|
307
|
+
* This is triggered by the AgentSessionManager's 'subroutineComplete' event
|
|
308
|
+
*/
|
|
309
|
+
async handleSubroutineTransition(linearAgentActivitySessionId, session, repo, agentSessionManager) {
|
|
310
|
+
console.log(`[Subroutine Transition] Handling subroutine completion for session ${linearAgentActivitySessionId}`);
|
|
311
|
+
// Get next subroutine (advancement already handled by AgentSessionManager)
|
|
312
|
+
const nextSubroutine = this.procedureRouter.getCurrentSubroutine(session);
|
|
313
|
+
if (!nextSubroutine) {
|
|
314
|
+
console.log(`[Subroutine Transition] Procedure complete for session ${linearAgentActivitySessionId}`);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
console.log(`[Subroutine Transition] Next subroutine: ${nextSubroutine.name}`);
|
|
318
|
+
// Load subroutine prompt
|
|
319
|
+
let subroutinePrompt;
|
|
320
|
+
try {
|
|
321
|
+
subroutinePrompt = await this.loadSubroutinePrompt(nextSubroutine, this.config.linearWorkspaceSlug);
|
|
322
|
+
if (!subroutinePrompt) {
|
|
323
|
+
// Fallback if loadSubroutinePrompt returns null
|
|
324
|
+
subroutinePrompt = `Continue with: ${nextSubroutine.description}`;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
console.error(`[Subroutine Transition] Failed to load subroutine prompt:`, error);
|
|
329
|
+
// Fallback to simple prompt
|
|
330
|
+
subroutinePrompt = `Continue with: ${nextSubroutine.description}`;
|
|
331
|
+
}
|
|
332
|
+
// Resume Claude session with subroutine prompt
|
|
333
|
+
try {
|
|
334
|
+
await this.resumeAgentSession(session, repo, linearAgentActivitySessionId, agentSessionManager, subroutinePrompt, "", // No attachment manifest
|
|
335
|
+
false, // Not a new session
|
|
336
|
+
[], // No additional allowed directories
|
|
337
|
+
nextSubroutine?.singleTurn ? 1 : undefined);
|
|
338
|
+
console.log(`[Subroutine Transition] Successfully resumed session for ${nextSubroutine.name} subroutine${nextSubroutine.singleTurn ? " (singleTurn)" : ""}`);
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
console.error(`[Subroutine Transition] Failed to resume session for ${nextSubroutine.name} subroutine:`, error);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
338
344
|
/**
|
|
339
345
|
* Start watching config file for changes
|
|
340
346
|
*/
|
|
@@ -510,8 +516,11 @@ export class EdgeWorker extends EventEmitter {
|
|
|
510
516
|
return this.childToParentAgentSession.get(childSessionId);
|
|
511
517
|
}, async (parentSessionId, prompt, childSessionId) => {
|
|
512
518
|
await this.handleResumeParentSession(parentSessionId, prompt, childSessionId, repo, agentSessionManager);
|
|
513
|
-
},
|
|
514
|
-
|
|
519
|
+
}, this.procedureRouter, this.sharedApplicationServer);
|
|
520
|
+
// Subscribe to subroutine completion events
|
|
521
|
+
agentSessionManager.on("subroutineComplete", async ({ linearAgentActivitySessionId, session }) => {
|
|
522
|
+
await this.handleSubroutineTransition(linearAgentActivitySessionId, session, repo, agentSessionManager);
|
|
523
|
+
});
|
|
515
524
|
this.agentSessionManagers.set(repo.id, agentSessionManager);
|
|
516
525
|
console.log(`✅ Repository added successfully: ${repo.name}`);
|
|
517
526
|
}
|
|
@@ -592,10 +601,10 @@ export class EdgeWorker extends EventEmitter {
|
|
|
592
601
|
for (const session of activeSessions) {
|
|
593
602
|
try {
|
|
594
603
|
console.log(` 🛑 Stopping session for issue ${session.issueId}`);
|
|
595
|
-
// Get the
|
|
596
|
-
const runner = manager?.
|
|
604
|
+
// Get the agent runner for this session
|
|
605
|
+
const runner = manager?.getAgentRunner(session.linearAgentActivitySessionId);
|
|
597
606
|
if (runner) {
|
|
598
|
-
// Stop the
|
|
607
|
+
// Stop the agent process
|
|
599
608
|
runner.stop();
|
|
600
609
|
console.log(` ✅ Stopped Claude runner for session ${session.linearAgentActivitySessionId}`);
|
|
601
610
|
}
|
|
@@ -754,7 +763,10 @@ export class EdgeWorker extends EventEmitter {
|
|
|
754
763
|
const attachmentsDir = join(this.cyrusHome, workspaceFolderName, "attachments");
|
|
755
764
|
await mkdir(attachmentsDir, { recursive: true });
|
|
756
765
|
// Build allowed directories list - always include attachments directory
|
|
757
|
-
const allowedDirectories = [
|
|
766
|
+
const allowedDirectories = [
|
|
767
|
+
attachmentsDir,
|
|
768
|
+
repository.repositoryPath,
|
|
769
|
+
];
|
|
758
770
|
console.log(`[EdgeWorker] Configured allowed directories for ${fullIssue.identifier}:`, allowedDirectories);
|
|
759
771
|
// Build allowed tools list with Linear MCP tools
|
|
760
772
|
const allowedTools = this.buildAllowedTools(repository);
|
|
@@ -821,14 +833,14 @@ export class EdgeWorker extends EventEmitter {
|
|
|
821
833
|
console.log(`[EdgeWorker] Handling agent session created: ${webhook.agentSession.issue.identifier}`);
|
|
822
834
|
const { agentSession, guidance } = webhook;
|
|
823
835
|
const commentBody = agentSession.comment?.body;
|
|
824
|
-
// Initialize
|
|
825
|
-
await this.
|
|
836
|
+
// Initialize agent runner using shared logic
|
|
837
|
+
await this.initializeAgentRunner(agentSession, repository, guidance, commentBody);
|
|
826
838
|
}
|
|
827
839
|
/**
|
|
828
840
|
|
|
829
841
|
/**
|
|
830
|
-
* Initialize and start
|
|
831
|
-
* This method contains the shared logic for creating
|
|
842
|
+
* Initialize and start agent runner for an agent session
|
|
843
|
+
* This method contains the shared logic for creating an agent runner that both
|
|
832
844
|
* handleAgentSessionCreatedWebhook and handleUserPromptedAgentActivity use.
|
|
833
845
|
*
|
|
834
846
|
* @param agentSession The Linear agent session
|
|
@@ -836,7 +848,7 @@ export class EdgeWorker extends EventEmitter {
|
|
|
836
848
|
* @param guidance Optional guidance rules from Linear
|
|
837
849
|
* @param commentBody Optional comment body (for mentions)
|
|
838
850
|
*/
|
|
839
|
-
async
|
|
851
|
+
async initializeAgentRunner(agentSession, repository, guidance, commentBody) {
|
|
840
852
|
const linearAgentActivitySessionId = agentSession.id;
|
|
841
853
|
const { issue } = agentSession;
|
|
842
854
|
if (!issue) {
|
|
@@ -973,28 +985,47 @@ export class EdgeWorker extends EventEmitter {
|
|
|
973
985
|
if (disallowedTools.length > 0) {
|
|
974
986
|
console.log(`[EdgeWorker] Configured disallowed tools for ${fullIssue.identifier}:`, disallowedTools);
|
|
975
987
|
}
|
|
976
|
-
//
|
|
977
|
-
const
|
|
978
|
-
|
|
979
|
-
|
|
988
|
+
// Get current subroutine to check for singleTurn mode
|
|
989
|
+
const currentSubroutine = this.procedureRouter.getCurrentSubroutine(session);
|
|
990
|
+
// Create agent runner with system prompt from assembly
|
|
991
|
+
// buildAgentRunnerConfig now determines runner type from labels internally
|
|
992
|
+
const { config: runnerConfig, runnerType } = this.buildAgentRunnerConfig(session, repository, linearAgentActivitySessionId, assembly.systemPrompt, allowedTools, allowedDirectories, disallowedTools, undefined, // resumeSessionId
|
|
993
|
+
labels, // Pass labels for runner selection and model override
|
|
994
|
+
undefined, // maxTurns
|
|
995
|
+
currentSubroutine?.singleTurn);
|
|
996
|
+
console.log(`[EdgeWorker] Label-based runner selection for new session: ${runnerType} (session ${linearAgentActivitySessionId})`);
|
|
997
|
+
const runner = runnerType === "claude"
|
|
998
|
+
? new ClaudeRunner(runnerConfig)
|
|
999
|
+
: new GeminiRunner(runnerConfig);
|
|
980
1000
|
// Store runner by comment ID
|
|
981
|
-
agentSessionManager.
|
|
1001
|
+
agentSessionManager.addAgentRunner(linearAgentActivitySessionId, runner);
|
|
982
1002
|
// Save state after mapping changes
|
|
983
1003
|
await this.savePersistedState();
|
|
984
1004
|
// Emit events using full Linear issue
|
|
985
1005
|
this.emit("session:started", fullIssue.id, fullIssue, repository.id);
|
|
986
1006
|
this.config.handlers?.onSessionStart?.(fullIssue.id, fullIssue, repository.id);
|
|
987
1007
|
// Update runner with version information (if available)
|
|
988
|
-
|
|
1008
|
+
// Note: updatePromptVersions is specific to ClaudeRunner
|
|
1009
|
+
if (systemPromptVersion &&
|
|
1010
|
+
"updatePromptVersions" in runner &&
|
|
1011
|
+
typeof runner.updatePromptVersions === "function") {
|
|
989
1012
|
runner.updatePromptVersions({
|
|
990
1013
|
systemPromptVersion,
|
|
991
1014
|
});
|
|
992
1015
|
}
|
|
993
1016
|
// Log metadata for debugging
|
|
994
1017
|
console.log(`[EdgeWorker] Initial prompt built successfully - components: ${assembly.metadata.components.join(", ")}, type: ${assembly.metadata.promptType}, length: ${assembly.userPrompt.length} characters`);
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1018
|
+
// Start session - use streaming mode if supported for ability to add messages later
|
|
1019
|
+
if (runner.supportsStreamingInput && runner.startStreaming) {
|
|
1020
|
+
console.log(`[EdgeWorker] Starting streaming session`);
|
|
1021
|
+
const sessionInfo = await runner.startStreaming(assembly.userPrompt);
|
|
1022
|
+
console.log(`[EdgeWorker] Streaming session started: ${sessionInfo.sessionId}`);
|
|
1023
|
+
}
|
|
1024
|
+
else {
|
|
1025
|
+
console.log(`[EdgeWorker] Starting non-streaming session`);
|
|
1026
|
+
const sessionInfo = await runner.start(assembly.userPrompt);
|
|
1027
|
+
console.log(`[EdgeWorker] Non-streaming session started: ${sessionInfo.sessionId}`);
|
|
1028
|
+
}
|
|
998
1029
|
// Note: AgentSessionManager will be initialized automatically when the first system message
|
|
999
1030
|
// is received via handleClaudeMessage() callback
|
|
1000
1031
|
}
|
|
@@ -1032,10 +1063,10 @@ export class EdgeWorker extends EventEmitter {
|
|
|
1032
1063
|
return;
|
|
1033
1064
|
}
|
|
1034
1065
|
// Stop the existing runner if it's active
|
|
1035
|
-
const existingRunner = foundSession.
|
|
1066
|
+
const existingRunner = foundSession.agentRunner;
|
|
1036
1067
|
if (existingRunner) {
|
|
1037
1068
|
existingRunner.stop();
|
|
1038
|
-
console.log(`[EdgeWorker] Stopped
|
|
1069
|
+
console.log(`[EdgeWorker] Stopped agent session for agent activity session ${agentSessionId}`);
|
|
1039
1070
|
}
|
|
1040
1071
|
// Post confirmation
|
|
1041
1072
|
const issueTitle = issue?.title || "this issue";
|
|
@@ -1075,9 +1106,9 @@ export class EdgeWorker extends EventEmitter {
|
|
|
1075
1106
|
this.repositoryRouter.getIssueRepositoryCache().set(issueId, repository.id);
|
|
1076
1107
|
// Post agent activity showing user-selected repository
|
|
1077
1108
|
await this.postRepositorySelectionActivity(agentSessionId, repository.id, repository.name, "user-selected");
|
|
1078
|
-
console.log(`[EdgeWorker] Initializing
|
|
1079
|
-
// Initialize
|
|
1080
|
-
await this.
|
|
1109
|
+
console.log(`[EdgeWorker] Initializing agent runner after repository selection: ${agentSession.issue.identifier} -> ${repository.name}`);
|
|
1110
|
+
// Initialize agent runner with the selected repository
|
|
1111
|
+
await this.initializeAgentRunner(agentSession, repository, guidance, commentBody);
|
|
1081
1112
|
}
|
|
1082
1113
|
/**
|
|
1083
1114
|
* Handle normal prompted activity (existing session continuation)
|
|
@@ -1124,8 +1155,8 @@ export class EdgeWorker extends EventEmitter {
|
|
|
1124
1155
|
else {
|
|
1125
1156
|
console.log(`[EdgeWorker] Found existing session ${linearAgentActivitySessionId} for new user prompt`);
|
|
1126
1157
|
// Post instant acknowledgment for existing session BEFORE any async work
|
|
1127
|
-
// Check streaming
|
|
1128
|
-
const isCurrentlyStreaming = session?.
|
|
1158
|
+
// Check if runner is currently running (streaming is Claude-specific, use isRunning for both)
|
|
1159
|
+
const isCurrentlyStreaming = session?.agentRunner?.isRunning() || false;
|
|
1129
1160
|
await this.postInstantPromptedAcknowledgment(linearAgentActivitySessionId, repository.id, isCurrentlyStreaming);
|
|
1130
1161
|
// Need to fetch full issue for routing context
|
|
1131
1162
|
const issueTracker = this.issueTrackers.get(repository.id);
|
|
@@ -1259,12 +1290,12 @@ export class EdgeWorker extends EventEmitter {
|
|
|
1259
1290
|
console.log("No agentSessionManager for unassigned issue, so no sessions to stop");
|
|
1260
1291
|
return;
|
|
1261
1292
|
}
|
|
1262
|
-
// Get all
|
|
1263
|
-
const
|
|
1264
|
-
// Stop all
|
|
1265
|
-
const activeThreadCount =
|
|
1266
|
-
for (const runner of
|
|
1267
|
-
console.log(`[EdgeWorker] Stopping
|
|
1293
|
+
// Get all agent runners for this specific issue
|
|
1294
|
+
const agentRunners = agentSessionManager.getAgentRunnersForIssue(issue.id);
|
|
1295
|
+
// Stop all agent runners for this issue
|
|
1296
|
+
const activeThreadCount = agentRunners.length;
|
|
1297
|
+
for (const runner of agentRunners) {
|
|
1298
|
+
console.log(`[EdgeWorker] Stopping agent runner for issue ${issue.identifier}`);
|
|
1268
1299
|
runner.stop();
|
|
1269
1300
|
}
|
|
1270
1301
|
// Post ONE farewell comment on the issue (not in any thread) if there were active sessions
|
|
@@ -1308,6 +1339,86 @@ export class EdgeWorker extends EventEmitter {
|
|
|
1308
1339
|
return [];
|
|
1309
1340
|
}
|
|
1310
1341
|
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Determine runner type and model from issue labels.
|
|
1344
|
+
* Returns the runner type ("claude" or "gemini"), optional model override, and fallback model.
|
|
1345
|
+
*
|
|
1346
|
+
* Label priority (case-insensitive):
|
|
1347
|
+
* - Gemini labels: gemini, gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite, gemini-3-pro, gemini-3-pro-preview
|
|
1348
|
+
* - Claude labels: claude, sonnet, opus
|
|
1349
|
+
*
|
|
1350
|
+
* If no runner label is found, defaults to claude.
|
|
1351
|
+
*/
|
|
1352
|
+
determineRunnerFromLabels(labels) {
|
|
1353
|
+
if (!labels || labels.length === 0) {
|
|
1354
|
+
return {
|
|
1355
|
+
runnerType: "claude",
|
|
1356
|
+
modelOverride: "sonnet",
|
|
1357
|
+
fallbackModelOverride: "haiku",
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
const lowercaseLabels = labels.map((label) => label.toLowerCase());
|
|
1361
|
+
// Check for Gemini labels first
|
|
1362
|
+
if (lowercaseLabels.includes("gemini-2.5-pro") ||
|
|
1363
|
+
lowercaseLabels.includes("gemini-2.5")) {
|
|
1364
|
+
return {
|
|
1365
|
+
runnerType: "gemini",
|
|
1366
|
+
modelOverride: "gemini-2.5-pro",
|
|
1367
|
+
fallbackModelOverride: "gemini-2.5-flash",
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
if (lowercaseLabels.includes("gemini-2.5-flash")) {
|
|
1371
|
+
return {
|
|
1372
|
+
runnerType: "gemini",
|
|
1373
|
+
modelOverride: "gemini-2.5-flash",
|
|
1374
|
+
fallbackModelOverride: "gemini-2.5-flash-lite",
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
if (lowercaseLabels.includes("gemini-2.5-flash-lite")) {
|
|
1378
|
+
return {
|
|
1379
|
+
runnerType: "gemini",
|
|
1380
|
+
modelOverride: "gemini-2.5-flash-lite",
|
|
1381
|
+
fallbackModelOverride: "gemini-2.5-flash-lite",
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
if (lowercaseLabels.includes("gemini-3") ||
|
|
1385
|
+
lowercaseLabels.includes("gemini-3-pro") ||
|
|
1386
|
+
lowercaseLabels.includes("gemini-3-pro-preview")) {
|
|
1387
|
+
return {
|
|
1388
|
+
runnerType: "gemini",
|
|
1389
|
+
modelOverride: "gemini-3-pro-preview",
|
|
1390
|
+
fallbackModelOverride: "gemini-2.5-pro",
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
if (lowercaseLabels.includes("gemini")) {
|
|
1394
|
+
return {
|
|
1395
|
+
runnerType: "gemini",
|
|
1396
|
+
modelOverride: "gemini-2.5-pro",
|
|
1397
|
+
fallbackModelOverride: "gemini-2.5-flash",
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
// Check for Claude labels
|
|
1401
|
+
if (lowercaseLabels.includes("opus")) {
|
|
1402
|
+
return {
|
|
1403
|
+
runnerType: "claude",
|
|
1404
|
+
modelOverride: "opus",
|
|
1405
|
+
fallbackModelOverride: "sonnet",
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
if (lowercaseLabels.includes("sonnet")) {
|
|
1409
|
+
return {
|
|
1410
|
+
runnerType: "claude",
|
|
1411
|
+
modelOverride: "sonnet",
|
|
1412
|
+
fallbackModelOverride: "haiku",
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
// Default to claude if no runner labels found
|
|
1416
|
+
return {
|
|
1417
|
+
runnerType: "claude",
|
|
1418
|
+
modelOverride: "sonnet",
|
|
1419
|
+
fallbackModelOverride: "haiku",
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1311
1422
|
/**
|
|
1312
1423
|
* Determine system prompt based on issue labels and repository configuration
|
|
1313
1424
|
*/
|
|
@@ -2378,7 +2489,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
2378
2489
|
let childRepo;
|
|
2379
2490
|
let childAgentSessionManager;
|
|
2380
2491
|
for (const [repoId, manager] of this.agentSessionManagers) {
|
|
2381
|
-
if (manager.
|
|
2492
|
+
if (manager.hasAgentRunner(childSessionId)) {
|
|
2382
2493
|
childRepo = this.repositories.get(repoId);
|
|
2383
2494
|
childAgentSessionManager = manager;
|
|
2384
2495
|
break;
|
|
@@ -2762,9 +2873,11 @@ ${input.userComment}
|
|
|
2762
2873
|
attachmentManifest, guidance);
|
|
2763
2874
|
}
|
|
2764
2875
|
/**
|
|
2765
|
-
* Build
|
|
2876
|
+
* Build agent runner configuration with common settings.
|
|
2877
|
+
* Also determines which runner type to use based on labels.
|
|
2878
|
+
* @returns Object containing the runner config and runner type to use
|
|
2766
2879
|
*/
|
|
2767
|
-
|
|
2880
|
+
buildAgentRunnerConfig(session, repository, linearAgentActivitySessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, resumeSessionId, labels, maxTurns, singleTurn) {
|
|
2768
2881
|
// Configure PostToolUse hook for playwright screenshots
|
|
2769
2882
|
const hooks = {
|
|
2770
2883
|
PostToolUse: [
|
|
@@ -2783,38 +2896,30 @@ ${input.userComment}
|
|
|
2783
2896
|
},
|
|
2784
2897
|
],
|
|
2785
2898
|
};
|
|
2786
|
-
//
|
|
2787
|
-
|
|
2788
|
-
let
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
else if (modelOverride === "sonnet") {
|
|
2811
|
-
fallbackModelOverride = "haiku";
|
|
2812
|
-
}
|
|
2813
|
-
else {
|
|
2814
|
-
fallbackModelOverride = "sonnet"; // haiku falls back to sonnet since same model retry doesn't help
|
|
2815
|
-
}
|
|
2816
|
-
}
|
|
2817
|
-
}
|
|
2899
|
+
// Determine runner type and model override from labels
|
|
2900
|
+
const runnerSelection = this.determineRunnerFromLabels(labels || []);
|
|
2901
|
+
let runnerType = runnerSelection.runnerType;
|
|
2902
|
+
let modelOverride = runnerSelection.modelOverride;
|
|
2903
|
+
let fallbackModelOverride = runnerSelection.fallbackModelOverride;
|
|
2904
|
+
// If the labels have changed, and we are resuming a session. Use the existing runner for the session.
|
|
2905
|
+
if (session.claudeSessionId && runnerType !== "claude") {
|
|
2906
|
+
runnerType = "claude";
|
|
2907
|
+
modelOverride = "sonnet";
|
|
2908
|
+
fallbackModelOverride = "haiku";
|
|
2909
|
+
}
|
|
2910
|
+
else if (session.geminiSessionId && runnerType !== "gemini") {
|
|
2911
|
+
runnerType = "gemini";
|
|
2912
|
+
modelOverride = "gemini-2.5-pro";
|
|
2913
|
+
fallbackModelOverride = "gemini-2.5-flash";
|
|
2914
|
+
}
|
|
2915
|
+
// Log model override if found
|
|
2916
|
+
if (modelOverride) {
|
|
2917
|
+
console.log(`[EdgeWorker] Model override via label: ${modelOverride} (for session ${linearAgentActivitySessionId})`);
|
|
2918
|
+
}
|
|
2919
|
+
// Convert singleTurn flag to effective maxTurns value
|
|
2920
|
+
const effectiveMaxTurns = singleTurn ? 1 : maxTurns;
|
|
2921
|
+
// Determine final model name with singleTurn suffix for Gemini
|
|
2922
|
+
const finalModel = modelOverride || repository.model || this.config.defaultModel;
|
|
2818
2923
|
const config = {
|
|
2819
2924
|
workingDirectory: session.workspace.path,
|
|
2820
2925
|
allowedTools,
|
|
@@ -2826,7 +2931,7 @@ ${input.userComment}
|
|
|
2826
2931
|
mcpConfig: this.buildMcpConfig(repository, linearAgentActivitySessionId),
|
|
2827
2932
|
appendSystemPrompt: systemPrompt || "",
|
|
2828
2933
|
// Priority order: label override > repository config > global default
|
|
2829
|
-
model:
|
|
2934
|
+
model: finalModel,
|
|
2830
2935
|
fallbackModel: fallbackModelOverride ||
|
|
2831
2936
|
repository.fallbackModel ||
|
|
2832
2937
|
this.config.defaultFallbackModel,
|
|
@@ -2839,10 +2944,13 @@ ${input.userComment}
|
|
|
2839
2944
|
if (resumeSessionId) {
|
|
2840
2945
|
config.resumeSessionId = resumeSessionId;
|
|
2841
2946
|
}
|
|
2842
|
-
if (
|
|
2843
|
-
config.maxTurns =
|
|
2947
|
+
if (effectiveMaxTurns !== undefined) {
|
|
2948
|
+
config.maxTurns = effectiveMaxTurns;
|
|
2949
|
+
if (singleTurn) {
|
|
2950
|
+
console.log(`[EdgeWorker] Applied singleTurn maxTurns=1 (for session ${linearAgentActivitySessionId})`);
|
|
2951
|
+
}
|
|
2844
2952
|
}
|
|
2845
|
-
return config;
|
|
2953
|
+
return { config, runnerType };
|
|
2846
2954
|
}
|
|
2847
2955
|
/**
|
|
2848
2956
|
* Build disallowed tools list following the same hierarchy as allowed tools
|
|
@@ -3222,19 +3330,21 @@ ${input.userComment}
|
|
|
3222
3330
|
* @returns true if message was added to stream, false if session was resumed
|
|
3223
3331
|
*/
|
|
3224
3332
|
async handlePromptWithStreamingCheck(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest, isNewSession, additionalAllowedDirs, logContext, commentAuthor, commentTimestamp) {
|
|
3225
|
-
// Check if runner is actively
|
|
3226
|
-
const existingRunner = session.
|
|
3227
|
-
const
|
|
3228
|
-
// Always route procedure for new input, UNLESS actively
|
|
3229
|
-
if (!
|
|
3333
|
+
// Check if runner is actively running before routing
|
|
3334
|
+
const existingRunner = session.agentRunner;
|
|
3335
|
+
const isRunning = existingRunner?.isRunning() || false;
|
|
3336
|
+
// Always route procedure for new input, UNLESS actively running
|
|
3337
|
+
if (!isRunning) {
|
|
3230
3338
|
await this.rerouteProcedureForSession(session, linearAgentActivitySessionId, agentSessionManager, promptBody, repository);
|
|
3231
3339
|
console.log(`[EdgeWorker] Routed procedure for ${logContext}`);
|
|
3232
3340
|
}
|
|
3233
3341
|
else {
|
|
3234
|
-
console.log(`[EdgeWorker] Skipping routing for ${linearAgentActivitySessionId} (${logContext}) - runner is actively
|
|
3342
|
+
console.log(`[EdgeWorker] Skipping routing for ${linearAgentActivitySessionId} (${logContext}) - runner is actively running`);
|
|
3235
3343
|
}
|
|
3236
|
-
// Handle
|
|
3237
|
-
if (existingRunner?.
|
|
3344
|
+
// Handle running case - add message to existing stream (if supported)
|
|
3345
|
+
if (existingRunner?.isRunning() &&
|
|
3346
|
+
existingRunner.supportsStreamingInput &&
|
|
3347
|
+
existingRunner.addStreamMessage) {
|
|
3238
3348
|
console.log(`[EdgeWorker] Adding prompt to existing stream for ${linearAgentActivitySessionId} (${logContext})`);
|
|
3239
3349
|
// Append attachment manifest to the prompt if we have one
|
|
3240
3350
|
let fullPrompt = promptBody;
|
|
@@ -3246,7 +3356,7 @@ ${input.userComment}
|
|
|
3246
3356
|
}
|
|
3247
3357
|
// Not streaming - resume/start session
|
|
3248
3358
|
console.log(`[EdgeWorker] Resuming Claude session for ${linearAgentActivitySessionId} (${logContext})`);
|
|
3249
|
-
await this.
|
|
3359
|
+
await this.resumeAgentSession(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest, isNewSession, additionalAllowedDirs, undefined, // maxTurns
|
|
3250
3360
|
commentAuthor, commentTimestamp);
|
|
3251
3361
|
return false; // Session was resumed
|
|
3252
3362
|
}
|
|
@@ -3336,7 +3446,7 @@ ${input.userComment}
|
|
|
3336
3446
|
}
|
|
3337
3447
|
}
|
|
3338
3448
|
/**
|
|
3339
|
-
* Resume or create
|
|
3449
|
+
* Resume or create an Agent session with the given prompt
|
|
3340
3450
|
* This is the core logic for handling prompted agent activities
|
|
3341
3451
|
* @param session The Cyrus agent session
|
|
3342
3452
|
* @param repository The repository configuration
|
|
@@ -3346,11 +3456,13 @@ ${input.userComment}
|
|
|
3346
3456
|
* @param attachmentManifest Optional attachment manifest
|
|
3347
3457
|
* @param isNewSession Whether this is a new session
|
|
3348
3458
|
*/
|
|
3349
|
-
async
|
|
3459
|
+
async resumeAgentSession(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest = "", isNewSession = false, additionalAllowedDirectories = [], maxTurns, commentAuthor, commentTimestamp) {
|
|
3350
3460
|
// Check for existing runner
|
|
3351
|
-
const existingRunner = session.
|
|
3352
|
-
// If there's an existing
|
|
3353
|
-
if (existingRunner?.
|
|
3461
|
+
const existingRunner = session.agentRunner;
|
|
3462
|
+
// If there's an existing running runner that supports streaming, add to it
|
|
3463
|
+
if (existingRunner?.isRunning() &&
|
|
3464
|
+
existingRunner.supportsStreamingInput &&
|
|
3465
|
+
existingRunner.addStreamMessage) {
|
|
3354
3466
|
let fullPrompt = promptBody;
|
|
3355
3467
|
if (attachmentManifest) {
|
|
3356
3468
|
fullPrompt = `${promptBody}\n\n${attachmentManifest}`;
|
|
@@ -3358,20 +3470,23 @@ ${input.userComment}
|
|
|
3358
3470
|
existingRunner.addStreamMessage(fullPrompt);
|
|
3359
3471
|
return;
|
|
3360
3472
|
}
|
|
3361
|
-
// Stop existing runner if it's not
|
|
3473
|
+
// Stop existing runner if it's not running
|
|
3362
3474
|
if (existingRunner) {
|
|
3363
3475
|
existingRunner.stop();
|
|
3364
3476
|
}
|
|
3365
|
-
// Determine if we need a new Claude session
|
|
3366
|
-
const needsNewClaudeSession = isNewSession || !session.claudeSessionId;
|
|
3367
3477
|
// Fetch full issue details
|
|
3368
3478
|
const fullIssue = await this.fetchFullIssueDetails(session.issueId, repository.id);
|
|
3369
3479
|
if (!fullIssue) {
|
|
3370
|
-
console.error(`[
|
|
3480
|
+
console.error(`[resumeAgentSession] Failed to fetch full issue details for ${session.issueId}`);
|
|
3371
3481
|
throw new Error(`Failed to fetch full issue details for ${session.issueId}`);
|
|
3372
3482
|
}
|
|
3373
|
-
// Fetch issue labels
|
|
3483
|
+
// Fetch issue labels early to determine runner type
|
|
3374
3484
|
const labels = await this.fetchIssueLabels(fullIssue);
|
|
3485
|
+
// Determine which runner to use based on existing session IDs
|
|
3486
|
+
const hasClaudeSession = !isNewSession && Boolean(session.claudeSessionId);
|
|
3487
|
+
const hasGeminiSession = !isNewSession && Boolean(session.geminiSessionId);
|
|
3488
|
+
const needsNewSession = isNewSession || (!hasClaudeSession && !hasGeminiSession);
|
|
3489
|
+
// Fetch system prompt based on labels
|
|
3375
3490
|
const systemPromptResult = await this.determineSystemPromptFromLabels(labels, repository);
|
|
3376
3491
|
const systemPrompt = systemPromptResult?.prompt;
|
|
3377
3492
|
const promptType = systemPromptResult?.type;
|
|
@@ -3386,27 +3501,43 @@ ${input.userComment}
|
|
|
3386
3501
|
await mkdir(attachmentsDir, { recursive: true });
|
|
3387
3502
|
const allowedDirectories = [
|
|
3388
3503
|
attachmentsDir,
|
|
3504
|
+
repository.repositoryPath,
|
|
3389
3505
|
...additionalAllowedDirectories,
|
|
3390
3506
|
];
|
|
3391
|
-
//
|
|
3392
|
-
const
|
|
3507
|
+
// Get current subroutine to check for singleTurn mode
|
|
3508
|
+
const currentSubroutine = this.procedureRouter.getCurrentSubroutine(session);
|
|
3509
|
+
const resumeSessionId = needsNewSession
|
|
3393
3510
|
? undefined
|
|
3394
|
-
: session.claudeSessionId
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3511
|
+
: session.claudeSessionId
|
|
3512
|
+
? session.claudeSessionId
|
|
3513
|
+
: session.geminiSessionId;
|
|
3514
|
+
// Create runner configuration
|
|
3515
|
+
// buildAgentRunnerConfig determines runner type from labels for new sessions
|
|
3516
|
+
// For existing sessions, we still need labels for model override but ignore runner type
|
|
3517
|
+
const { config: runnerConfig, runnerType } = this.buildAgentRunnerConfig(session, repository, linearAgentActivitySessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, resumeSessionId, labels, // Always pass labels to preserve model override
|
|
3518
|
+
maxTurns, // Pass maxTurns if specified
|
|
3519
|
+
currentSubroutine?.singleTurn);
|
|
3520
|
+
// Create the appropriate runner based on session state
|
|
3521
|
+
const runner = runnerType === "claude"
|
|
3522
|
+
? new ClaudeRunner(runnerConfig)
|
|
3523
|
+
: new GeminiRunner(runnerConfig);
|
|
3398
3524
|
// Store runner
|
|
3399
|
-
agentSessionManager.
|
|
3525
|
+
agentSessionManager.addAgentRunner(linearAgentActivitySessionId, runner);
|
|
3400
3526
|
// Save state
|
|
3401
3527
|
await this.savePersistedState();
|
|
3402
3528
|
// Prepare the full prompt
|
|
3403
3529
|
const fullPrompt = await this.buildSessionPrompt(isNewSession, session, fullIssue, repository, promptBody, attachmentManifest, commentAuthor, commentTimestamp);
|
|
3404
|
-
// Start streaming
|
|
3530
|
+
// Start session - use streaming mode if supported for ability to add messages later
|
|
3405
3531
|
try {
|
|
3406
|
-
|
|
3532
|
+
if (runner.supportsStreamingInput && runner.startStreaming) {
|
|
3533
|
+
await runner.startStreaming(fullPrompt);
|
|
3534
|
+
}
|
|
3535
|
+
else {
|
|
3536
|
+
await runner.start(fullPrompt);
|
|
3537
|
+
}
|
|
3407
3538
|
}
|
|
3408
3539
|
catch (error) {
|
|
3409
|
-
console.error(`[
|
|
3540
|
+
console.error(`[resumeAgentSession] Failed to start streaming session for ${linearAgentActivitySessionId}:`, error);
|
|
3410
3541
|
throw error;
|
|
3411
3542
|
}
|
|
3412
3543
|
}
|