@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.
- 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/__tests__/runner.test.d.ts +1 -0
- package/dist/__tests__/runner.test.js +87 -0
- package/dist/__tests__/runner.test.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 +20 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/restart.d.ts +4 -1
- package/dist/cli/restart.js +4 -2
- package/dist/cli/restart.js.map +1 -1
- 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.js +5 -1
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/whoami.js +17 -0
- package/dist/cli/whoami.js.map +1 -1
- package/dist/config/schema.d.ts +5 -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 +12 -0
- package/dist/core/daemon.js +108 -4
- 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/runner.d.ts +49 -0
- package/dist/core/runner.js +148 -0
- package/dist/core/runner.js.map +1 -0
- package/dist/core/tmux.d.ts +16 -0
- package/dist/core/tmux.js +85 -7
- package/dist/core/tmux.js.map +1 -1
- package/package.json +11 -11
- package/src/__tests__/context.test.ts +464 -0
- package/src/__tests__/runner.test.ts +105 -0
- package/src/cli/context.ts +232 -0
- package/src/cli/index.ts +20 -2
- package/src/cli/restart.ts +9 -2
- package/src/cli/start.ts +11 -9
- package/src/cli/status.ts +6 -1
- package/src/cli/whoami.ts +18 -0
- package/src/config/schema.ts +6 -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 +121 -1
- package/src/core/heartbeat.ts +13 -0
- package/src/core/injector.ts +74 -30
- package/src/core/runner.ts +200 -0
- package/src/core/tmux.ts +103 -9
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import { getAgentState, loadState } from "../config/loader.js";
|
|
3
|
+
import { extractHandoffContext, formatHandoffContextSummary } from "../context/handoff.js";
|
|
4
|
+
import {
|
|
5
|
+
CONTEXT_DIR,
|
|
6
|
+
deleteContext,
|
|
7
|
+
exportContext,
|
|
8
|
+
importContext,
|
|
9
|
+
listContexts,
|
|
10
|
+
loadContext,
|
|
11
|
+
} from "../context/index.js";
|
|
12
|
+
|
|
13
|
+
export interface ContextOptions {
|
|
14
|
+
output?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function contextCmd(
|
|
18
|
+
action: string,
|
|
19
|
+
nameOrPath?: string,
|
|
20
|
+
options?: ContextOptions,
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
switch (action) {
|
|
23
|
+
case "show":
|
|
24
|
+
await showContext(nameOrPath);
|
|
25
|
+
break;
|
|
26
|
+
case "clear":
|
|
27
|
+
await clearContext(nameOrPath);
|
|
28
|
+
break;
|
|
29
|
+
case "export":
|
|
30
|
+
await exportContextCmd(nameOrPath, options?.output);
|
|
31
|
+
break;
|
|
32
|
+
case "import":
|
|
33
|
+
await importContextCmd(nameOrPath);
|
|
34
|
+
break;
|
|
35
|
+
case "list":
|
|
36
|
+
case "ls":
|
|
37
|
+
await listContextsCmd();
|
|
38
|
+
break;
|
|
39
|
+
case "path":
|
|
40
|
+
console.log(CONTEXT_DIR);
|
|
41
|
+
break;
|
|
42
|
+
default:
|
|
43
|
+
console.log(pc.red(`Unknown action: ${action}`));
|
|
44
|
+
console.log("Available actions: show, clear, export, import, list, path");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function showContext(name?: string): Promise<void> {
|
|
50
|
+
const agentId = await resolveAgentId(name);
|
|
51
|
+
if (!agentId) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const context = loadContext(agentId);
|
|
56
|
+
if (!context) {
|
|
57
|
+
console.log(pc.yellow(`No saved context found for agent.`));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(pc.bold(`Context for ${context.agentName} (${context.agentId}):`));
|
|
62
|
+
console.log(pc.dim(`Saved at: ${new Date(context.savedAt).toLocaleString()}`));
|
|
63
|
+
console.log();
|
|
64
|
+
|
|
65
|
+
// Working state
|
|
66
|
+
console.log(pc.bold("Working State:"));
|
|
67
|
+
console.log(` Directory: ${context.workingState.workdir}`);
|
|
68
|
+
if (context.workingState.gitBranch) {
|
|
69
|
+
console.log(` Git branch: ${context.workingState.gitBranch}`);
|
|
70
|
+
}
|
|
71
|
+
if (context.workingState.recentFiles.length > 0) {
|
|
72
|
+
console.log(` Recent files: ${context.workingState.recentFiles.slice(0, 5).join(", ")}`);
|
|
73
|
+
}
|
|
74
|
+
console.log();
|
|
75
|
+
|
|
76
|
+
// Tasks
|
|
77
|
+
const activeTasks = context.tasks.tasks.filter(
|
|
78
|
+
(t) => t.status === "in_progress" || t.status === "pending",
|
|
79
|
+
);
|
|
80
|
+
if (activeTasks.length > 0 || context.tasks.currentGoal) {
|
|
81
|
+
console.log(pc.bold("Tasks:"));
|
|
82
|
+
if (context.tasks.currentGoal) {
|
|
83
|
+
console.log(` Goal: ${context.tasks.currentGoal}`);
|
|
84
|
+
}
|
|
85
|
+
for (const task of activeTasks) {
|
|
86
|
+
const icon = task.status === "in_progress" ? pc.yellow(">") : pc.dim("-");
|
|
87
|
+
const priority =
|
|
88
|
+
task.priority === "high" ? pc.red(`[${task.priority}]`) : pc.dim(`[${task.priority}]`);
|
|
89
|
+
console.log(` ${icon} ${task.content} ${priority}`);
|
|
90
|
+
}
|
|
91
|
+
console.log();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Conversation
|
|
95
|
+
if (context.conversation.topics.length > 0 || context.conversation.accomplishments.length > 0) {
|
|
96
|
+
console.log(pc.bold("Conversation:"));
|
|
97
|
+
if (context.conversation.topics.length > 0) {
|
|
98
|
+
console.log(` Topics: ${context.conversation.topics.join(", ")}`);
|
|
99
|
+
}
|
|
100
|
+
if (context.conversation.accomplishments.length > 0) {
|
|
101
|
+
console.log(" Accomplishments:");
|
|
102
|
+
for (const acc of context.conversation.accomplishments.slice(0, 5)) {
|
|
103
|
+
console.log(` - ${acc}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
console.log();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Custom context
|
|
110
|
+
if (Object.keys(context.custom).length > 0) {
|
|
111
|
+
console.log(pc.bold("Custom Context:"));
|
|
112
|
+
console.log(JSON.stringify(context.custom, null, 2));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function clearContext(name?: string): Promise<void> {
|
|
117
|
+
const agentId = await resolveAgentId(name);
|
|
118
|
+
if (!agentId) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const deleted = deleteContext(agentId);
|
|
123
|
+
if (deleted) {
|
|
124
|
+
console.log(pc.green(`Context cleared for agent.`));
|
|
125
|
+
} else {
|
|
126
|
+
console.log(pc.yellow(`No context found to clear.`));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function exportContextCmd(name?: string, outputPath?: string): Promise<void> {
|
|
131
|
+
const agentId = await resolveAgentId(name);
|
|
132
|
+
if (!agentId) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const context = loadContext(agentId);
|
|
137
|
+
if (!context) {
|
|
138
|
+
console.log(pc.yellow(`No saved context found for agent.`));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const output = outputPath || `${context.agentName}-context.json`;
|
|
143
|
+
const success = exportContext(agentId, output);
|
|
144
|
+
|
|
145
|
+
if (success) {
|
|
146
|
+
console.log(pc.green(`Context exported to: ${output}`));
|
|
147
|
+
} else {
|
|
148
|
+
console.log(pc.red(`Failed to export context.`));
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function importContextCmd(inputPath?: string): Promise<void> {
|
|
154
|
+
if (!inputPath) {
|
|
155
|
+
console.log(pc.red("Please specify a file path to import."));
|
|
156
|
+
console.log("Usage: agentmesh context import <file>");
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const context = importContext(inputPath);
|
|
161
|
+
if (context) {
|
|
162
|
+
console.log(pc.green(`Context imported for agent: ${context.agentName} (${context.agentId})`));
|
|
163
|
+
} else {
|
|
164
|
+
console.log(pc.red(`Failed to import context from: ${inputPath}`));
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function listContextsCmd(): Promise<void> {
|
|
170
|
+
const contexts = listContexts();
|
|
171
|
+
|
|
172
|
+
if (contexts.length === 0) {
|
|
173
|
+
console.log(pc.yellow("No saved contexts found."));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log(pc.bold("Saved Contexts:"));
|
|
178
|
+
console.log();
|
|
179
|
+
|
|
180
|
+
for (const ctx of contexts) {
|
|
181
|
+
const context = loadContext(ctx.agentId);
|
|
182
|
+
if (!context) continue;
|
|
183
|
+
|
|
184
|
+
console.log(` ${pc.cyan(context.agentName)} (${pc.dim(ctx.agentId.slice(0, 8))}...)`);
|
|
185
|
+
console.log(` Saved: ${new Date(ctx.savedAt).toLocaleString()}`);
|
|
186
|
+
if (context.tasks.currentGoal) {
|
|
187
|
+
console.log(` Goal: ${context.tasks.currentGoal}`);
|
|
188
|
+
}
|
|
189
|
+
console.log();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function resolveAgentId(name?: string): Promise<string | null> {
|
|
194
|
+
// If no name provided, try to get from environment
|
|
195
|
+
if (!name) {
|
|
196
|
+
const envAgentId = process.env.AGENTMESH_AGENT_ID;
|
|
197
|
+
if (envAgentId) {
|
|
198
|
+
return envAgentId;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Try to find single running agent
|
|
202
|
+
const state = loadState();
|
|
203
|
+
if (state.agents.length === 1) {
|
|
204
|
+
return state.agents[0].agentId;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (state.agents.length === 0) {
|
|
208
|
+
console.log(pc.yellow("No running agents found."));
|
|
209
|
+
console.log("Usage: agentmesh context show <name>");
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
console.log(pc.yellow("Multiple agents running. Please specify a name."));
|
|
214
|
+
console.log("Usage: agentmesh context show <name>");
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Try to find by name
|
|
219
|
+
const agentState = getAgentState(name);
|
|
220
|
+
if (agentState) {
|
|
221
|
+
return agentState.agentId;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Maybe it's already an agent ID
|
|
225
|
+
const context = loadContext(name);
|
|
226
|
+
if (context) {
|
|
227
|
+
return name;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
console.log(pc.red(`Agent not found: ${name}`));
|
|
231
|
+
return null;
|
|
232
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { Command } from "commander";
|
|
|
5
5
|
import pc from "picocolors";
|
|
6
6
|
import { attach } from "./attach.js";
|
|
7
7
|
import { configCmd } from "./config.js";
|
|
8
|
+
import { contextCmd } from "./context.js";
|
|
8
9
|
import { init } from "./init.js";
|
|
9
10
|
import { list } from "./list.js";
|
|
10
11
|
import { logs } from "./logs.js";
|
|
@@ -46,6 +47,7 @@ program
|
|
|
46
47
|
.option("-w, --workdir <path>", "Working directory")
|
|
47
48
|
.option("-m, --model <model>", "Model identifier")
|
|
48
49
|
.option("-f, --foreground", "Run in foreground (blocking)")
|
|
50
|
+
.option("--no-context", "Start fresh without restoring previous context")
|
|
49
51
|
.action(async (options) => {
|
|
50
52
|
try {
|
|
51
53
|
await start(options);
|
|
@@ -166,9 +168,10 @@ program
|
|
|
166
168
|
.command("restart")
|
|
167
169
|
.description("Restart an agent (preserves agent ID)")
|
|
168
170
|
.argument("<name>", "Agent name")
|
|
169
|
-
.
|
|
171
|
+
.option("-m, --model <model>", "Model to use (default: preserve previous model)")
|
|
172
|
+
.action(async (name, options) => {
|
|
170
173
|
try {
|
|
171
|
-
await restart(name);
|
|
174
|
+
await restart(name, { model: options.model });
|
|
172
175
|
} catch (error) {
|
|
173
176
|
console.error(pc.red((error as Error).message));
|
|
174
177
|
process.exit(1);
|
|
@@ -190,4 +193,19 @@ program
|
|
|
190
193
|
}
|
|
191
194
|
});
|
|
192
195
|
|
|
196
|
+
program
|
|
197
|
+
.command("context")
|
|
198
|
+
.description("Manage agent context persistence")
|
|
199
|
+
.argument("[action]", "Action: show (default), clear, export, import, list, path")
|
|
200
|
+
.argument("[name]", "Agent name or file path (for import)")
|
|
201
|
+
.option("-o, --output <path>", "Output file path (for export)")
|
|
202
|
+
.action(async (action, name, options) => {
|
|
203
|
+
try {
|
|
204
|
+
await contextCmd(action || "show", name, options);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error(pc.red((error as Error).message));
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
193
211
|
program.parse();
|
package/src/cli/restart.ts
CHANGED
|
@@ -4,7 +4,11 @@ import { destroySession, getSessionName, sessionExists } from "../core/tmux.js";
|
|
|
4
4
|
import { start } from "./start.js";
|
|
5
5
|
import { stop } from "./stop.js";
|
|
6
6
|
|
|
7
|
-
export
|
|
7
|
+
export interface RestartOptions {
|
|
8
|
+
model?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function restart(name: string, options: RestartOptions = {}): Promise<void> {
|
|
8
12
|
const config = loadConfig();
|
|
9
13
|
|
|
10
14
|
if (!config) {
|
|
@@ -34,6 +38,9 @@ export async function restart(name: string): Promise<void> {
|
|
|
34
38
|
// Find agent config to get workdir and other settings
|
|
35
39
|
const agentConfig = config.agents.find((a) => a.name === name);
|
|
36
40
|
|
|
41
|
+
// Model priority: CLI option > previous runtime model > agent config
|
|
42
|
+
const effectiveModel = options.model || agent.runtimeModel || agentConfig?.model;
|
|
43
|
+
|
|
37
44
|
console.log(pc.dim("Starting new session..."));
|
|
38
45
|
|
|
39
46
|
// Start with the same settings, agent ID will be reused from state
|
|
@@ -41,7 +48,7 @@ export async function restart(name: string): Promise<void> {
|
|
|
41
48
|
name,
|
|
42
49
|
command: agentConfig?.command,
|
|
43
50
|
workdir: agentConfig?.workdir,
|
|
44
|
-
model:
|
|
51
|
+
model: effectiveModel,
|
|
45
52
|
foreground: false,
|
|
46
53
|
});
|
|
47
54
|
|
package/src/cli/start.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import { loadConfig, getAgentState } from "../config/loader.js";
|
|
6
|
-
import { sessionExists, getSessionName } from "../core/tmux.js";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
7
4
|
import pc from "picocolors";
|
|
5
|
+
import { getAgentState, loadConfig } from "../config/loader.js";
|
|
6
|
+
import { AgentDaemon } from "../core/daemon.js";
|
|
7
|
+
import { getSessionName, sessionExists } from "../core/tmux.js";
|
|
8
8
|
|
|
9
9
|
export interface StartOptions {
|
|
10
10
|
name: string;
|
|
@@ -12,6 +12,7 @@ export interface StartOptions {
|
|
|
12
12
|
workdir?: string;
|
|
13
13
|
model?: string;
|
|
14
14
|
foreground?: boolean;
|
|
15
|
+
noContext?: boolean;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export async function start(options: StartOptions): Promise<void> {
|
|
@@ -40,7 +41,10 @@ export async function start(options: StartOptions): Promise<void> {
|
|
|
40
41
|
// If --foreground flag is set, run in foreground (blocking)
|
|
41
42
|
if (options.foreground) {
|
|
42
43
|
try {
|
|
43
|
-
const daemon = new AgentDaemon(
|
|
44
|
+
const daemon = new AgentDaemon({
|
|
45
|
+
...options,
|
|
46
|
+
restoreContext: !options.noContext,
|
|
47
|
+
});
|
|
44
48
|
await daemon.start();
|
|
45
49
|
// Keep process alive
|
|
46
50
|
await new Promise(() => {});
|
|
@@ -55,16 +59,14 @@ export async function start(options: StartOptions): Promise<void> {
|
|
|
55
59
|
console.log(`Starting agent "${options.name}" in background...`);
|
|
56
60
|
|
|
57
61
|
// Get the path to this CLI
|
|
58
|
-
const cliPath = path.resolve(
|
|
59
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
60
|
-
"../cli/index.js"
|
|
61
|
-
);
|
|
62
|
+
const cliPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../cli/index.js");
|
|
62
63
|
|
|
63
64
|
// Build args for the background process
|
|
64
65
|
const args = ["start", "--name", options.name, "--foreground"];
|
|
65
66
|
if (options.command) args.push("--command", options.command);
|
|
66
67
|
if (options.workdir) args.push("--workdir", options.workdir);
|
|
67
68
|
if (options.model) args.push("--model", options.model);
|
|
69
|
+
if (options.noContext) args.push("--no-context");
|
|
68
70
|
|
|
69
71
|
// Spawn detached background process
|
|
70
72
|
const child = spawn("node", [cliPath, ...args], {
|
package/src/cli/status.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import pc from "picocolors";
|
|
2
2
|
import { loadConfig, loadState } from "../config/loader.js";
|
|
3
|
+
import { getRunnerDisplayName } from "../core/runner.js";
|
|
3
4
|
import { getSessionName, sessionExists } from "../core/tmux.js";
|
|
4
5
|
|
|
5
6
|
interface HealthResponse {
|
|
@@ -60,7 +61,11 @@ export async function status(): Promise<void> {
|
|
|
60
61
|
console.log(` Total: ${pc.dim(String(state.agents.length))}`);
|
|
61
62
|
|
|
62
63
|
if (runningAgents.length > 0) {
|
|
63
|
-
console.log(`
|
|
64
|
+
console.log(` Active:`);
|
|
65
|
+
for (const agent of runningAgents) {
|
|
66
|
+
const modelInfo = agent.runtimeModel ? ` [${agent.runtimeModel}]` : "";
|
|
67
|
+
console.log(` - ${pc.cyan(agent.name)}${pc.dim(modelInfo)}`);
|
|
68
|
+
}
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
// Check tmux
|
package/src/cli/whoami.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import pc from "picocolors";
|
|
2
2
|
import { loadConfig, loadState } from "../config/loader.js";
|
|
3
|
+
import { getRunnerDisplayName, type RunnerType } from "../core/runner.js";
|
|
3
4
|
import { decodeToken, getTokenExpiry } from "../utils/jwt.js";
|
|
4
5
|
|
|
5
6
|
export async function whoami(agentName?: string): Promise<void> {
|
|
@@ -23,9 +24,18 @@ export async function whoami(agentName?: string): Promise<void> {
|
|
|
23
24
|
const expiry = getTokenExpiry(envToken);
|
|
24
25
|
const expiryStr = formatExpiry(expiry);
|
|
25
26
|
|
|
27
|
+
// Find the agent in state to get model info
|
|
28
|
+
const agentState = state.agents.find((a) => a.agentId === envAgentId);
|
|
29
|
+
|
|
26
30
|
console.log(pc.bold("Current Agent"));
|
|
27
31
|
console.log(` ID: ${pc.cyan(envAgentId)}`);
|
|
28
32
|
console.log(` Workspace: ${pc.dim(config.workspace)}`);
|
|
33
|
+
if (agentState?.runtimeModel) {
|
|
34
|
+
const runnerName = agentState.runnerType
|
|
35
|
+
? getRunnerDisplayName(agentState.runnerType)
|
|
36
|
+
: "Unknown";
|
|
37
|
+
console.log(` Model: ${pc.cyan(agentState.runtimeModel)} (${pc.dim(runnerName)})`);
|
|
38
|
+
}
|
|
29
39
|
console.log(` Token: ${expiryStr}`);
|
|
30
40
|
console.log(` Hub: ${pc.dim(config.hubUrl)}`);
|
|
31
41
|
return;
|
|
@@ -49,6 +59,10 @@ export async function whoami(agentName?: string): Promise<void> {
|
|
|
49
59
|
console.log(` ${pc.cyan(agent.name)}`);
|
|
50
60
|
console.log(` ID: ${pc.dim(agent.agentId)}`);
|
|
51
61
|
console.log(` Status: ${running ? pc.green("running") : pc.yellow("stopped")}`);
|
|
62
|
+
if (agent.runtimeModel) {
|
|
63
|
+
const runnerName = agent.runnerType ? getRunnerDisplayName(agent.runnerType) : "Unknown";
|
|
64
|
+
console.log(` Model: ${pc.dim(agent.runtimeModel)} (${runnerName})`);
|
|
65
|
+
}
|
|
52
66
|
console.log(` Token: ${expiryStr}`);
|
|
53
67
|
console.log();
|
|
54
68
|
}
|
|
@@ -71,6 +85,10 @@ export async function whoami(agentName?: string): Promise<void> {
|
|
|
71
85
|
console.log(` ID: ${pc.cyan(agent.agentId)}`);
|
|
72
86
|
console.log(` Workspace: ${pc.dim(config.workspace)}`);
|
|
73
87
|
console.log(` Status: ${running ? pc.green("running") : pc.yellow("stopped")}`);
|
|
88
|
+
if (agent.runtimeModel) {
|
|
89
|
+
const runnerName = agent.runnerType ? getRunnerDisplayName(agent.runnerType) : "Unknown";
|
|
90
|
+
console.log(` Model: ${pc.cyan(agent.runtimeModel)} (${pc.dim(runnerName)})`);
|
|
91
|
+
}
|
|
74
92
|
console.log(` Token: ${expiryStr}`);
|
|
75
93
|
console.log(` Session: ${pc.dim(agent.tmuxSession || "none")}`);
|
|
76
94
|
console.log(` Started: ${pc.dim(agent.startedAt || "unknown")}`);
|
package/src/config/schema.ts
CHANGED
|
@@ -30,6 +30,8 @@ export const DEFAULT_CONFIG: Partial<Config> = {
|
|
|
30
30
|
export const CONFIG_PATH = `${process.env.HOME}/.agentmesh/config.json`;
|
|
31
31
|
export const STATE_PATH = `${process.env.HOME}/.agentmesh/state.json`;
|
|
32
32
|
|
|
33
|
+
export type RunnerType = "opencode" | "claude" | "custom";
|
|
34
|
+
|
|
33
35
|
export interface AgentState {
|
|
34
36
|
name: string;
|
|
35
37
|
agentId: string;
|
|
@@ -37,6 +39,10 @@ export interface AgentState {
|
|
|
37
39
|
tmuxSession: string;
|
|
38
40
|
startedAt: string;
|
|
39
41
|
token?: string;
|
|
42
|
+
/** The effective runtime model (resolved from CLI > agent > defaults) */
|
|
43
|
+
runtimeModel?: string;
|
|
44
|
+
/** The runner type (opencode, claude, custom) */
|
|
45
|
+
runnerType?: RunnerType;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
export interface State {
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Handoff Module
|
|
3
|
+
* Functions for sharing context between agents via handoffs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AgentContext } from "./schema.js";
|
|
7
|
+
import { loadContext } from "./storage.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Minimal context for handoff (excludes sensitive/large data)
|
|
11
|
+
*/
|
|
12
|
+
export interface HandoffContext {
|
|
13
|
+
/** Working directory */
|
|
14
|
+
workdir: string;
|
|
15
|
+
/** Git branch if in a repo */
|
|
16
|
+
gitBranch?: string;
|
|
17
|
+
/** Current goal being worked on */
|
|
18
|
+
currentGoal?: string;
|
|
19
|
+
/** Active tasks (in_progress and pending) */
|
|
20
|
+
activeTasks: Array<{
|
|
21
|
+
content: string;
|
|
22
|
+
status: "in_progress" | "pending";
|
|
23
|
+
priority: string;
|
|
24
|
+
}>;
|
|
25
|
+
/** Recent accomplishments */
|
|
26
|
+
recentAccomplishments: string[];
|
|
27
|
+
/** Key topics from conversation */
|
|
28
|
+
topics: string[];
|
|
29
|
+
/** Any custom context data */
|
|
30
|
+
custom?: Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extracts handoff-relevant context from full agent context
|
|
35
|
+
*/
|
|
36
|
+
export function extractHandoffContext(context: AgentContext): HandoffContext {
|
|
37
|
+
return {
|
|
38
|
+
workdir: context.workingState.workdir,
|
|
39
|
+
gitBranch: context.workingState.gitBranch,
|
|
40
|
+
currentGoal: context.tasks.currentGoal,
|
|
41
|
+
activeTasks: context.tasks.tasks
|
|
42
|
+
.filter((t) => t.status === "in_progress" || t.status === "pending")
|
|
43
|
+
.map((t) => ({
|
|
44
|
+
content: t.content,
|
|
45
|
+
status: t.status as "in_progress" | "pending",
|
|
46
|
+
priority: t.priority,
|
|
47
|
+
})),
|
|
48
|
+
recentAccomplishments: context.conversation.accomplishments.slice(0, 5),
|
|
49
|
+
topics: context.conversation.topics.slice(0, 10),
|
|
50
|
+
custom: Object.keys(context.custom).length > 0 ? context.custom : undefined,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Gets handoff context for an agent by ID
|
|
56
|
+
*/
|
|
57
|
+
export function getHandoffContextForAgent(agentId: string): HandoffContext | null {
|
|
58
|
+
const context = loadContext(agentId);
|
|
59
|
+
if (!context) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return extractHandoffContext(context);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Serializes handoff context to a string for inclusion in API calls
|
|
67
|
+
*/
|
|
68
|
+
export function serializeHandoffContext(context: HandoffContext): string {
|
|
69
|
+
return JSON.stringify(context);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parses handoff context from a string (received from API)
|
|
74
|
+
*/
|
|
75
|
+
export function parseHandoffContext(contextString: string): HandoffContext | null {
|
|
76
|
+
try {
|
|
77
|
+
return JSON.parse(contextString) as HandoffContext;
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Generates a human-readable summary of handoff context
|
|
85
|
+
*/
|
|
86
|
+
export function formatHandoffContextSummary(context: HandoffContext): string {
|
|
87
|
+
const lines: string[] = [];
|
|
88
|
+
|
|
89
|
+
if (context.workdir) {
|
|
90
|
+
lines.push(`Working directory: ${context.workdir}`);
|
|
91
|
+
}
|
|
92
|
+
if (context.gitBranch) {
|
|
93
|
+
lines.push(`Git branch: ${context.gitBranch}`);
|
|
94
|
+
}
|
|
95
|
+
if (context.currentGoal) {
|
|
96
|
+
lines.push(`Current goal: ${context.currentGoal}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (context.activeTasks.length > 0) {
|
|
100
|
+
lines.push("");
|
|
101
|
+
lines.push("Active tasks:");
|
|
102
|
+
for (const task of context.activeTasks) {
|
|
103
|
+
const icon = task.status === "in_progress" ? ">" : "-";
|
|
104
|
+
lines.push(` ${icon} ${task.content}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (context.recentAccomplishments.length > 0) {
|
|
109
|
+
lines.push("");
|
|
110
|
+
lines.push("Recent accomplishments:");
|
|
111
|
+
for (const acc of context.recentAccomplishments) {
|
|
112
|
+
lines.push(` - ${acc}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (context.topics.length > 0) {
|
|
117
|
+
lines.push("");
|
|
118
|
+
lines.push(`Topics: ${context.topics.join(", ")}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return lines.join("\n");
|
|
122
|
+
}
|
|
@@ -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
|
+
}
|