cyrus-edge-worker 0.2.2 → 0.2.4

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.
@@ -1,13 +1,14 @@
1
- import { LinearDocument } from "@linear/sdk";
1
+ import { EventEmitter } from "node:events";
2
+ import { AgentActivitySignal, AgentSessionStatus, AgentSessionType, } from "cyrus-core";
2
3
  /**
3
- * Manages Linear Agent Sessions integration with Claude Code SDK
4
+ * Manages Agent Sessions integration with Claude Code SDK
4
5
  * Transforms Claude streaming messages into Agent Session format
5
6
  * Handles session lifecycle: create → active → complete/error
6
7
  *
7
8
  * CURRENTLY BEING HANDLED 'per repository'
8
9
  */
9
- export class AgentSessionManager {
10
- linearClient;
10
+ export class AgentSessionManager extends EventEmitter {
11
+ issueTracker;
11
12
  sessions = new Map();
12
13
  entries = new Map(); // Stores a list of session entries per each session by its linearAgentActivitySessionId
13
14
  activeTasksBySession = new Map(); // Maps session ID to active Task tool use ID
@@ -17,12 +18,11 @@ export class AgentSessionManager {
17
18
  sharedApplicationServer;
18
19
  getParentSessionId;
19
20
  resumeParentSession;
20
- resumeNextSubroutine;
21
- constructor(linearClient, getParentSessionId, resumeParentSession, resumeNextSubroutine, procedureRouter, sharedApplicationServer) {
22
- this.linearClient = linearClient;
21
+ constructor(issueTracker, getParentSessionId, resumeParentSession, procedureRouter, sharedApplicationServer) {
22
+ super();
23
+ this.issueTracker = issueTracker;
23
24
  this.getParentSessionId = getParentSessionId;
24
25
  this.resumeParentSession = resumeParentSession;
25
- this.resumeNextSubroutine = resumeNextSubroutine;
26
26
  this.procedureRouter = procedureRouter;
27
27
  this.sharedApplicationServer = sharedApplicationServer;
28
28
  }
@@ -34,9 +34,9 @@ export class AgentSessionManager {
34
34
  console.log(`[AgentSessionManager] Tracking Linear session ${linearAgentActivitySessionId} for issue ${issueId}`);
35
35
  const agentSession = {
36
36
  linearAgentActivitySessionId,
37
- type: LinearDocument.AgentSessionType.CommentThread,
38
- status: LinearDocument.AgentSessionStatus.Active,
39
- context: LinearDocument.AgentSessionType.CommentThread,
37
+ type: AgentSessionType.CommentThread,
38
+ status: AgentSessionStatus.Active,
39
+ context: AgentSessionType.CommentThread,
40
40
  createdAt: Date.now(),
41
41
  updatedAt: Date.now(),
42
42
  issueId,
@@ -49,7 +49,8 @@ export class AgentSessionManager {
49
49
  return agentSession;
50
50
  }
51
51
  /**
52
- * Create a new Agent Session from Claude system initialization
52
+ * Update Agent Session with session ID from system initialization
53
+ * Automatically detects whether it's Claude or Gemini based on the runner
53
54
  */
54
55
  updateAgentSessionWithClaudeSessionId(linearAgentActivitySessionId, claudeSystemMessage) {
55
56
  const linearSession = this.sessions.get(linearAgentActivitySessionId);
@@ -57,7 +58,16 @@ export class AgentSessionManager {
57
58
  console.warn(`[AgentSessionManager] No Linear session found for linearAgentActivitySessionId ${linearAgentActivitySessionId}`);
58
59
  return;
59
60
  }
60
- linearSession.claudeSessionId = claudeSystemMessage.session_id;
61
+ // Determine which runner is being used
62
+ const runner = linearSession.agentRunner;
63
+ const isGeminiRunner = runner?.constructor.name === "GeminiRunner";
64
+ // Update the appropriate session ID based on runner type
65
+ if (isGeminiRunner) {
66
+ linearSession.geminiSessionId = claudeSystemMessage.session_id;
67
+ }
68
+ else {
69
+ linearSession.claudeSessionId = claudeSystemMessage.session_id;
70
+ }
61
71
  linearSession.updatedAt = Date.now();
62
72
  linearSession.metadata = {
63
73
  ...linearSession.metadata, // Preserve existing metadata
@@ -68,17 +78,24 @@ export class AgentSessionManager {
68
78
  };
69
79
  }
70
80
  /**
71
- * Create a session entry from Claude user/assistant message (without syncing to Linear)
81
+ * Create a session entry from user/assistant message (without syncing to Linear)
72
82
  */
73
- async createSessionEntry(_linearAgentActivitySessionId, sdkMessage) {
83
+ async createSessionEntry(linearAgentActivitySessionId, sdkMessage) {
74
84
  // Extract tool info if this is an assistant message
75
85
  const toolInfo = sdkMessage.type === "assistant" ? this.extractToolInfo(sdkMessage) : null;
76
86
  // Extract tool_use_id and error status if this is a user message with tool_result
77
87
  const toolResultInfo = sdkMessage.type === "user"
78
88
  ? this.extractToolResultInfo(sdkMessage)
79
89
  : null;
90
+ // Determine which runner is being used
91
+ const session = this.sessions.get(linearAgentActivitySessionId);
92
+ const runner = session?.agentRunner;
93
+ const isGeminiRunner = runner?.constructor.name === "GeminiRunner";
80
94
  const sessionEntry = {
81
- claudeSessionId: sdkMessage.session_id,
95
+ // Set the appropriate session ID based on runner type
96
+ ...(isGeminiRunner
97
+ ? { geminiSessionId: sdkMessage.session_id }
98
+ : { claudeSessionId: sdkMessage.session_id }),
82
99
  type: sdkMessage.type,
83
100
  content: this.extractContent(sdkMessage),
84
101
  metadata: {
@@ -98,352 +115,6 @@ export class AgentSessionManager {
98
115
  // DON'T store locally yet - wait until we actually post to Linear
99
116
  return sessionEntry;
100
117
  }
101
- /**
102
- * Format TodoWrite tool parameter as a nice checklist
103
- */
104
- formatTodoWriteParameter(jsonContent) {
105
- try {
106
- const data = JSON.parse(jsonContent);
107
- if (!data.todos || !Array.isArray(data.todos)) {
108
- return jsonContent;
109
- }
110
- const todos = data.todos;
111
- // Keep original order but add status indicators
112
- let formatted = "\n";
113
- todos.forEach((todo, index) => {
114
- let statusEmoji = "";
115
- if (todo.status === "completed") {
116
- statusEmoji = "✅ ";
117
- }
118
- else if (todo.status === "in_progress") {
119
- statusEmoji = "🔄 ";
120
- }
121
- else if (todo.status === "pending") {
122
- statusEmoji = "⏳ ";
123
- }
124
- formatted += `${statusEmoji}${todo.content}`;
125
- if (index < todos.length - 1) {
126
- formatted += "\n";
127
- }
128
- });
129
- return formatted;
130
- }
131
- catch (error) {
132
- console.error("[AgentSessionManager] Failed to format TodoWrite parameter:", error);
133
- return jsonContent;
134
- }
135
- }
136
- /**
137
- * Format tool input for display in Linear agent activities
138
- * Converts raw tool inputs into user-friendly parameter strings
139
- */
140
- formatToolParameter(toolName, toolInput) {
141
- // If input is already a string, return it
142
- if (typeof toolInput === "string") {
143
- return toolInput;
144
- }
145
- try {
146
- switch (toolName) {
147
- case "Bash":
148
- case "↪ Bash": {
149
- // Show command only - description goes in action field via formatToolActionName
150
- return toolInput.command || JSON.stringify(toolInput);
151
- }
152
- case "Read":
153
- case "↪ Read":
154
- if (toolInput.file_path) {
155
- let param = toolInput.file_path;
156
- if (toolInput.offset !== undefined ||
157
- toolInput.limit !== undefined) {
158
- const start = toolInput.offset || 0;
159
- const end = toolInput.limit ? start + toolInput.limit : "end";
160
- param += ` (lines ${start + 1}-${end})`;
161
- }
162
- return param;
163
- }
164
- break;
165
- case "Edit":
166
- case "↪ Edit":
167
- if (toolInput.file_path) {
168
- return toolInput.file_path;
169
- }
170
- break;
171
- case "Write":
172
- case "↪ Write":
173
- if (toolInput.file_path) {
174
- return toolInput.file_path;
175
- }
176
- break;
177
- case "Grep":
178
- case "↪ Grep":
179
- if (toolInput.pattern) {
180
- let param = `Pattern: \`${toolInput.pattern}\``;
181
- if (toolInput.path) {
182
- param += ` in ${toolInput.path}`;
183
- }
184
- if (toolInput.glob) {
185
- param += ` (${toolInput.glob})`;
186
- }
187
- if (toolInput.type) {
188
- param += ` [${toolInput.type} files]`;
189
- }
190
- return param;
191
- }
192
- break;
193
- case "Glob":
194
- case "↪ Glob":
195
- if (toolInput.pattern) {
196
- let param = `Pattern: \`${toolInput.pattern}\``;
197
- if (toolInput.path) {
198
- param += ` in ${toolInput.path}`;
199
- }
200
- return param;
201
- }
202
- break;
203
- case "Task":
204
- case "↪ Task":
205
- if (toolInput.description) {
206
- return toolInput.description;
207
- }
208
- break;
209
- case "WebFetch":
210
- case "↪ WebFetch":
211
- if (toolInput.url) {
212
- return toolInput.url;
213
- }
214
- break;
215
- case "WebSearch":
216
- case "↪ WebSearch":
217
- if (toolInput.query) {
218
- return `Query: ${toolInput.query}`;
219
- }
220
- break;
221
- case "NotebookEdit":
222
- case "↪ NotebookEdit":
223
- if (toolInput.notebook_path) {
224
- let param = toolInput.notebook_path;
225
- if (toolInput.cell_id) {
226
- param += ` (cell ${toolInput.cell_id})`;
227
- }
228
- return param;
229
- }
230
- break;
231
- default:
232
- // For MCP tools or other unknown tools, try to extract meaningful info
233
- if (toolName.startsWith("mcp__")) {
234
- // Extract key fields that are commonly meaningful
235
- const meaningfulFields = [
236
- "query",
237
- "id",
238
- "issueId",
239
- "title",
240
- "name",
241
- "path",
242
- "file",
243
- ];
244
- for (const field of meaningfulFields) {
245
- if (toolInput[field]) {
246
- return `${field}: ${toolInput[field]}`;
247
- }
248
- }
249
- }
250
- break;
251
- }
252
- // Fallback to JSON but make it compact
253
- return JSON.stringify(toolInput);
254
- }
255
- catch (error) {
256
- console.error("[AgentSessionManager] Failed to format tool parameter:", error);
257
- return JSON.stringify(toolInput);
258
- }
259
- }
260
- /**
261
- * Format tool action name with description for Bash tool
262
- * Puts the description in round brackets after the tool name in the action field
263
- */
264
- formatToolActionName(toolName, toolInput, isError) {
265
- // Handle Bash tool with description
266
- if (toolName === "Bash" || toolName === "↪ Bash") {
267
- // Check if toolInput has a description field
268
- if (toolInput &&
269
- typeof toolInput === "object" &&
270
- "description" in toolInput &&
271
- toolInput.description) {
272
- const baseName = isError ? `${toolName} (Error)` : toolName;
273
- return `${baseName} (${toolInput.description})`;
274
- }
275
- }
276
- // Default formatting for other tools or Bash without description
277
- return isError ? `${toolName} (Error)` : toolName;
278
- }
279
- /**
280
- * Format tool result for display in Linear agent activities
281
- * Converts raw tool results into formatted Markdown
282
- */
283
- formatToolResult(toolName, toolInput, result, isError) {
284
- // If there's an error, wrap in error formatting
285
- if (isError) {
286
- return `\`\`\`\n${result}\n\`\`\``;
287
- }
288
- try {
289
- switch (toolName) {
290
- case "Bash":
291
- case "↪ Bash": {
292
- // Show command first if not already in parameter
293
- let formatted = "";
294
- if (toolInput.command && !toolInput.description) {
295
- formatted += `\`\`\`bash\n${toolInput.command}\n\`\`\`\n\n`;
296
- }
297
- // Then show output
298
- if (result?.trim()) {
299
- formatted += `\`\`\`\n${result}\n\`\`\``;
300
- }
301
- else {
302
- formatted += "*No output*";
303
- }
304
- return formatted;
305
- }
306
- case "Read":
307
- case "↪ Read":
308
- // For Read, the result is file content - use code block
309
- if (result?.trim()) {
310
- // Clean up the result: remove line numbers and system-reminder tags
311
- let cleanedResult = result;
312
- // Remove line numbers (format: " 123→")
313
- cleanedResult = cleanedResult.replace(/^\s*\d+→/gm, "");
314
- // Remove system-reminder blocks
315
- cleanedResult = cleanedResult.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "");
316
- // Trim any extra whitespace
317
- cleanedResult = cleanedResult.trim();
318
- // Try to detect language from file extension
319
- let lang = "";
320
- if (toolInput.file_path) {
321
- const ext = toolInput.file_path.split(".").pop()?.toLowerCase();
322
- const langMap = {
323
- ts: "typescript",
324
- tsx: "typescript",
325
- js: "javascript",
326
- jsx: "javascript",
327
- py: "python",
328
- rb: "ruby",
329
- go: "go",
330
- rs: "rust",
331
- java: "java",
332
- c: "c",
333
- cpp: "cpp",
334
- cs: "csharp",
335
- php: "php",
336
- swift: "swift",
337
- kt: "kotlin",
338
- scala: "scala",
339
- sh: "bash",
340
- bash: "bash",
341
- zsh: "bash",
342
- yml: "yaml",
343
- yaml: "yaml",
344
- json: "json",
345
- xml: "xml",
346
- html: "html",
347
- css: "css",
348
- scss: "scss",
349
- md: "markdown",
350
- sql: "sql",
351
- };
352
- lang = langMap[ext || ""] || "";
353
- }
354
- return `\`\`\`${lang}\n${cleanedResult}\n\`\`\``;
355
- }
356
- return "*Empty file*";
357
- case "Edit":
358
- case "↪ Edit": {
359
- // For Edit, show changes as a diff
360
- // Extract old_string and new_string from toolInput
361
- if (toolInput.old_string && toolInput.new_string) {
362
- // Format as a unified diff
363
- const oldLines = toolInput.old_string.split("\n");
364
- const newLines = toolInput.new_string.split("\n");
365
- let diff = "```diff\n";
366
- // Add context lines before changes (show all old lines with - prefix)
367
- for (const line of oldLines) {
368
- diff += `-${line}\n`;
369
- }
370
- // Add new lines with + prefix
371
- for (const line of newLines) {
372
- diff += `+${line}\n`;
373
- }
374
- diff += "```";
375
- return diff;
376
- }
377
- // Fallback to result if old/new strings not available
378
- if (result?.trim()) {
379
- return result;
380
- }
381
- return "*Edit completed*";
382
- }
383
- case "Write":
384
- case "↪ Write":
385
- // For Write, just confirm
386
- if (result?.trim()) {
387
- return result; // In case there's an error or message
388
- }
389
- return "*File written successfully*";
390
- case "Grep":
391
- case "↪ Grep": {
392
- // Format grep results
393
- if (result?.trim()) {
394
- const lines = result.split("\n");
395
- // If it looks like file paths (files_with_matches mode)
396
- if (lines.length > 0 &&
397
- lines[0] &&
398
- !lines[0].includes(":") &&
399
- lines[0].trim().length > 0) {
400
- return `Found ${lines.filter((l) => l.trim()).length} matching files:\n\`\`\`\n${result}\n\`\`\``;
401
- }
402
- // Otherwise it's content matches
403
- return `\`\`\`\n${result}\n\`\`\``;
404
- }
405
- return "*No matches found*";
406
- }
407
- case "Glob":
408
- case "↪ Glob": {
409
- if (result?.trim()) {
410
- const lines = result.split("\n").filter((l) => l.trim());
411
- return `Found ${lines.length} matching files:\n\`\`\`\n${result}\n\`\`\``;
412
- }
413
- return "*No files found*";
414
- }
415
- case "Task":
416
- case "↪ Task":
417
- // Task results can be complex - keep as is but in code block if multiline
418
- if (result?.trim()) {
419
- if (result.includes("\n")) {
420
- return `\`\`\`\n${result}\n\`\`\``;
421
- }
422
- return result;
423
- }
424
- return "*Task completed*";
425
- case "WebFetch":
426
- case "↪ WebFetch":
427
- case "WebSearch":
428
- case "↪ WebSearch":
429
- // Web results are usually formatted, keep as is
430
- return result || "*No results*";
431
- default:
432
- // For unknown tools, use code block if result has multiple lines
433
- if (result?.trim()) {
434
- if (result.includes("\n") && result.length > 100) {
435
- return `\`\`\`\n${result}\n\`\`\``;
436
- }
437
- return result;
438
- }
439
- return "*Completed*";
440
- }
441
- }
442
- catch (error) {
443
- console.error("[AgentSessionManager] Failed to format tool result:", error);
444
- return result || "";
445
- }
446
- }
447
118
  /**
448
119
  * Complete a session from Claude result message
449
120
  */
@@ -459,8 +130,8 @@ export class AgentSessionManager {
459
130
  // Note: We should ideally track by session, but for now clearing all is safer
460
131
  // to prevent memory leaks
461
132
  const status = resultMessage.subtype === "success"
462
- ? LinearDocument.AgentSessionStatus.Complete
463
- : LinearDocument.AgentSessionStatus.Error;
133
+ ? AgentSessionStatus.Complete
134
+ : AgentSessionStatus.Error;
464
135
  // Update session status and metadata
465
136
  await this.updateSessionStatus(linearAgentActivitySessionId, status, {
466
137
  totalCostUsd: resultMessage.total_cost_usd,
@@ -483,9 +154,10 @@ export class AgentSessionManager {
483
154
  console.log(`[AgentSessionManager] Subroutine completed with error, not triggering next subroutine`);
484
155
  return;
485
156
  }
486
- const claudeSessionId = session.claudeSessionId;
487
- if (!claudeSessionId) {
488
- console.error(`[AgentSessionManager] No Claude session ID found for procedure session`);
157
+ // Get the session ID (either Claude or Gemini)
158
+ const sessionId = session.claudeSessionId || session.geminiSessionId;
159
+ if (!sessionId) {
160
+ console.error(`[AgentSessionManager] No session ID found for procedure session`);
489
161
  return;
490
162
  }
491
163
  // Check if there's a next subroutine
@@ -546,16 +218,13 @@ export class AgentSessionManager {
546
218
  }
547
219
  // Advance procedure state
548
220
  console.log(`[AgentSessionManager] Subroutine completed, advancing to next: ${nextSubroutine.name}`);
549
- this.procedureRouter.advanceToNextSubroutine(session, claudeSessionId);
550
- // Trigger next subroutine
551
- if (this.resumeNextSubroutine) {
552
- try {
553
- await this.resumeNextSubroutine(linearAgentActivitySessionId);
554
- }
555
- catch (error) {
556
- console.error(`[AgentSessionManager] Failed to trigger next subroutine:`, error);
557
- }
558
- }
221
+ this.procedureRouter.advanceToNextSubroutine(session, sessionId);
222
+ // Emit event for EdgeWorker to handle subroutine transition
223
+ // This replaces the callback pattern and allows EdgeWorker to subscribe
224
+ this.emit("subroutineComplete", {
225
+ linearAgentActivitySessionId,
226
+ session,
227
+ });
559
228
  }
560
229
  else {
561
230
  // Procedure complete - post final result
@@ -633,7 +302,7 @@ export class AgentSessionManager {
633
302
  catch (error) {
634
303
  console.error(`[AgentSessionManager] Error handling message:`, error);
635
304
  // Mark session as error state
636
- await this.updateSessionStatus(linearAgentActivitySessionId, LinearDocument.AgentSessionStatus.Error);
305
+ await this.updateSessionStatus(linearAgentActivitySessionId, AgentSessionStatus.Error);
637
306
  }
638
307
  }
639
308
  /**
@@ -651,11 +320,18 @@ export class AgentSessionManager {
651
320
  this.sessions.set(linearAgentActivitySessionId, session);
652
321
  }
653
322
  /**
654
- * Add result entry from Claude result message
323
+ * Add result entry from result message
655
324
  */
656
325
  async addResultEntry(linearAgentActivitySessionId, resultMessage) {
326
+ // Determine which runner is being used
327
+ const session = this.sessions.get(linearAgentActivitySessionId);
328
+ const runner = session?.agentRunner;
329
+ const isGeminiRunner = runner?.constructor.name === "GeminiRunner";
657
330
  const resultEntry = {
658
- claudeSessionId: resultMessage.session_id,
331
+ // Set the appropriate session ID based on runner type
332
+ ...(isGeminiRunner
333
+ ? { geminiSessionId: resultMessage.session_id }
334
+ : { claudeSessionId: resultMessage.session_id }),
659
335
  type: "result",
660
336
  content: "result" in resultMessage ? resultMessage.result : "",
661
337
  metadata: {
@@ -798,15 +474,23 @@ export class AgentSessionManager {
798
474
  if (entry.metadata.toolUseId) {
799
475
  this.toolCallsByToolUseId.delete(entry.metadata.toolUseId);
800
476
  }
801
- // Skip creating activity for TodoWrite results since TodoWrite already created a non-ephemeral thought
802
- if (toolName === "TodoWrite" || toolName === "↪ TodoWrite") {
477
+ // Skip creating activity for TodoWrite/write_todos results since they already created a non-ephemeral thought
478
+ if (toolName === "TodoWrite" ||
479
+ toolName === "↪ TodoWrite" ||
480
+ toolName === "write_todos") {
481
+ return;
482
+ }
483
+ // Get formatter from runner
484
+ const formatter = session.agentRunner?.getFormatter();
485
+ if (!formatter) {
486
+ console.warn(`[AgentSessionManager] No formatter available for session ${linearAgentActivitySessionId}`);
803
487
  return;
804
488
  }
805
- // Format parameter and result using our formatters
806
- const formattedParameter = this.formatToolParameter(toolName, toolInput);
807
- const formattedResult = this.formatToolResult(toolName, toolInput, toolResult.content?.trim() || "", toolResult.isError);
489
+ // Format parameter and result using runner's formatter
490
+ const formattedParameter = formatter.formatToolParameter(toolName, toolInput);
491
+ const formattedResult = formatter.formatToolResult(toolName, toolInput, toolResult.content?.trim() || "", toolResult.isError);
808
492
  // Format the action name (with description for Bash tool)
809
- const formattedAction = this.formatToolActionName(toolName, toolInput, toolResult.isError);
493
+ const formattedAction = formatter.formatToolActionName(toolName, toolInput, toolResult.isError);
810
494
  content = {
811
495
  type: "action",
812
496
  action: formattedAction,
@@ -842,20 +526,32 @@ export class AgentSessionManager {
842
526
  input: entry.metadata.toolInput || entry.content,
843
527
  });
844
528
  }
845
- // Special handling for TodoWrite tool - treat as thought instead of action
846
- if (toolName === "TodoWrite") {
847
- const formattedTodos = this.formatTodoWriteParameter(entry.content);
529
+ // Special handling for TodoWrite tool (Claude) and write_todos (Gemini) - treat as thought instead of action
530
+ if (toolName === "TodoWrite" || toolName === "write_todos") {
531
+ // Get formatter from runner
532
+ const formatter = session.agentRunner?.getFormatter();
533
+ if (!formatter) {
534
+ console.warn(`[AgentSessionManager] No formatter available for session ${linearAgentActivitySessionId}`);
535
+ return;
536
+ }
537
+ const formattedTodos = formatter.formatTodoWriteParameter(entry.content);
848
538
  content = {
849
539
  type: "thought",
850
540
  body: formattedTodos,
851
541
  };
852
- // TodoWrite is not ephemeral
542
+ // TodoWrite/write_todos is not ephemeral
853
543
  ephemeral = false;
854
544
  }
855
545
  else if (toolName === "Task") {
546
+ // Get formatter from runner
547
+ const formatter = session.agentRunner?.getFormatter();
548
+ if (!formatter) {
549
+ console.warn(`[AgentSessionManager] No formatter available for session ${linearAgentActivitySessionId}`);
550
+ return;
551
+ }
856
552
  // Special handling for Task tool - add start marker and track active task
857
553
  const toolInput = entry.metadata.toolInput || entry.content;
858
- const formattedParameter = this.formatToolParameter(toolName, toolInput);
554
+ const formattedParameter = formatter.formatToolParameter(toolName, toolInput);
859
555
  const displayName = toolName;
860
556
  // Track this as the active Task for this session
861
557
  if (entry.metadata?.toolUseId) {
@@ -871,6 +567,12 @@ export class AgentSessionManager {
871
567
  ephemeral = false;
872
568
  }
873
569
  else {
570
+ // Get formatter from runner
571
+ const formatter = session.agentRunner?.getFormatter();
572
+ if (!formatter) {
573
+ console.warn(`[AgentSessionManager] No formatter available for session ${linearAgentActivitySessionId}`);
574
+ return;
575
+ }
874
576
  // Other tools - check if they're within an active Task
875
577
  const toolInput = entry.metadata.toolInput || entry.content;
876
578
  let displayName = toolName;
@@ -880,7 +582,7 @@ export class AgentSessionManager {
880
582
  displayName = `↪ ${toolName}`;
881
583
  }
882
584
  }
883
- const formattedParameter = this.formatToolParameter(displayName, toolInput);
585
+ const formattedParameter = formatter.formatToolParameter(displayName, toolInput);
884
586
  content = {
885
587
  type: "action",
886
588
  action: displayName,
@@ -944,7 +646,7 @@ export class AgentSessionManager {
944
646
  content,
945
647
  ...(ephemeral && { ephemeral: true }),
946
648
  };
947
- const result = await this.linearClient.createAgentActivity(activityInput);
649
+ const result = await this.issueTracker.createAgentActivity(activityInput);
948
650
  if (result.success && result.agentActivity) {
949
651
  const agentActivity = await result.agentActivity;
950
652
  entry.linearAgentActivityId = agentActivity.id;
@@ -974,36 +676,36 @@ export class AgentSessionManager {
974
676
  * Get all active sessions
975
677
  */
976
678
  getActiveSessions() {
977
- return Array.from(this.sessions.values()).filter((session) => session.status === LinearDocument.AgentSessionStatus.Active);
679
+ return Array.from(this.sessions.values()).filter((session) => session.status === AgentSessionStatus.Active);
978
680
  }
979
681
  /**
980
- * Add or update ClaudeRunner for a session
682
+ * Add or update agent runner for a session
981
683
  */
982
- addClaudeRunner(linearAgentActivitySessionId, claudeRunner) {
684
+ addAgentRunner(linearAgentActivitySessionId, agentRunner) {
983
685
  const session = this.sessions.get(linearAgentActivitySessionId);
984
686
  if (!session) {
985
687
  console.warn(`[AgentSessionManager] No session found for linearAgentActivitySessionId ${linearAgentActivitySessionId}`);
986
688
  return;
987
689
  }
988
- session.claudeRunner = claudeRunner;
690
+ session.agentRunner = agentRunner;
989
691
  session.updatedAt = Date.now();
990
- console.log(`[AgentSessionManager] Added ClaudeRunner to session ${linearAgentActivitySessionId}`);
692
+ console.log(`[AgentSessionManager] Added agent runner to session ${linearAgentActivitySessionId}`);
991
693
  }
992
694
  /**
993
- * Get all ClaudeRunners
695
+ * Get all agent runners
994
696
  */
995
- getAllClaudeRunners() {
697
+ getAllAgentRunners() {
996
698
  return Array.from(this.sessions.values())
997
- .map((session) => session.claudeRunner)
699
+ .map((session) => session.agentRunner)
998
700
  .filter((runner) => runner !== undefined);
999
701
  }
1000
702
  /**
1001
- * Get all ClaudeRunners for a specific issue
703
+ * Get all agent runners for a specific issue
1002
704
  */
1003
- getClaudeRunnersForIssue(issueId) {
705
+ getAgentRunnersForIssue(issueId) {
1004
706
  return Array.from(this.sessions.values())
1005
707
  .filter((session) => session.issueId === issueId)
1006
- .map((session) => session.claudeRunner)
708
+ .map((session) => session.agentRunner)
1007
709
  .filter((runner) => runner !== undefined);
1008
710
  }
1009
711
  /**
@@ -1017,7 +719,7 @@ export class AgentSessionManager {
1017
719
  */
1018
720
  getActiveSessionsByIssueId(issueId) {
1019
721
  return Array.from(this.sessions.values()).filter((session) => session.issueId === issueId &&
1020
- session.status === LinearDocument.AgentSessionStatus.Active);
722
+ session.status === AgentSessionStatus.Active);
1021
723
  }
1022
724
  /**
1023
725
  * Get all sessions
@@ -1026,18 +728,18 @@ export class AgentSessionManager {
1026
728
  return Array.from(this.sessions.values());
1027
729
  }
1028
730
  /**
1029
- * Get ClaudeRunner for a specific session
731
+ * Get agent runner for a specific session
1030
732
  */
1031
- getClaudeRunner(linearAgentActivitySessionId) {
733
+ getAgentRunner(linearAgentActivitySessionId) {
1032
734
  const session = this.sessions.get(linearAgentActivitySessionId);
1033
- return session?.claudeRunner;
735
+ return session?.agentRunner;
1034
736
  }
1035
737
  /**
1036
- * Check if a ClaudeRunner exists for a session
738
+ * Check if an agent runner exists for a session
1037
739
  */
1038
- hasClaudeRunner(linearAgentActivitySessionId) {
740
+ hasAgentRunner(linearAgentActivitySessionId) {
1039
741
  const session = this.sessions.get(linearAgentActivitySessionId);
1040
- return session?.claudeRunner !== undefined;
742
+ return session?.agentRunner !== undefined;
1041
743
  }
1042
744
  /**
1043
745
  * Create a thought activity
@@ -1049,7 +751,7 @@ export class AgentSessionManager {
1049
751
  return;
1050
752
  }
1051
753
  try {
1052
- const result = await this.linearClient.createAgentActivity({
754
+ const result = await this.issueTracker.createAgentActivity({
1053
755
  agentSessionId: session.linearAgentActivitySessionId,
1054
756
  content: {
1055
757
  type: "thought",
@@ -1085,7 +787,7 @@ export class AgentSessionManager {
1085
787
  if (result !== undefined) {
1086
788
  content.result = result;
1087
789
  }
1088
- const response = await this.linearClient.createAgentActivity({
790
+ const response = await this.issueTracker.createAgentActivity({
1089
791
  agentSessionId: session.linearAgentActivitySessionId,
1090
792
  content,
1091
793
  });
@@ -1110,7 +812,7 @@ export class AgentSessionManager {
1110
812
  return;
1111
813
  }
1112
814
  try {
1113
- const result = await this.linearClient.createAgentActivity({
815
+ const result = await this.issueTracker.createAgentActivity({
1114
816
  agentSessionId: session.linearAgentActivitySessionId,
1115
817
  content: {
1116
818
  type: "response",
@@ -1138,7 +840,7 @@ export class AgentSessionManager {
1138
840
  return;
1139
841
  }
1140
842
  try {
1141
- const result = await this.linearClient.createAgentActivity({
843
+ const result = await this.issueTracker.createAgentActivity({
1142
844
  agentSessionId: session.linearAgentActivitySessionId,
1143
845
  content: {
1144
846
  type: "error",
@@ -1166,7 +868,7 @@ export class AgentSessionManager {
1166
868
  return;
1167
869
  }
1168
870
  try {
1169
- const result = await this.linearClient.createAgentActivity({
871
+ const result = await this.issueTracker.createAgentActivity({
1170
872
  agentSessionId: session.linearAgentActivitySessionId,
1171
873
  content: {
1172
874
  type: "elicitation",
@@ -1194,13 +896,13 @@ export class AgentSessionManager {
1194
896
  return;
1195
897
  }
1196
898
  try {
1197
- const result = await this.linearClient.createAgentActivity({
899
+ const result = await this.issueTracker.createAgentActivity({
1198
900
  agentSessionId: session.linearAgentActivitySessionId,
1199
901
  content: {
1200
902
  type: "elicitation",
1201
903
  body,
1202
904
  },
1203
- signal: LinearDocument.AgentActivitySignal.Auth,
905
+ signal: AgentActivitySignal.Auth,
1204
906
  signalMetadata: {
1205
907
  url: approvalUrl,
1206
908
  },
@@ -1238,8 +940,8 @@ export class AgentSessionManager {
1238
940
  const entries = {};
1239
941
  // Serialize sessions
1240
942
  for (const [sessionId, session] of this.sessions.entries()) {
1241
- // Exclude claudeRunner from serialization as it's not serializable
1242
- const { claudeRunner: _claudeRunner, ...serializableSession } = session;
943
+ // Exclude agentRunner from serialization as it's not serializable
944
+ const { agentRunner: _agentRunner, ...serializableSession } = session;
1243
945
  sessions[sessionId] = serializableSession;
1244
946
  }
1245
947
  // Serialize entries
@@ -1278,7 +980,7 @@ export class AgentSessionManager {
1278
980
  */
1279
981
  async postModelNotificationThought(linearAgentActivitySessionId, model) {
1280
982
  try {
1281
- const result = await this.linearClient.createAgentActivity({
983
+ const result = await this.issueTracker.createAgentActivity({
1282
984
  agentSessionId: linearAgentActivitySessionId,
1283
985
  content: {
1284
986
  type: "thought",
@@ -1301,7 +1003,7 @@ export class AgentSessionManager {
1301
1003
  */
1302
1004
  async postRoutingThought(linearAgentActivitySessionId) {
1303
1005
  try {
1304
- const result = await this.linearClient.createAgentActivity({
1006
+ const result = await this.issueTracker.createAgentActivity({
1305
1007
  agentSessionId: linearAgentActivitySessionId,
1306
1008
  content: {
1307
1009
  type: "thought",
@@ -1329,7 +1031,7 @@ export class AgentSessionManager {
1329
1031
  */
1330
1032
  async postProcedureSelectionThought(linearAgentActivitySessionId, procedureName, classification) {
1331
1033
  try {
1332
- const result = await this.linearClient.createAgentActivity({
1034
+ const result = await this.issueTracker.createAgentActivity({
1333
1035
  agentSessionId: linearAgentActivitySessionId,
1334
1036
  content: {
1335
1037
  type: "thought",
@@ -1360,7 +1062,7 @@ export class AgentSessionManager {
1360
1062
  try {
1361
1063
  if (message.status === "compacting") {
1362
1064
  // Create an ephemeral thought for the compacting status
1363
- const result = await this.linearClient.createAgentActivity({
1065
+ const result = await this.issueTracker.createAgentActivity({
1364
1066
  agentSessionId: session.linearAgentActivitySessionId,
1365
1067
  content: {
1366
1068
  type: "thought",
@@ -1380,7 +1082,7 @@ export class AgentSessionManager {
1380
1082
  }
1381
1083
  else if (message.status === null) {
1382
1084
  // Clear the status - post a non-ephemeral thought to replace the ephemeral one
1383
- const result = await this.linearClient.createAgentActivity({
1085
+ const result = await this.issueTracker.createAgentActivity({
1384
1086
  agentSessionId: session.linearAgentActivitySessionId,
1385
1087
  content: {
1386
1088
  type: "thought",