@agentmeshhq/agent 0.1.7 → 0.1.9

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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/dist/__tests__/context.test.d.ts +1 -0
  3. package/dist/__tests__/context.test.js +353 -0
  4. package/dist/__tests__/context.test.js.map +1 -0
  5. package/dist/__tests__/runner.test.d.ts +1 -0
  6. package/dist/__tests__/runner.test.js +87 -0
  7. package/dist/__tests__/runner.test.js.map +1 -0
  8. package/dist/cli/context.d.ts +4 -0
  9. package/dist/cli/context.js +190 -0
  10. package/dist/cli/context.js.map +1 -0
  11. package/dist/cli/index.js +20 -2
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/cli/restart.d.ts +4 -1
  14. package/dist/cli/restart.js +4 -2
  15. package/dist/cli/restart.js.map +1 -1
  16. package/dist/cli/start.d.ts +1 -0
  17. package/dist/cli/start.js +10 -5
  18. package/dist/cli/start.js.map +1 -1
  19. package/dist/cli/status.js +5 -1
  20. package/dist/cli/status.js.map +1 -1
  21. package/dist/cli/whoami.js +17 -0
  22. package/dist/cli/whoami.js.map +1 -1
  23. package/dist/config/schema.d.ts +5 -0
  24. package/dist/context/handoff.d.ts +48 -0
  25. package/dist/context/handoff.js +88 -0
  26. package/dist/context/handoff.js.map +1 -0
  27. package/dist/context/index.d.ts +7 -0
  28. package/dist/context/index.js +8 -0
  29. package/dist/context/index.js.map +1 -0
  30. package/dist/context/schema.d.ts +82 -0
  31. package/dist/context/schema.js +33 -0
  32. package/dist/context/schema.js.map +1 -0
  33. package/dist/context/storage.d.ts +49 -0
  34. package/dist/context/storage.js +172 -0
  35. package/dist/context/storage.js.map +1 -0
  36. package/dist/core/daemon.d.ts +12 -0
  37. package/dist/core/daemon.js +108 -4
  38. package/dist/core/daemon.js.map +1 -1
  39. package/dist/core/heartbeat.d.ts +6 -0
  40. package/dist/core/heartbeat.js +8 -0
  41. package/dist/core/heartbeat.js.map +1 -1
  42. package/dist/core/injector.d.ts +9 -0
  43. package/dist/core/injector.js +55 -3
  44. package/dist/core/injector.js.map +1 -1
  45. package/dist/core/runner.d.ts +49 -0
  46. package/dist/core/runner.js +148 -0
  47. package/dist/core/runner.js.map +1 -0
  48. package/dist/core/tmux.d.ts +16 -0
  49. package/dist/core/tmux.js +85 -7
  50. package/dist/core/tmux.js.map +1 -1
  51. package/package.json +11 -11
  52. package/src/__tests__/context.test.ts +464 -0
  53. package/src/__tests__/runner.test.ts +105 -0
  54. package/src/cli/context.ts +232 -0
  55. package/src/cli/index.ts +20 -2
  56. package/src/cli/restart.ts +9 -2
  57. package/src/cli/start.ts +11 -9
  58. package/src/cli/status.ts +6 -1
  59. package/src/cli/whoami.ts +18 -0
  60. package/src/config/schema.ts +6 -0
  61. package/src/context/handoff.ts +122 -0
  62. package/src/context/index.ts +8 -0
  63. package/src/context/schema.ts +111 -0
  64. package/src/context/storage.ts +197 -0
  65. package/src/core/daemon.ts +121 -1
  66. package/src/core/heartbeat.ts +13 -0
  67. package/src/core/injector.ts +74 -30
  68. package/src/core/runner.ts +200 -0
  69. package/src/core/tmux.ts +103 -9
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Context Storage Module
3
+ * Handles saving and loading agent context to/from disk
4
+ */
5
+
6
+ import * as fs from "node:fs";
7
+ import * as path from "node:path";
8
+ import { type AgentContext, CONTEXT_DIR, CONTEXT_VERSION, createEmptyContext } from "./schema.js";
9
+
10
+ /**
11
+ * Ensures the context directory exists
12
+ */
13
+ export function ensureContextDir(): void {
14
+ if (!fs.existsSync(CONTEXT_DIR)) {
15
+ fs.mkdirSync(CONTEXT_DIR, { recursive: true });
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Gets the context file path for an agent
21
+ */
22
+ export function getContextPath(agentId: string): string {
23
+ return path.join(CONTEXT_DIR, `${agentId}.json`);
24
+ }
25
+
26
+ /**
27
+ * Saves agent context to disk
28
+ */
29
+ export function saveContext(context: AgentContext): void {
30
+ ensureContextDir();
31
+ const contextPath = getContextPath(context.agentId);
32
+
33
+ // Update savedAt timestamp
34
+ context.savedAt = new Date().toISOString();
35
+
36
+ fs.writeFileSync(contextPath, JSON.stringify(context, null, 2));
37
+ }
38
+
39
+ /**
40
+ * Loads agent context from disk
41
+ * Returns null if context doesn't exist or is invalid
42
+ */
43
+ export function loadContext(agentId: string): AgentContext | null {
44
+ const contextPath = getContextPath(agentId);
45
+
46
+ try {
47
+ if (!fs.existsSync(contextPath)) {
48
+ return null;
49
+ }
50
+
51
+ const content = fs.readFileSync(contextPath, "utf-8");
52
+ const context = JSON.parse(content) as AgentContext;
53
+
54
+ // Validate version
55
+ if (context.version !== CONTEXT_VERSION) {
56
+ // In the future, we can migrate old versions here
57
+ console.warn(`Context version mismatch: expected ${CONTEXT_VERSION}, got ${context.version}`);
58
+ return null;
59
+ }
60
+
61
+ return context;
62
+ } catch (error) {
63
+ console.error(`Failed to load context for agent ${agentId}:`, error);
64
+ return null;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Loads context or creates a new empty one
70
+ */
71
+ export function loadOrCreateContext(agentId: string, agentName: string): AgentContext {
72
+ const existing = loadContext(agentId);
73
+ if (existing) {
74
+ return existing;
75
+ }
76
+ return createEmptyContext(agentId, agentName);
77
+ }
78
+
79
+ /**
80
+ * Deletes agent context from disk
81
+ */
82
+ export function deleteContext(agentId: string): boolean {
83
+ const contextPath = getContextPath(agentId);
84
+
85
+ try {
86
+ if (fs.existsSync(contextPath)) {
87
+ fs.unlinkSync(contextPath);
88
+ return true;
89
+ }
90
+ return false;
91
+ } catch (error) {
92
+ console.error(`Failed to delete context for agent ${agentId}:`, error);
93
+ return false;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Lists all saved context files
99
+ */
100
+ export function listContexts(): Array<{ agentId: string; savedAt: string }> {
101
+ ensureContextDir();
102
+
103
+ try {
104
+ const files = fs.readdirSync(CONTEXT_DIR);
105
+ const contexts: Array<{ agentId: string; savedAt: string }> = [];
106
+
107
+ for (const file of files) {
108
+ if (!file.endsWith(".json")) continue;
109
+
110
+ const agentId = file.replace(".json", "");
111
+ const context = loadContext(agentId);
112
+ if (context) {
113
+ contexts.push({
114
+ agentId,
115
+ savedAt: context.savedAt,
116
+ });
117
+ }
118
+ }
119
+
120
+ return contexts.sort((a, b) => new Date(b.savedAt).getTime() - new Date(a.savedAt).getTime());
121
+ } catch {
122
+ return [];
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Updates specific fields in the context
128
+ */
129
+ export function updateContext(
130
+ agentId: string,
131
+ updates: Partial<Omit<AgentContext, "version" | "agentId" | "savedAt">>,
132
+ ): AgentContext | null {
133
+ const context = loadContext(agentId);
134
+ if (!context) {
135
+ return null;
136
+ }
137
+
138
+ const updated: AgentContext = {
139
+ ...context,
140
+ ...updates,
141
+ // Preserve these fields
142
+ version: CONTEXT_VERSION,
143
+ agentId: context.agentId,
144
+ savedAt: new Date().toISOString(),
145
+ };
146
+
147
+ saveContext(updated);
148
+ return updated;
149
+ }
150
+
151
+ /**
152
+ * Exports context to a specified file path
153
+ */
154
+ export function exportContext(agentId: string, outputPath: string): boolean {
155
+ const context = loadContext(agentId);
156
+ if (!context) {
157
+ return false;
158
+ }
159
+
160
+ try {
161
+ fs.writeFileSync(outputPath, JSON.stringify(context, null, 2));
162
+ return true;
163
+ } catch (error) {
164
+ console.error(`Failed to export context:`, error);
165
+ return false;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Imports context from a file path
171
+ */
172
+ export function importContext(inputPath: string): AgentContext | null {
173
+ try {
174
+ if (!fs.existsSync(inputPath)) {
175
+ console.error(`File not found: ${inputPath}`);
176
+ return null;
177
+ }
178
+
179
+ const content = fs.readFileSync(inputPath, "utf-8");
180
+ const context = JSON.parse(content) as AgentContext;
181
+
182
+ // Validate required fields
183
+ if (!context.agentId || !context.agentName) {
184
+ console.error("Invalid context file: missing agentId or agentName");
185
+ return null;
186
+ }
187
+
188
+ // Update version and save
189
+ context.version = CONTEXT_VERSION;
190
+ saveContext(context);
191
+
192
+ return context;
193
+ } catch (error) {
194
+ console.error(`Failed to import context:`, error);
195
+ return null;
196
+ }
197
+ }
@@ -2,14 +2,18 @@ import {
2
2
  addAgentToState,
3
3
  getAgentState,
4
4
  loadConfig,
5
+ loadState,
5
6
  removeAgentFromState,
6
7
  updateAgentInState,
7
8
  } from "../config/loader.js";
8
9
  import type { AgentConfig, Config } from "../config/schema.js";
10
+ import { loadContext, loadOrCreateContext, saveContext } from "../context/index.js";
9
11
  import { Heartbeat } from "./heartbeat.js";
10
- import { handleWebSocketEvent, injectStartupMessage } from "./injector.js";
12
+ import { handleWebSocketEvent, injectRestoredContext, injectStartupMessage } from "./injector.js";
11
13
  import { checkInbox, registerAgent } from "./registry.js";
14
+ import { buildRunnerConfig, getRunnerDisplayName, type RunnerConfig } from "./runner.js";
12
15
  import {
16
+ captureSessionContext,
13
17
  createSession,
14
18
  destroySession,
15
19
  getSessionName,
@@ -24,17 +28,21 @@ export interface DaemonOptions {
24
28
  workdir?: string;
25
29
  model?: string;
26
30
  daemonize?: boolean;
31
+ /** Whether to restore context from previous session (default: true) */
32
+ restoreContext?: boolean;
27
33
  }
28
34
 
29
35
  export class AgentDaemon {
30
36
  private agentName: string;
31
37
  private config: Config;
32
38
  private agentConfig: AgentConfig;
39
+ private runnerConfig: RunnerConfig;
33
40
  private ws: AgentWebSocket | null = null;
34
41
  private heartbeat: Heartbeat | null = null;
35
42
  private token: string | null = null;
36
43
  private agentId: string | null = null;
37
44
  private isRunning = false;
45
+ private shouldRestoreContext: boolean;
38
46
 
39
47
  constructor(options: DaemonOptions) {
40
48
  const config = loadConfig();
@@ -44,6 +52,7 @@ export class AgentDaemon {
44
52
 
45
53
  this.config = config;
46
54
  this.agentName = options.name;
55
+ this.shouldRestoreContext = options.restoreContext !== false;
47
56
 
48
57
  // Find or create agent config
49
58
  let agentConfig = config.agents.find((a) => a.name === options.name);
@@ -63,6 +72,58 @@ export class AgentDaemon {
63
72
  if (options.model) agentConfig.model = options.model;
64
73
 
65
74
  this.agentConfig = agentConfig;
75
+
76
+ // Build runner configuration with model resolution
77
+ this.runnerConfig = buildRunnerConfig({
78
+ cliModel: options.model,
79
+ agentModel: agentConfig.model,
80
+ defaultModel: config.defaults.model,
81
+ command: agentConfig.command,
82
+ });
83
+
84
+ const runnerName = getRunnerDisplayName(this.runnerConfig.type);
85
+ console.log(`Runner: ${runnerName}`);
86
+ console.log(`Effective model: ${this.runnerConfig.model}`);
87
+
88
+ // Check workdir conflicts - prevent multiple agents from using same directory
89
+ this.checkWorkdirConflict(agentConfig.workdir);
90
+ }
91
+
92
+ /**
93
+ * Checks if another agent is already using the specified workdir
94
+ */
95
+ private checkWorkdirConflict(workdir?: string): void {
96
+ if (!workdir) return;
97
+
98
+ const state = loadState();
99
+ const conflictingAgent = state.agents.find((a) => {
100
+ // Skip self
101
+ if (a.name === this.agentName) return false;
102
+
103
+ // Check if agent is actually running
104
+ if (!a.pid) return false;
105
+ try {
106
+ process.kill(a.pid, 0); // Check if process exists
107
+ } catch {
108
+ return false; // Process not running
109
+ }
110
+
111
+ // Check if session exists and has same workdir
112
+ // We need to check the config for this agent's workdir
113
+ const otherAgentConfig = this.config.agents.find((c) => c.name === a.name);
114
+ if (otherAgentConfig?.workdir === workdir) {
115
+ return true;
116
+ }
117
+
118
+ return false;
119
+ });
120
+
121
+ if (conflictingAgent) {
122
+ throw new Error(
123
+ `Workdir conflict: Agent "${conflictingAgent.name}" is already using "${workdir}".\n` +
124
+ `Use a different --workdir or stop the other agent first.`,
125
+ );
126
+ }
66
127
  }
67
128
 
68
129
  async start(): Promise<void> {
@@ -84,6 +145,7 @@ export class AgentDaemon {
84
145
  this.agentName,
85
146
  this.agentConfig.command,
86
147
  this.agentConfig.workdir,
148
+ this.runnerConfig.env, // Pass runner env vars (e.g., OPENCODE_MODEL)
87
149
  );
88
150
 
89
151
  if (!created) {
@@ -91,6 +153,10 @@ export class AgentDaemon {
91
153
  }
92
154
  } else {
93
155
  console.log(`Reconnecting to existing session: ${sessionName}`);
156
+ // Update environment for existing session
157
+ if (Object.keys(this.runnerConfig.env).length > 0) {
158
+ updateSessionEnvironment(this.agentName, this.runnerConfig.env);
159
+ }
94
160
  }
95
161
 
96
162
  // Register with hub
@@ -126,6 +192,8 @@ export class AgentDaemon {
126
192
  tmuxSession: sessionName,
127
193
  startedAt: new Date().toISOString(),
128
194
  token: this.token,
195
+ runtimeModel: this.runnerConfig.model,
196
+ runnerType: this.runnerConfig.type,
129
197
  });
130
198
 
131
199
  // Start heartbeat with auto-refresh
@@ -141,6 +209,11 @@ export class AgentDaemon {
141
209
  onError: (error) => {
142
210
  console.error("Heartbeat error:", error.message);
143
211
  },
212
+ onContextSave: () => {
213
+ // Periodically save context (every 5 heartbeats = ~2.5 minutes)
214
+ this.saveAgentContext();
215
+ },
216
+ contextSaveFrequency: 5,
144
217
  onTokenRefresh: (newToken) => {
145
218
  this.token = newToken;
146
219
  // Update state file
@@ -210,6 +283,18 @@ export class AgentDaemon {
210
283
  injectStartupMessage(this.agentName, 0);
211
284
  }
212
285
 
286
+ // Restore context from previous session
287
+ if (this.shouldRestoreContext && this.agentId) {
288
+ console.log("Checking for previous context...");
289
+ const savedContext = loadContext(this.agentId);
290
+ if (savedContext) {
291
+ console.log(`Restoring context from ${savedContext.savedAt}`);
292
+ // Wait a moment for the session to be ready
293
+ await new Promise((resolve) => setTimeout(resolve, 1000));
294
+ injectRestoredContext(this.agentName, savedContext);
295
+ }
296
+ }
297
+
213
298
  this.isRunning = true;
214
299
 
215
300
  console.log(`
@@ -235,6 +320,12 @@ Nudge agent:
235
320
 
236
321
  this.isRunning = false;
237
322
 
323
+ // Save context before stopping
324
+ if (this.agentId) {
325
+ console.log("Saving agent context...");
326
+ this.saveAgentContext();
327
+ }
328
+
238
329
  // Stop heartbeat
239
330
  if (this.heartbeat) {
240
331
  this.heartbeat.stop();
@@ -256,4 +347,33 @@ Nudge agent:
256
347
  console.log("Agent stopped.");
257
348
  process.exit(0);
258
349
  }
350
+
351
+ /**
352
+ * Saves the current agent context to disk
353
+ */
354
+ private saveAgentContext(): void {
355
+ if (!this.agentId) return;
356
+
357
+ try {
358
+ // Load existing context or create new
359
+ const context = loadOrCreateContext(this.agentId, this.agentName);
360
+
361
+ // Capture current session state
362
+ const sessionContext = captureSessionContext(this.agentName);
363
+ if (sessionContext) {
364
+ context.workingState = {
365
+ ...context.workingState,
366
+ workdir: sessionContext.workdir,
367
+ gitBranch: sessionContext.gitBranch,
368
+ gitStatus: sessionContext.gitStatus,
369
+ };
370
+ }
371
+
372
+ // Save updated context
373
+ saveContext(context);
374
+ console.log(`Context saved for agent ${this.agentName}`);
375
+ } catch (error) {
376
+ console.error("Failed to save agent context:", error);
377
+ }
378
+ }
259
379
  }
@@ -13,16 +13,23 @@ export interface HeartbeatConfig {
13
13
  workspace: string;
14
14
  onError?: (error: Error) => void;
15
15
  onTokenRefresh?: (newToken: string) => void;
16
+ /** Called periodically to save agent context (every N heartbeats) */
17
+ onContextSave?: () => void;
18
+ /** How many heartbeats between context saves (default: 5) */
19
+ contextSaveFrequency?: number;
16
20
  }
17
21
 
18
22
  export class Heartbeat {
19
23
  private config: HeartbeatConfig;
20
24
  private currentToken: string;
21
25
  private intervalId: NodeJS.Timeout | null = null;
26
+ private heartbeatCount = 0;
27
+ private contextSaveFrequency: number;
22
28
 
23
29
  constructor(config: HeartbeatConfig) {
24
30
  this.config = config;
25
31
  this.currentToken = config.token;
32
+ this.contextSaveFrequency = config.contextSaveFrequency ?? 5;
26
33
  }
27
34
 
28
35
  start(): void {
@@ -78,6 +85,12 @@ export class Heartbeat {
78
85
  }
79
86
  throw new Error(`Heartbeat failed: ${response.status}`);
80
87
  }
88
+
89
+ // Periodically save context
90
+ this.heartbeatCount++;
91
+ if (this.config.onContextSave && this.heartbeatCount % this.contextSaveFrequency === 0) {
92
+ this.config.onContextSave();
93
+ }
81
94
  } catch (error) {
82
95
  this.config.onError?.(error as Error);
83
96
  }
@@ -1,11 +1,10 @@
1
- import { sendKeys } from "./tmux.js";
1
+ import { formatHandoffContextSummary, parseHandoffContext } from "../context/handoff.js";
2
+ import type { AgentContext } from "../context/schema.js";
2
3
  import type { InboxItem } from "./registry.js";
4
+ import { sendKeys } from "./tmux.js";
3
5
  import type { WebSocketEvent } from "./websocket.js";
4
6
 
5
- export function injectStartupMessage(
6
- agentName: string,
7
- pendingCount: number
8
- ): void {
7
+ export function injectStartupMessage(agentName: string, pendingCount: number): void {
9
8
  if (pendingCount === 0) {
10
9
  const message = `[AgentMesh] Connected and ready. No pending items in inbox.`;
11
10
  sendKeys(agentName, message);
@@ -18,10 +17,7 @@ Use agentmesh_check_inbox to see them, or agentmesh_accept_handoff to start work
18
17
  sendKeys(agentName, message);
19
18
  }
20
19
 
21
- export function injectHandoffReceived(
22
- agentName: string,
23
- event: WebSocketEvent
24
- ): void {
20
+ export function injectHandoffReceived(agentName: string, event: WebSocketEvent): void {
25
21
  const fromName =
26
22
  (event.from_agent as { display_name?: string })?.display_name ||
27
23
  (event.from_agent_id as string) ||
@@ -41,14 +37,9 @@ Accept this handoff and begin work.`;
41
37
  sendKeys(agentName, message);
42
38
  }
43
39
 
44
- export function injectNudge(
45
- agentName: string,
46
- event: WebSocketEvent
47
- ): void {
40
+ export function injectNudge(agentName: string, event: WebSocketEvent): void {
48
41
  const fromName =
49
- (event.from as { name?: string })?.name ||
50
- (event.from_name as string) ||
51
- "Someone";
42
+ (event.from as { name?: string })?.name || (event.from_name as string) || "Someone";
52
43
  const message = (event.message as string) || "Check your inbox";
53
44
 
54
45
  const formatted = `[AgentMesh] Nudge from ${fromName}:
@@ -57,12 +48,8 @@ ${message}`;
57
48
  sendKeys(agentName, formatted);
58
49
  }
59
50
 
60
- export function injectBlockerResolved(
61
- agentName: string,
62
- event: WebSocketEvent
63
- ): void {
64
- const description =
65
- (event.description as string) || "A blocker has been resolved";
51
+ export function injectBlockerResolved(agentName: string, event: WebSocketEvent): void {
52
+ const description = (event.description as string) || "A blocker has been resolved";
66
53
  const resolvedBy =
67
54
  (event.resolved_by as { display_name?: string })?.display_name ||
68
55
  (event.resolved_by_name as string) ||
@@ -78,10 +65,7 @@ You can now proceed with your work.`;
78
65
  sendKeys(agentName, message);
79
66
  }
80
67
 
81
- export function injectInboxItems(
82
- agentName: string,
83
- items: InboxItem[]
84
- ): void {
68
+ export function injectInboxItems(agentName: string, items: InboxItem[]): void {
85
69
  if (items.length === 0) {
86
70
  sendKeys(agentName, "[AgentMesh] Your inbox is empty.");
87
71
  return;
@@ -101,10 +85,7 @@ export function injectInboxItems(
101
85
  sendKeys(agentName, message);
102
86
  }
103
87
 
104
- export function handleWebSocketEvent(
105
- agentName: string,
106
- event: WebSocketEvent
107
- ): void {
88
+ export function handleWebSocketEvent(agentName: string, event: WebSocketEvent): void {
108
89
  switch (event.type) {
109
90
  case "handoff_received":
110
91
  case "handoff.received":
@@ -126,3 +107,66 @@ export function handleWebSocketEvent(
126
107
  break;
127
108
  }
128
109
  }
110
+
111
+ /**
112
+ * Injects restored context from a previous session
113
+ */
114
+ export function injectRestoredContext(agentName: string, context: AgentContext): void {
115
+ const parts: string[] = ["[AgentMesh] Restored context from previous session:"];
116
+
117
+ // Working state
118
+ if (context.workingState.workdir) {
119
+ parts.push(`Working directory: ${context.workingState.workdir}`);
120
+ }
121
+ if (context.workingState.gitBranch) {
122
+ parts.push(`Git branch: ${context.workingState.gitBranch}`);
123
+ }
124
+
125
+ // Tasks
126
+ const activeTasks = context.tasks.tasks.filter(
127
+ (t) => t.status === "in_progress" || t.status === "pending",
128
+ );
129
+ if (activeTasks.length > 0) {
130
+ parts.push("");
131
+ parts.push("Active tasks:");
132
+ for (const task of activeTasks.slice(0, 5)) {
133
+ const statusIcon = task.status === "in_progress" ? ">" : "-";
134
+ parts.push(` ${statusIcon} ${task.content}`);
135
+ }
136
+ if (activeTasks.length > 5) {
137
+ parts.push(` ... and ${activeTasks.length - 5} more`);
138
+ }
139
+ }
140
+
141
+ // Current goal
142
+ if (context.tasks.currentGoal) {
143
+ parts.push("");
144
+ parts.push(`Goal: ${context.tasks.currentGoal}`);
145
+ }
146
+
147
+ // Accomplishments
148
+ if (context.conversation.accomplishments.length > 0) {
149
+ parts.push("");
150
+ parts.push("Recent accomplishments:");
151
+ for (const acc of context.conversation.accomplishments.slice(0, 3)) {
152
+ parts.push(` - ${acc}`);
153
+ }
154
+ }
155
+
156
+ const message = parts.join("\n");
157
+ sendKeys(agentName, message);
158
+ }
159
+
160
+ /**
161
+ * Injects context received from a handoff
162
+ */
163
+ export function injectHandoffContext(agentName: string, contextString: string): void {
164
+ const context = parseHandoffContext(contextString);
165
+ if (!context) {
166
+ return;
167
+ }
168
+
169
+ const summary = formatHandoffContextSummary(context);
170
+ const message = `[AgentMesh] Context from previous agent:\n\n${summary}`;
171
+ sendKeys(agentName, message);
172
+ }