@agentmeshhq/agent 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/__tests__/context.test.d.ts +1 -0
- package/dist/__tests__/context.test.js +353 -0
- package/dist/__tests__/context.test.js.map +1 -0
- package/dist/cli/config.d.ts +1 -0
- package/dist/cli/config.js +129 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/context.d.ts +4 -0
- package/dist/cli/context.js +190 -0
- package/dist/cli/context.js.map +1 -0
- package/dist/cli/index.js +76 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/logs.d.ts +4 -0
- package/dist/cli/logs.js +54 -0
- package/dist/cli/logs.js.map +1 -0
- package/dist/cli/restart.d.ts +1 -0
- package/dist/cli/restart.js +41 -0
- package/dist/cli/restart.js.map +1 -0
- package/dist/cli/start.d.ts +1 -0
- package/dist/cli/start.js +10 -5
- package/dist/cli/start.js.map +1 -1
- package/dist/cli/status.d.ts +1 -0
- package/dist/cli/status.js +73 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/context/handoff.d.ts +48 -0
- package/dist/context/handoff.js +88 -0
- package/dist/context/handoff.js.map +1 -0
- package/dist/context/index.d.ts +7 -0
- package/dist/context/index.js +8 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/schema.d.ts +82 -0
- package/dist/context/schema.js +33 -0
- package/dist/context/schema.js.map +1 -0
- package/dist/context/storage.d.ts +49 -0
- package/dist/context/storage.js +172 -0
- package/dist/context/storage.js.map +1 -0
- package/dist/core/daemon.d.ts +7 -0
- package/dist/core/daemon.js +53 -2
- package/dist/core/daemon.js.map +1 -1
- package/dist/core/heartbeat.d.ts +6 -0
- package/dist/core/heartbeat.js +8 -0
- package/dist/core/heartbeat.js.map +1 -1
- package/dist/core/injector.d.ts +9 -0
- package/dist/core/injector.js +55 -3
- package/dist/core/injector.js.map +1 -1
- package/dist/core/tmux.d.ts +13 -0
- package/dist/core/tmux.js +62 -0
- package/dist/core/tmux.js.map +1 -1
- package/package.json +11 -11
- package/src/__tests__/context.test.ts +464 -0
- package/src/cli/config.ts +148 -0
- package/src/cli/context.ts +232 -0
- package/src/cli/index.ts +76 -0
- package/src/cli/logs.ts +64 -0
- package/src/cli/restart.ts +50 -0
- package/src/cli/start.ts +11 -9
- package/src/cli/status.ts +83 -0
- package/src/context/handoff.ts +122 -0
- package/src/context/index.ts +8 -0
- package/src/context/schema.ts +111 -0
- package/src/context/storage.ts +197 -0
- package/src/core/daemon.ts +59 -1
- package/src/core/heartbeat.ts +13 -0
- package/src/core/injector.ts +74 -30
- package/src/core/tmux.ts +75 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Context Schema
|
|
3
|
+
* Defines the structure for persisting agent context across sessions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const CONTEXT_VERSION = 1;
|
|
7
|
+
export const CONTEXT_DIR = `${process.env.HOME}/.agentmesh/context`;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Summary of conversation history for context restoration
|
|
11
|
+
*/
|
|
12
|
+
export interface ConversationSummary {
|
|
13
|
+
/** Total messages in the conversation */
|
|
14
|
+
messageCount: number;
|
|
15
|
+
/** Key topics discussed */
|
|
16
|
+
topics: string[];
|
|
17
|
+
/** Summary of what was accomplished */
|
|
18
|
+
accomplishments: string[];
|
|
19
|
+
/** Last 5 user messages (truncated) */
|
|
20
|
+
recentMessages: Array<{
|
|
21
|
+
role: "user" | "assistant";
|
|
22
|
+
content: string;
|
|
23
|
+
timestamp: string;
|
|
24
|
+
}>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Current working state of the agent
|
|
29
|
+
*/
|
|
30
|
+
export interface WorkingState {
|
|
31
|
+
/** Current working directory */
|
|
32
|
+
workdir: string;
|
|
33
|
+
/** Recently accessed files */
|
|
34
|
+
recentFiles: string[];
|
|
35
|
+
/** Open file paths being edited */
|
|
36
|
+
openFiles: string[];
|
|
37
|
+
/** Git branch if in a repo */
|
|
38
|
+
gitBranch?: string;
|
|
39
|
+
/** Git status summary */
|
|
40
|
+
gitStatus?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* In-progress task state from TodoWrite
|
|
45
|
+
*/
|
|
46
|
+
export interface TaskState {
|
|
47
|
+
/** Active tasks */
|
|
48
|
+
tasks: Array<{
|
|
49
|
+
content: string;
|
|
50
|
+
status: "pending" | "in_progress" | "completed" | "cancelled";
|
|
51
|
+
priority: "high" | "medium" | "low";
|
|
52
|
+
}>;
|
|
53
|
+
/** Overall goal being worked on */
|
|
54
|
+
currentGoal?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Custom key-value store for agent-specific context
|
|
59
|
+
*/
|
|
60
|
+
export interface CustomContext {
|
|
61
|
+
[key: string]: unknown;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Full agent context structure
|
|
66
|
+
*/
|
|
67
|
+
export interface AgentContext {
|
|
68
|
+
/** Schema version for migration support */
|
|
69
|
+
version: number;
|
|
70
|
+
/** Agent ID this context belongs to */
|
|
71
|
+
agentId: string;
|
|
72
|
+
/** Agent name */
|
|
73
|
+
agentName: string;
|
|
74
|
+
/** When this context was last saved */
|
|
75
|
+
savedAt: string;
|
|
76
|
+
/** Conversation summary */
|
|
77
|
+
conversation: ConversationSummary;
|
|
78
|
+
/** Current working state */
|
|
79
|
+
workingState: WorkingState;
|
|
80
|
+
/** Task state */
|
|
81
|
+
tasks: TaskState;
|
|
82
|
+
/** Custom context data */
|
|
83
|
+
custom: CustomContext;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Creates an empty context for a new agent
|
|
88
|
+
*/
|
|
89
|
+
export function createEmptyContext(agentId: string, agentName: string): AgentContext {
|
|
90
|
+
return {
|
|
91
|
+
version: CONTEXT_VERSION,
|
|
92
|
+
agentId,
|
|
93
|
+
agentName,
|
|
94
|
+
savedAt: new Date().toISOString(),
|
|
95
|
+
conversation: {
|
|
96
|
+
messageCount: 0,
|
|
97
|
+
topics: [],
|
|
98
|
+
accomplishments: [],
|
|
99
|
+
recentMessages: [],
|
|
100
|
+
},
|
|
101
|
+
workingState: {
|
|
102
|
+
workdir: process.cwd(),
|
|
103
|
+
recentFiles: [],
|
|
104
|
+
openFiles: [],
|
|
105
|
+
},
|
|
106
|
+
tasks: {
|
|
107
|
+
tasks: [],
|
|
108
|
+
},
|
|
109
|
+
custom: {},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -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
|
+
}
|
package/src/core/daemon.ts
CHANGED
|
@@ -6,10 +6,12 @@ import {
|
|
|
6
6
|
updateAgentInState,
|
|
7
7
|
} from "../config/loader.js";
|
|
8
8
|
import type { AgentConfig, Config } from "../config/schema.js";
|
|
9
|
+
import { loadContext, loadOrCreateContext, saveContext } from "../context/index.js";
|
|
9
10
|
import { Heartbeat } from "./heartbeat.js";
|
|
10
|
-
import { handleWebSocketEvent, injectStartupMessage } from "./injector.js";
|
|
11
|
+
import { handleWebSocketEvent, injectRestoredContext, injectStartupMessage } from "./injector.js";
|
|
11
12
|
import { checkInbox, registerAgent } from "./registry.js";
|
|
12
13
|
import {
|
|
14
|
+
captureSessionContext,
|
|
13
15
|
createSession,
|
|
14
16
|
destroySession,
|
|
15
17
|
getSessionName,
|
|
@@ -24,6 +26,8 @@ export interface DaemonOptions {
|
|
|
24
26
|
workdir?: string;
|
|
25
27
|
model?: string;
|
|
26
28
|
daemonize?: boolean;
|
|
29
|
+
/** Whether to restore context from previous session (default: true) */
|
|
30
|
+
restoreContext?: boolean;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
export class AgentDaemon {
|
|
@@ -35,6 +39,7 @@ export class AgentDaemon {
|
|
|
35
39
|
private token: string | null = null;
|
|
36
40
|
private agentId: string | null = null;
|
|
37
41
|
private isRunning = false;
|
|
42
|
+
private shouldRestoreContext: boolean;
|
|
38
43
|
|
|
39
44
|
constructor(options: DaemonOptions) {
|
|
40
45
|
const config = loadConfig();
|
|
@@ -44,6 +49,7 @@ export class AgentDaemon {
|
|
|
44
49
|
|
|
45
50
|
this.config = config;
|
|
46
51
|
this.agentName = options.name;
|
|
52
|
+
this.shouldRestoreContext = options.restoreContext !== false;
|
|
47
53
|
|
|
48
54
|
// Find or create agent config
|
|
49
55
|
let agentConfig = config.agents.find((a) => a.name === options.name);
|
|
@@ -141,6 +147,11 @@ export class AgentDaemon {
|
|
|
141
147
|
onError: (error) => {
|
|
142
148
|
console.error("Heartbeat error:", error.message);
|
|
143
149
|
},
|
|
150
|
+
onContextSave: () => {
|
|
151
|
+
// Periodically save context (every 5 heartbeats = ~2.5 minutes)
|
|
152
|
+
this.saveAgentContext();
|
|
153
|
+
},
|
|
154
|
+
contextSaveFrequency: 5,
|
|
144
155
|
onTokenRefresh: (newToken) => {
|
|
145
156
|
this.token = newToken;
|
|
146
157
|
// Update state file
|
|
@@ -210,6 +221,18 @@ export class AgentDaemon {
|
|
|
210
221
|
injectStartupMessage(this.agentName, 0);
|
|
211
222
|
}
|
|
212
223
|
|
|
224
|
+
// Restore context from previous session
|
|
225
|
+
if (this.shouldRestoreContext && this.agentId) {
|
|
226
|
+
console.log("Checking for previous context...");
|
|
227
|
+
const savedContext = loadContext(this.agentId);
|
|
228
|
+
if (savedContext) {
|
|
229
|
+
console.log(`Restoring context from ${savedContext.savedAt}`);
|
|
230
|
+
// Wait a moment for the session to be ready
|
|
231
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
232
|
+
injectRestoredContext(this.agentName, savedContext);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
213
236
|
this.isRunning = true;
|
|
214
237
|
|
|
215
238
|
console.log(`
|
|
@@ -235,6 +258,12 @@ Nudge agent:
|
|
|
235
258
|
|
|
236
259
|
this.isRunning = false;
|
|
237
260
|
|
|
261
|
+
// Save context before stopping
|
|
262
|
+
if (this.agentId) {
|
|
263
|
+
console.log("Saving agent context...");
|
|
264
|
+
this.saveAgentContext();
|
|
265
|
+
}
|
|
266
|
+
|
|
238
267
|
// Stop heartbeat
|
|
239
268
|
if (this.heartbeat) {
|
|
240
269
|
this.heartbeat.stop();
|
|
@@ -256,4 +285,33 @@ Nudge agent:
|
|
|
256
285
|
console.log("Agent stopped.");
|
|
257
286
|
process.exit(0);
|
|
258
287
|
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Saves the current agent context to disk
|
|
291
|
+
*/
|
|
292
|
+
private saveAgentContext(): void {
|
|
293
|
+
if (!this.agentId) return;
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
// Load existing context or create new
|
|
297
|
+
const context = loadOrCreateContext(this.agentId, this.agentName);
|
|
298
|
+
|
|
299
|
+
// Capture current session state
|
|
300
|
+
const sessionContext = captureSessionContext(this.agentName);
|
|
301
|
+
if (sessionContext) {
|
|
302
|
+
context.workingState = {
|
|
303
|
+
...context.workingState,
|
|
304
|
+
workdir: sessionContext.workdir,
|
|
305
|
+
gitBranch: sessionContext.gitBranch,
|
|
306
|
+
gitStatus: sessionContext.gitStatus,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Save updated context
|
|
311
|
+
saveContext(context);
|
|
312
|
+
console.log(`Context saved for agent ${this.agentName}`);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
console.error("Failed to save agent context:", error);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
259
317
|
}
|
package/src/core/heartbeat.ts
CHANGED
|
@@ -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
|
}
|
package/src/core/injector.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
+
}
|
package/src/core/tmux.ts
CHANGED
|
@@ -176,3 +176,78 @@ export function getSessionInfo(agentName: string): { exists: boolean; command?:
|
|
|
176
176
|
return { exists: true };
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
|
+
|
|
180
|
+
export interface SessionContext {
|
|
181
|
+
workdir: string;
|
|
182
|
+
gitBranch?: string;
|
|
183
|
+
gitStatus?: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Captures current working state from the tmux session
|
|
188
|
+
*/
|
|
189
|
+
export function captureSessionContext(agentName: string): SessionContext | null {
|
|
190
|
+
const sessionName = getSessionName(agentName);
|
|
191
|
+
|
|
192
|
+
if (!sessionExists(sessionName)) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
// Get current working directory from the pane
|
|
198
|
+
const workdir = execSync(`tmux display-message -t "${sessionName}" -p "#{pane_current_path}"`, {
|
|
199
|
+
encoding: "utf-8",
|
|
200
|
+
}).trim();
|
|
201
|
+
|
|
202
|
+
const context: SessionContext = { workdir };
|
|
203
|
+
|
|
204
|
+
// Try to get git info if in a git repo
|
|
205
|
+
try {
|
|
206
|
+
const gitBranch = execSync("git rev-parse --abbrev-ref HEAD 2>/dev/null", {
|
|
207
|
+
encoding: "utf-8",
|
|
208
|
+
cwd: workdir,
|
|
209
|
+
}).trim();
|
|
210
|
+
if (gitBranch) {
|
|
211
|
+
context.gitBranch = gitBranch;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const gitStatus = execSync("git status --short 2>/dev/null", {
|
|
215
|
+
encoding: "utf-8",
|
|
216
|
+
cwd: workdir,
|
|
217
|
+
}).trim();
|
|
218
|
+
if (gitStatus) {
|
|
219
|
+
// Truncate if too long
|
|
220
|
+
context.gitStatus =
|
|
221
|
+
gitStatus.length > 500 ? `${gitStatus.substring(0, 500)}...` : gitStatus;
|
|
222
|
+
}
|
|
223
|
+
} catch {
|
|
224
|
+
// Not a git repo or git not available
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return context;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.error(`Failed to capture session context: ${error}`);
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Captures recent output from the tmux pane (last N lines)
|
|
236
|
+
*/
|
|
237
|
+
export function captureSessionOutput(agentName: string, lines = 100): string | null {
|
|
238
|
+
const sessionName = getSessionName(agentName);
|
|
239
|
+
|
|
240
|
+
if (!sessionExists(sessionName)) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const output = execSync(`tmux capture-pane -t "${sessionName}" -p -S -${lines}`, {
|
|
246
|
+
encoding: "utf-8",
|
|
247
|
+
});
|
|
248
|
+
return output;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.error(`Failed to capture session output: ${error}`);
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|