@ai-devkit/agent-manager 0.33.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AgentManager.d.ts +11 -2
- package/dist/AgentManager.d.ts.map +1 -1
- package/dist/AgentManager.js +37 -6
- package/dist/AgentManager.js.map +1 -1
- package/dist/__tests__/AgentManager.test.js +212 -5
- package/dist/__tests__/AgentManager.test.js.map +1 -1
- package/dist/__tests__/capacity/codex.test.js +2 -1
- package/dist/__tests__/capacity/codex.test.js.map +1 -1
- package/dist/__tests__/capacity/index.test.js +6 -3
- package/dist/__tests__/capacity/index.test.js.map +1 -1
- package/dist/__tests__/capacity/zai.test.js +213 -0
- package/dist/__tests__/capacity/zai.test.js.map +1 -0
- package/dist/__tests__/{adapters → providers/copilot}/CopilotAdapter.test.js +19 -14
- package/dist/__tests__/providers/copilot/CopilotAdapter.test.js.map +1 -0
- package/dist/__tests__/{adapters → providers/gemini}/GeminiCliAdapter.test.js +37 -29
- package/dist/__tests__/providers/gemini/GeminiCliAdapter.test.js.map +1 -0
- package/dist/__tests__/providers/opencode/OpenCodeAdapter.test.js +189 -0
- package/dist/__tests__/providers/opencode/OpenCodeAdapter.test.js.map +1 -0
- package/dist/__tests__/providers/opencode/OpenCodeAgentMapper.test.js +89 -0
- package/dist/__tests__/providers/opencode/OpenCodeAgentMapper.test.js.map +1 -0
- package/dist/__tests__/providers/opencode/OpenCodeSessionLocator.test.js +94 -0
- package/dist/__tests__/providers/opencode/OpenCodeSessionLocator.test.js.map +1 -0
- package/dist/__tests__/providers/opencode/OpenCodeSessionParser.test.js +147 -0
- package/dist/__tests__/providers/opencode/OpenCodeSessionParser.test.js.map +1 -0
- package/dist/__tests__/runtime/HerdrAgentDiscovery.test.js +164 -0
- package/dist/__tests__/runtime/HerdrAgentDiscovery.test.js.map +1 -0
- package/dist/__tests__/runtime/HerdrAgentRuntime.test.js +37 -1
- package/dist/__tests__/runtime/HerdrAgentRuntime.test.js.map +1 -1
- package/dist/__tests__/utils/AgentRegistry.test.js +20 -13
- package/dist/__tests__/utils/AgentRegistry.test.js.map +1 -1
- package/dist/adapters/index.d.ts +3 -3
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +3 -3
- package/dist/adapters/index.js.map +1 -1
- package/dist/capacity/codex.d.ts +1 -0
- package/dist/capacity/codex.d.ts.map +1 -1
- package/dist/capacity/codex.js +9 -4
- package/dist/capacity/codex.js.map +1 -1
- package/dist/capacity/index.d.ts +8 -0
- package/dist/capacity/index.d.ts.map +1 -1
- package/dist/capacity/index.js +10 -9
- package/dist/capacity/index.js.map +1 -1
- package/dist/capacity/types.d.ts +8 -0
- package/dist/capacity/types.d.ts.map +1 -1
- package/dist/capacity/types.js.map +1 -1
- package/dist/capacity/zai.d.ts +14 -0
- package/dist/capacity/zai.d.ts.map +1 -0
- package/dist/capacity/zai.js +203 -0
- package/dist/capacity/zai.js.map +1 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/{adapters → providers/copilot}/CopilotAdapter.d.ts +11 -25
- package/dist/providers/copilot/CopilotAdapter.d.ts.map +1 -0
- package/dist/providers/copilot/CopilotAdapter.js +108 -0
- package/dist/providers/copilot/CopilotAdapter.js.map +1 -0
- package/dist/providers/copilot/CopilotAgentMapper.d.ts +9 -0
- package/dist/providers/copilot/CopilotAgentMapper.d.ts.map +1 -0
- package/dist/providers/copilot/CopilotAgentMapper.js +37 -0
- package/dist/providers/copilot/CopilotAgentMapper.js.map +1 -0
- package/dist/providers/copilot/CopilotSessionLocator.d.ts +19 -0
- package/dist/providers/copilot/CopilotSessionLocator.d.ts.map +1 -0
- package/dist/providers/copilot/CopilotSessionLocator.js +41 -0
- package/dist/providers/copilot/CopilotSessionLocator.js.map +1 -0
- package/dist/providers/copilot/CopilotSessionParser.d.ts +30 -0
- package/dist/providers/copilot/CopilotSessionParser.d.ts.map +1 -0
- package/dist/providers/copilot/CopilotSessionParser.js +181 -0
- package/dist/providers/copilot/CopilotSessionParser.js.map +1 -0
- package/dist/providers/gemini/GeminiAgentMapper.d.ts +9 -0
- package/dist/providers/gemini/GeminiAgentMapper.d.ts.map +1 -0
- package/dist/providers/gemini/GeminiAgentMapper.js +37 -0
- package/dist/providers/gemini/GeminiAgentMapper.js.map +1 -0
- package/dist/providers/gemini/GeminiCliAdapter.d.ts +52 -0
- package/dist/providers/gemini/GeminiCliAdapter.d.ts.map +1 -0
- package/dist/providers/gemini/GeminiCliAdapter.js +188 -0
- package/dist/providers/gemini/GeminiCliAdapter.js.map +1 -0
- package/dist/providers/gemini/GeminiSessionLocator.d.ts +30 -0
- package/dist/providers/gemini/GeminiSessionLocator.d.ts.map +1 -0
- package/dist/providers/gemini/GeminiSessionLocator.js +142 -0
- package/dist/providers/gemini/GeminiSessionLocator.js.map +1 -0
- package/dist/providers/gemini/GeminiSessionParser.d.ts +94 -0
- package/dist/providers/gemini/GeminiSessionParser.d.ts.map +1 -0
- package/dist/providers/gemini/GeminiSessionParser.js +166 -0
- package/dist/providers/gemini/GeminiSessionParser.js.map +1 -0
- package/dist/{adapters → providers/opencode}/OpenCodeAdapter.d.ts +6 -12
- package/dist/providers/opencode/OpenCodeAdapter.d.ts.map +1 -0
- package/dist/providers/opencode/OpenCodeAdapter.js +86 -0
- package/dist/providers/opencode/OpenCodeAdapter.js.map +1 -0
- package/dist/providers/opencode/OpenCodeAgentMapper.d.ts +11 -0
- package/dist/providers/opencode/OpenCodeAgentMapper.d.ts.map +1 -0
- package/dist/providers/opencode/OpenCodeAgentMapper.js +50 -0
- package/dist/providers/opencode/OpenCodeAgentMapper.js.map +1 -0
- package/dist/providers/opencode/OpenCodeSessionLocator.d.ts +21 -0
- package/dist/providers/opencode/OpenCodeSessionLocator.d.ts.map +1 -0
- package/dist/providers/opencode/OpenCodeSessionLocator.js +99 -0
- package/dist/providers/opencode/OpenCodeSessionLocator.js.map +1 -0
- package/dist/providers/opencode/OpenCodeSessionParser.d.ts +18 -0
- package/dist/providers/opencode/OpenCodeSessionParser.d.ts.map +1 -0
- package/dist/providers/opencode/OpenCodeSessionParser.js +104 -0
- package/dist/providers/opencode/OpenCodeSessionParser.js.map +1 -0
- package/dist/runtime/herdr/HerdrAgentDiscovery.d.ts +18 -0
- package/dist/runtime/herdr/HerdrAgentDiscovery.d.ts.map +1 -0
- package/dist/runtime/herdr/HerdrAgentDiscovery.js +183 -0
- package/dist/runtime/herdr/HerdrAgentDiscovery.js.map +1 -0
- package/dist/runtime/herdr/HerdrAgentRuntime.d.ts +5 -0
- package/dist/runtime/herdr/HerdrAgentRuntime.d.ts.map +1 -1
- package/dist/runtime/herdr/HerdrAgentRuntime.js +26 -8
- package/dist/runtime/herdr/HerdrAgentRuntime.js.map +1 -1
- package/dist/runtime/herdr/HerdrResponseParsers.d.ts +2 -0
- package/dist/runtime/herdr/HerdrResponseParsers.d.ts.map +1 -1
- package/dist/runtime/herdr/HerdrResponseParsers.js +12 -1
- package/dist/runtime/herdr/HerdrResponseParsers.js.map +1 -1
- package/dist/utils/AgentRegistry.d.ts.map +1 -1
- package/dist/utils/AgentRegistry.js +5 -5
- package/dist/utils/AgentRegistry.js.map +1 -1
- package/package.json +1 -1
- package/src/AgentManager.ts +67 -5
- package/src/__tests__/AgentManager.test.ts +231 -5
- package/src/__tests__/capacity/codex.test.ts +94 -26
- package/src/__tests__/capacity/fixtures/zai-auth.json +6 -0
- package/src/__tests__/capacity/fixtures/zai-quota-invalid.json +7 -0
- package/src/__tests__/capacity/fixtures/zai-quota.json +38 -0
- package/src/__tests__/capacity/index.test.ts +10 -3
- package/src/__tests__/capacity/zai.test.ts +186 -0
- package/src/__tests__/{adapters → providers/copilot}/CopilotAdapter.test.ts +13 -16
- package/src/__tests__/{adapters → providers/gemini}/GeminiCliAdapter.test.ts +39 -31
- package/src/__tests__/providers/opencode/OpenCodeAdapter.test.ts +197 -0
- package/src/__tests__/providers/opencode/OpenCodeAgentMapper.test.ts +83 -0
- package/src/__tests__/providers/opencode/OpenCodeSessionLocator.test.ts +84 -0
- package/src/__tests__/providers/opencode/OpenCodeSessionParser.test.ts +148 -0
- package/src/__tests__/runtime/HerdrAgentDiscovery.test.ts +131 -0
- package/src/__tests__/runtime/HerdrAgentRuntime.test.ts +20 -1
- package/src/__tests__/utils/AgentRegistry.test.ts +20 -10
- package/src/adapters/index.ts +3 -3
- package/src/capacity/codex.ts +96 -28
- package/src/capacity/index.ts +29 -11
- package/src/capacity/types.ts +8 -0
- package/src/capacity/zai.ts +237 -0
- package/src/index.ts +18 -6
- package/src/providers/copilot/CopilotAdapter.ts +145 -0
- package/src/providers/copilot/CopilotAgentMapper.ts +36 -0
- package/src/providers/copilot/CopilotSessionLocator.ts +57 -0
- package/src/providers/copilot/CopilotSessionParser.ts +289 -0
- package/src/providers/gemini/GeminiAgentMapper.ts +36 -0
- package/src/providers/gemini/GeminiCliAdapter.ts +266 -0
- package/src/providers/gemini/GeminiSessionLocator.ts +183 -0
- package/src/providers/gemini/GeminiSessionParser.ts +271 -0
- package/src/providers/opencode/OpenCodeAdapter.ts +112 -0
- package/src/providers/opencode/OpenCodeAgentMapper.ts +61 -0
- package/src/providers/opencode/OpenCodeSessionLocator.ts +123 -0
- package/src/providers/opencode/OpenCodeSessionParser.ts +145 -0
- package/src/runtime/herdr/HerdrAgentDiscovery.ts +178 -0
- package/src/runtime/herdr/HerdrAgentRuntime.ts +33 -7
- package/src/runtime/herdr/HerdrResponseParsers.ts +11 -1
- package/src/utils/AgentRegistry.ts +8 -6
- package/dist/__tests__/adapters/CopilotAdapter.test.js.map +0 -1
- package/dist/__tests__/adapters/GeminiCliAdapter.test.js.map +0 -1
- package/dist/__tests__/adapters/OpenCodeAdapter.test.js +0 -635
- package/dist/__tests__/adapters/OpenCodeAdapter.test.js.map +0 -1
- package/dist/adapters/CopilotAdapter.d.ts.map +0 -1
- package/dist/adapters/CopilotAdapter.js +0 -329
- package/dist/adapters/CopilotAdapter.js.map +0 -1
- package/dist/adapters/GeminiCliAdapter.d.ts +0 -98
- package/dist/adapters/GeminiCliAdapter.d.ts.map +0 -1
- package/dist/adapters/GeminiCliAdapter.js +0 -513
- package/dist/adapters/GeminiCliAdapter.js.map +0 -1
- package/dist/adapters/OpenCodeAdapter.d.ts.map +0 -1
- package/dist/adapters/OpenCodeAdapter.js +0 -286
- package/dist/adapters/OpenCodeAdapter.js.map +0 -1
- package/src/__tests__/adapters/OpenCodeAdapter.test.ts +0 -543
- package/src/adapters/CopilotAdapter.ts +0 -483
- package/src/adapters/GeminiCliAdapter.ts +0 -699
- package/src/adapters/OpenCodeAdapter.ts +0 -369
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { AgentStatus, type ProcessInfo } from "../../../adapters/AgentAdapter.js";
|
|
7
|
+
import { OpenCodeAdapter } from "../../../providers/opencode/OpenCodeAdapter.js";
|
|
8
|
+
|
|
9
|
+
describe("OpenCodeAdapter", () => {
|
|
10
|
+
let adapter: OpenCodeAdapter;
|
|
11
|
+
let tmpDir: string;
|
|
12
|
+
let dbPath: string;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-adapter-test-"));
|
|
16
|
+
dbPath = path.join(tmpDir, "opencode.db");
|
|
17
|
+
adapter = new OpenCodeAdapter(dbPath);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
adapter.close();
|
|
22
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("detects opencode executables", () => {
|
|
26
|
+
expect(adapter.canHandle({ pid: 1, command: "opencode", cwd: "/repo" })).toBe(true);
|
|
27
|
+
expect(adapter.canHandle({ pid: 2, command: "/usr/bin/opencode.exe", cwd: "/repo" })).toBe(
|
|
28
|
+
true,
|
|
29
|
+
);
|
|
30
|
+
expect(adapter.canHandle({ pid: 3, command: "node /repo/opencode-plugin.js" })).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("returns process-only agents when the database is unavailable", async () => {
|
|
34
|
+
const agents = await adapter.detectAgents({
|
|
35
|
+
processes: [{ pid: 100, command: "opencode", cwd: "/repo", tty: "ttys001" }],
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(agents).toEqual([
|
|
39
|
+
expect.objectContaining({
|
|
40
|
+
type: "opencode",
|
|
41
|
+
status: AgentStatus.RUNNING,
|
|
42
|
+
pid: 100,
|
|
43
|
+
projectPath: "/repo",
|
|
44
|
+
sessionId: "pid-100",
|
|
45
|
+
summary: "OpenCode process running",
|
|
46
|
+
}),
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("maps matching OpenCode sessions to agents", async () => {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
writeDatabase(dbPath, {
|
|
53
|
+
sessions: [{ id: "sess-001", directory: "/repo", timeCreated: now - 60_000 }],
|
|
54
|
+
messages: [
|
|
55
|
+
{
|
|
56
|
+
id: "msg-user",
|
|
57
|
+
sessionId: "sess-001",
|
|
58
|
+
role: "user",
|
|
59
|
+
timeCreated: now - 50_000,
|
|
60
|
+
timeUpdated: now - 50_000,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "msg-assistant",
|
|
64
|
+
sessionId: "sess-001",
|
|
65
|
+
role: "assistant",
|
|
66
|
+
timeCreated: now - 30_000,
|
|
67
|
+
timeUpdated: now - 30_000,
|
|
68
|
+
completed: now - 20_000,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
parts: [
|
|
72
|
+
{
|
|
73
|
+
messageId: "msg-user",
|
|
74
|
+
sessionId: "sess-001",
|
|
75
|
+
type: "text",
|
|
76
|
+
text: "Refactor this module",
|
|
77
|
+
timeCreated: now - 50_000,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const process: ProcessInfo = { pid: 200, command: "opencode", cwd: "/repo", tty: "ttys002" };
|
|
83
|
+
const [agent] = await adapter.detectAgents({ processes: [process] });
|
|
84
|
+
|
|
85
|
+
expect(agent).toMatchObject({
|
|
86
|
+
type: "opencode",
|
|
87
|
+
status: AgentStatus.WAITING,
|
|
88
|
+
pid: 200,
|
|
89
|
+
projectPath: "/repo",
|
|
90
|
+
sessionId: "sess-001",
|
|
91
|
+
summary: "Refactor this module",
|
|
92
|
+
sessionFilePath: `${dbPath}::sess-001`,
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("returns conversation messages for encoded OpenCode session refs", () => {
|
|
97
|
+
writeDatabase(dbPath, {
|
|
98
|
+
sessions: [{ id: "sess-abc", directory: "/repo", timeCreated: Date.now() }],
|
|
99
|
+
messages: [
|
|
100
|
+
{
|
|
101
|
+
id: "msg-user",
|
|
102
|
+
sessionId: "sess-abc",
|
|
103
|
+
role: "user",
|
|
104
|
+
timeCreated: 1000,
|
|
105
|
+
timeUpdated: 1000,
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
parts: [
|
|
109
|
+
{
|
|
110
|
+
messageId: "msg-user",
|
|
111
|
+
sessionId: "sess-abc",
|
|
112
|
+
type: "text",
|
|
113
|
+
text: "Hello agent",
|
|
114
|
+
timeCreated: 1000,
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(adapter.getConversation(`${dbPath}::sess-abc`)).toEqual([
|
|
120
|
+
{ role: "user", content: "Hello agent" },
|
|
121
|
+
]);
|
|
122
|
+
expect(adapter.getConversation("/not-an-opencode-ref")).toEqual([]);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
function writeDatabase(
|
|
127
|
+
filePath: string,
|
|
128
|
+
data: {
|
|
129
|
+
sessions: Array<{ id: string; directory: string; timeCreated: number }>;
|
|
130
|
+
messages?: Array<{
|
|
131
|
+
id: string;
|
|
132
|
+
sessionId: string;
|
|
133
|
+
role: string;
|
|
134
|
+
timeCreated: number;
|
|
135
|
+
timeUpdated: number;
|
|
136
|
+
completed?: number;
|
|
137
|
+
}>;
|
|
138
|
+
parts?: Array<{
|
|
139
|
+
messageId: string;
|
|
140
|
+
sessionId: string;
|
|
141
|
+
type: string;
|
|
142
|
+
text?: string;
|
|
143
|
+
reasoning?: string;
|
|
144
|
+
tool?: string;
|
|
145
|
+
timeCreated: number;
|
|
146
|
+
}>;
|
|
147
|
+
},
|
|
148
|
+
): void {
|
|
149
|
+
const db = new Database(filePath);
|
|
150
|
+
db.exec(`
|
|
151
|
+
CREATE TABLE session (id TEXT, directory TEXT, time_created INTEGER);
|
|
152
|
+
CREATE TABLE message (id TEXT, session_id TEXT, data TEXT, time_created INTEGER, time_updated INTEGER);
|
|
153
|
+
CREATE TABLE part (message_id TEXT, session_id TEXT, data TEXT, time_created INTEGER);
|
|
154
|
+
`);
|
|
155
|
+
|
|
156
|
+
const insertSession = db.prepare(
|
|
157
|
+
"INSERT INTO session (id, directory, time_created) VALUES (?, ?, ?)",
|
|
158
|
+
);
|
|
159
|
+
for (const session of data.sessions) {
|
|
160
|
+
insertSession.run(session.id, session.directory, session.timeCreated);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const insertMessage = db.prepare(
|
|
164
|
+
"INSERT INTO message (id, session_id, data, time_created, time_updated) VALUES (?, ?, ?, ?, ?)",
|
|
165
|
+
);
|
|
166
|
+
for (const message of data.messages ?? []) {
|
|
167
|
+
insertMessage.run(
|
|
168
|
+
message.id,
|
|
169
|
+
message.sessionId,
|
|
170
|
+
JSON.stringify({
|
|
171
|
+
role: message.role,
|
|
172
|
+
time: message.completed === undefined ? {} : { completed: message.completed },
|
|
173
|
+
}),
|
|
174
|
+
message.timeCreated,
|
|
175
|
+
message.timeUpdated,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const insertPart = db.prepare(
|
|
180
|
+
"INSERT INTO part (message_id, session_id, data, time_created) VALUES (?, ?, ?, ?)",
|
|
181
|
+
);
|
|
182
|
+
for (const part of data.parts ?? []) {
|
|
183
|
+
insertPart.run(
|
|
184
|
+
part.messageId,
|
|
185
|
+
part.sessionId,
|
|
186
|
+
JSON.stringify({
|
|
187
|
+
type: part.type,
|
|
188
|
+
text: part.text,
|
|
189
|
+
reasoning: part.reasoning,
|
|
190
|
+
tool: part.tool,
|
|
191
|
+
}),
|
|
192
|
+
part.timeCreated,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
db.close();
|
|
197
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { AgentStatus } from "../../../adapters/AgentAdapter.js";
|
|
3
|
+
import { OpenCodeAgentMapper } from "../../../providers/opencode/OpenCodeAgentMapper.js";
|
|
4
|
+
|
|
5
|
+
describe("OpenCodeAgentMapper", () => {
|
|
6
|
+
const mapper = new OpenCodeAgentMapper("/tmp/opencode.db");
|
|
7
|
+
|
|
8
|
+
it("maps completed assistant sessions as waiting", () => {
|
|
9
|
+
const now = Date.now();
|
|
10
|
+
|
|
11
|
+
const agent = mapper.mapSessionToAgent(
|
|
12
|
+
{ sessionId: "sess-1", directory: "/repo", timeCreated: now - 60_000 },
|
|
13
|
+
{
|
|
14
|
+
lastRole: "assistant",
|
|
15
|
+
lastTimeUpdated: now - 30_000,
|
|
16
|
+
lastAssistantCompleted: true,
|
|
17
|
+
lastAssistantErrored: false,
|
|
18
|
+
summary: "Ship the feature",
|
|
19
|
+
},
|
|
20
|
+
{ pid: 123, command: "opencode", cwd: "/repo" },
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
expect(agent).toMatchObject({
|
|
24
|
+
type: "opencode",
|
|
25
|
+
status: AgentStatus.WAITING,
|
|
26
|
+
summary: "Ship the feature",
|
|
27
|
+
pid: 123,
|
|
28
|
+
projectPath: "/repo",
|
|
29
|
+
sessionId: "sess-1",
|
|
30
|
+
sessionFilePath: "/tmp/opencode.db::sess-1",
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("keeps incomplete assistant sessions running even when they are quiet briefly", () => {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
|
|
37
|
+
const agent = mapper.mapSessionToAgent(
|
|
38
|
+
{ sessionId: "sess-2", directory: "/repo", timeCreated: now - 120_000 },
|
|
39
|
+
{
|
|
40
|
+
lastRole: "assistant",
|
|
41
|
+
lastTimeUpdated: now - 120_000,
|
|
42
|
+
lastAssistantCompleted: false,
|
|
43
|
+
lastAssistantErrored: false,
|
|
44
|
+
summary: "",
|
|
45
|
+
},
|
|
46
|
+
{ pid: 124, command: "opencode", cwd: "/repo" },
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expect(agent.status).toBe(AgentStatus.RUNNING);
|
|
50
|
+
expect(agent.summary).toBe("OpenCode session active");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("marks stale sessions idle", () => {
|
|
54
|
+
const staleTime = Date.now() - 10 * 60 * 1000;
|
|
55
|
+
|
|
56
|
+
const agent = mapper.mapSessionToAgent(
|
|
57
|
+
{ sessionId: "sess-3", directory: "/old", timeCreated: staleTime },
|
|
58
|
+
{
|
|
59
|
+
lastRole: "assistant",
|
|
60
|
+
lastTimeUpdated: staleTime,
|
|
61
|
+
lastAssistantCompleted: true,
|
|
62
|
+
lastAssistantErrored: false,
|
|
63
|
+
summary: "",
|
|
64
|
+
},
|
|
65
|
+
{ pid: 125, command: "opencode", cwd: "/old" },
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(agent.status).toBe(AgentStatus.IDLE);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("maps unmatched processes as running process-only agents", () => {
|
|
72
|
+
const agent = mapper.mapProcessOnlyAgent({ pid: 500, command: "opencode", cwd: "/repo" });
|
|
73
|
+
|
|
74
|
+
expect(agent).toMatchObject({
|
|
75
|
+
type: "opencode",
|
|
76
|
+
status: AgentStatus.RUNNING,
|
|
77
|
+
summary: "OpenCode process running",
|
|
78
|
+
pid: 500,
|
|
79
|
+
projectPath: "/repo",
|
|
80
|
+
sessionId: "pid-500",
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { OpenCodeSessionLocator } from "../../../providers/opencode/OpenCodeSessionLocator.js";
|
|
7
|
+
import { OpenCodeSessionParser } from "../../../providers/opencode/OpenCodeSessionParser.js";
|
|
8
|
+
|
|
9
|
+
describe("OpenCodeSessionLocator", () => {
|
|
10
|
+
let tmpDir: string;
|
|
11
|
+
let dbPath: string;
|
|
12
|
+
let locator: OpenCodeSessionLocator;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-locator-test-"));
|
|
16
|
+
dbPath = path.join(tmpDir, "opencode.db");
|
|
17
|
+
locator = new OpenCodeSessionLocator(dbPath, new OpenCodeSessionParser());
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
locator.close();
|
|
22
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("returns null when the database file does not exist", () => {
|
|
26
|
+
expect(locator.openDb()).toBeNull();
|
|
27
|
+
expect(locator.listSessions()).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("finds the newest session for a directory", () => {
|
|
31
|
+
writeDatabase(dbPath, [
|
|
32
|
+
{ id: "old", directory: "/repo", timeCreated: 1000 },
|
|
33
|
+
{ id: "new", directory: "/repo", timeCreated: 2000 },
|
|
34
|
+
{ id: "other", directory: "/other", timeCreated: 3000 },
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const db = locator.openDb();
|
|
38
|
+
expect(db).not.toBeNull();
|
|
39
|
+
expect(locator.findSessionForDirectory(db!, "/repo")).toEqual({
|
|
40
|
+
sessionId: "new",
|
|
41
|
+
directory: "/repo",
|
|
42
|
+
timeCreated: 2000,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("lists sessions and filters by cwd", () => {
|
|
47
|
+
writeDatabase(dbPath, [
|
|
48
|
+
{ id: "sess-a", directory: "/repo-a", timeCreated: 1000 },
|
|
49
|
+
{ id: "sess-b", directory: "/repo-b", timeCreated: 2000 },
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
const sessions = locator.listSessions({ cwd: "/repo-a" });
|
|
53
|
+
|
|
54
|
+
expect(sessions).toEqual([
|
|
55
|
+
expect.objectContaining({
|
|
56
|
+
type: "opencode",
|
|
57
|
+
sessionId: "sess-a",
|
|
58
|
+
cwd: "/repo-a",
|
|
59
|
+
sessionFilePath: `${dbPath}::sess-a`,
|
|
60
|
+
}),
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
function writeDatabase(
|
|
66
|
+
filePath: string,
|
|
67
|
+
sessions: Array<{ id: string; directory: string; timeCreated: number }>,
|
|
68
|
+
): void {
|
|
69
|
+
const db = new Database(filePath);
|
|
70
|
+
db.exec(`
|
|
71
|
+
CREATE TABLE session (id TEXT, directory TEXT, time_created INTEGER);
|
|
72
|
+
CREATE TABLE message (id TEXT, session_id TEXT, data TEXT, time_created INTEGER, time_updated INTEGER);
|
|
73
|
+
CREATE TABLE part (message_id TEXT, session_id TEXT, data TEXT, time_created INTEGER);
|
|
74
|
+
`);
|
|
75
|
+
|
|
76
|
+
const insertSession = db.prepare(
|
|
77
|
+
"INSERT INTO session (id, directory, time_created) VALUES (?, ?, ?)",
|
|
78
|
+
);
|
|
79
|
+
for (const session of sessions) {
|
|
80
|
+
insertSession.run(session.id, session.directory, session.timeCreated);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
db.close();
|
|
84
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { OpenCodeSessionParser } from "../../../providers/opencode/OpenCodeSessionParser.js";
|
|
4
|
+
|
|
5
|
+
describe("OpenCodeSessionParser", () => {
|
|
6
|
+
let db: Database.Database;
|
|
7
|
+
let parser: OpenCodeSessionParser;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
db = new Database(":memory:");
|
|
11
|
+
db.exec(`
|
|
12
|
+
CREATE TABLE message (id TEXT, session_id TEXT, data TEXT, time_created INTEGER, time_updated INTEGER);
|
|
13
|
+
CREATE TABLE part (message_id TEXT, session_id TEXT, data TEXT, time_created INTEGER);
|
|
14
|
+
`);
|
|
15
|
+
parser = new OpenCodeSessionParser();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
db.close();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("extracts session stats from OpenCode messages and parts", () => {
|
|
23
|
+
insertMessage(db, {
|
|
24
|
+
id: "msg-user",
|
|
25
|
+
sessionId: "sess-1",
|
|
26
|
+
role: "user",
|
|
27
|
+
timeCreated: 1000,
|
|
28
|
+
timeUpdated: 1000,
|
|
29
|
+
});
|
|
30
|
+
insertMessage(db, {
|
|
31
|
+
id: "msg-assistant",
|
|
32
|
+
sessionId: "sess-1",
|
|
33
|
+
role: "assistant",
|
|
34
|
+
timeCreated: 2000,
|
|
35
|
+
timeUpdated: 2500,
|
|
36
|
+
completed: 2400,
|
|
37
|
+
});
|
|
38
|
+
insertPart(db, {
|
|
39
|
+
messageId: "msg-user",
|
|
40
|
+
sessionId: "sess-1",
|
|
41
|
+
data: { type: "text", text: "Plan the migration" },
|
|
42
|
+
timeCreated: 1000,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(parser.getSessionStats(db, "sess-1")).toEqual({
|
|
46
|
+
lastRole: "assistant",
|
|
47
|
+
lastTimeUpdated: 2500,
|
|
48
|
+
lastAssistantCompleted: true,
|
|
49
|
+
lastAssistantErrored: false,
|
|
50
|
+
summary: "Plan the migration",
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("orders stats by message creation time, not metadata update time", () => {
|
|
55
|
+
insertMessage(db, {
|
|
56
|
+
id: "msg-user",
|
|
57
|
+
sessionId: "sess-1",
|
|
58
|
+
role: "user",
|
|
59
|
+
timeCreated: 1000,
|
|
60
|
+
timeUpdated: 3000,
|
|
61
|
+
});
|
|
62
|
+
insertMessage(db, {
|
|
63
|
+
id: "msg-assistant",
|
|
64
|
+
sessionId: "sess-1",
|
|
65
|
+
role: "assistant",
|
|
66
|
+
timeCreated: 2000,
|
|
67
|
+
timeUpdated: 2100,
|
|
68
|
+
completed: 2200,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(parser.getSessionStats(db, "sess-1").lastRole).toBe("assistant");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("reads visible conversation text and hides verbose-only parts by default", () => {
|
|
75
|
+
insertMessage(db, {
|
|
76
|
+
id: "msg-user",
|
|
77
|
+
sessionId: "sess-1",
|
|
78
|
+
role: "user",
|
|
79
|
+
timeCreated: 1000,
|
|
80
|
+
timeUpdated: 1000,
|
|
81
|
+
});
|
|
82
|
+
insertMessage(db, {
|
|
83
|
+
id: "msg-assistant",
|
|
84
|
+
sessionId: "sess-1",
|
|
85
|
+
role: "assistant",
|
|
86
|
+
timeCreated: 2000,
|
|
87
|
+
timeUpdated: 2000,
|
|
88
|
+
});
|
|
89
|
+
insertPart(db, {
|
|
90
|
+
messageId: "msg-user",
|
|
91
|
+
sessionId: "sess-1",
|
|
92
|
+
data: { type: "text", text: "Hello" },
|
|
93
|
+
timeCreated: 1000,
|
|
94
|
+
});
|
|
95
|
+
insertPart(db, {
|
|
96
|
+
messageId: "msg-assistant",
|
|
97
|
+
sessionId: "sess-1",
|
|
98
|
+
data: { type: "reasoning", reasoning: "private thought" },
|
|
99
|
+
timeCreated: 2000,
|
|
100
|
+
});
|
|
101
|
+
insertPart(db, {
|
|
102
|
+
messageId: "msg-assistant",
|
|
103
|
+
sessionId: "sess-1",
|
|
104
|
+
data: { type: "tool", tool: "read_file" },
|
|
105
|
+
timeCreated: 3000,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(parser.getConversation(db, "sess-1")).toEqual([{ role: "user", content: "Hello" }]);
|
|
109
|
+
expect(parser.getConversation(db, "sess-1", { verbose: true })).toEqual([
|
|
110
|
+
{ role: "user", content: "Hello" },
|
|
111
|
+
{ role: "assistant", content: "[thinking] private thought" },
|
|
112
|
+
{ role: "assistant", content: "[tool: read_file]" },
|
|
113
|
+
]);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
function insertMessage(
|
|
118
|
+
db: Database.Database,
|
|
119
|
+
input: {
|
|
120
|
+
id: string;
|
|
121
|
+
sessionId: string;
|
|
122
|
+
role: string;
|
|
123
|
+
timeCreated: number;
|
|
124
|
+
timeUpdated: number;
|
|
125
|
+
completed?: number;
|
|
126
|
+
},
|
|
127
|
+
): void {
|
|
128
|
+
db.prepare(
|
|
129
|
+
"INSERT INTO message (id, session_id, data, time_created, time_updated) VALUES (?, ?, ?, ?, ?)",
|
|
130
|
+
).run(
|
|
131
|
+
input.id,
|
|
132
|
+
input.sessionId,
|
|
133
|
+
JSON.stringify({
|
|
134
|
+
role: input.role,
|
|
135
|
+
time: input.completed === undefined ? {} : { completed: input.completed },
|
|
136
|
+
}),
|
|
137
|
+
input.timeCreated,
|
|
138
|
+
input.timeUpdated,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function insertPart(
|
|
143
|
+
db: Database.Database,
|
|
144
|
+
input: { messageId: string; sessionId: string; data: unknown; timeCreated: number },
|
|
145
|
+
): void {
|
|
146
|
+
db.prepare("INSERT INTO part (message_id, session_id, data, time_created) VALUES (?, ?, ?, ?)")
|
|
147
|
+
.run(input.messageId, input.sessionId, JSON.stringify(input.data), input.timeCreated);
|
|
148
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import {
|
|
2
|
+
extractHerdrAgentPanes,
|
|
3
|
+
fetchHerdrAgentPanes,
|
|
4
|
+
findMatchingHerdrPane,
|
|
5
|
+
} from "../../runtime/herdr/HerdrAgentDiscovery.js";
|
|
6
|
+
import type { HerdrCommandRunner } from "../../runtime/herdr/HerdrCliClient.js";
|
|
7
|
+
|
|
8
|
+
function jsonResponse(value: unknown): { stdout: string; stderr: string } {
|
|
9
|
+
return { stdout: JSON.stringify(value), stderr: "" };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe("HerdrAgentDiscovery", () => {
|
|
13
|
+
it("extracts agent panes from pane list responses", () => {
|
|
14
|
+
const panes = extractHerdrAgentPanes({
|
|
15
|
+
result: {
|
|
16
|
+
panes: [
|
|
17
|
+
{
|
|
18
|
+
agent: "codex",
|
|
19
|
+
agent_session: { value: "session-1" },
|
|
20
|
+
cwd: "/repo",
|
|
21
|
+
pane_id: "w32:p1",
|
|
22
|
+
workspace_id: "w32",
|
|
23
|
+
tab_id: "w32:t1",
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(panes).toEqual([
|
|
30
|
+
{
|
|
31
|
+
agent: "codex",
|
|
32
|
+
agentSessionId: "session-1",
|
|
33
|
+
cwd: "/repo",
|
|
34
|
+
paneId: "w32:p1",
|
|
35
|
+
workspaceId: "w32",
|
|
36
|
+
tabId: "w32:t1",
|
|
37
|
+
},
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("matches by foreground PID before falling back to ambiguous cwd and type", () => {
|
|
42
|
+
const pane = findMatchingHerdrPane(
|
|
43
|
+
{
|
|
44
|
+
type: "codex",
|
|
45
|
+
projectPath: "/repo",
|
|
46
|
+
sessionId: "pid-64904",
|
|
47
|
+
pid: 64904,
|
|
48
|
+
},
|
|
49
|
+
[
|
|
50
|
+
{ agent: "codex", cwd: "/repo", paneId: "w31:p1", foregroundPids: [44290] },
|
|
51
|
+
{ agent: "codex", cwd: "/repo", paneId: "w32:p1", foregroundPids: [64904] },
|
|
52
|
+
],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
expect(pane?.paneId).toBe("w32:p1");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("prefers a foreground PID match over a shell PID match", () => {
|
|
59
|
+
const pane = findMatchingHerdrPane(
|
|
60
|
+
{
|
|
61
|
+
type: "codex",
|
|
62
|
+
projectPath: "/repo",
|
|
63
|
+
sessionId: "pid-64904",
|
|
64
|
+
pid: 64904,
|
|
65
|
+
},
|
|
66
|
+
[
|
|
67
|
+
{ agent: "codex", cwd: "/repo", paneId: "w31:p1", shellPid: 64904 },
|
|
68
|
+
{ agent: "codex", cwd: "/repo", paneId: "w32:p1", foregroundPids: [64904] },
|
|
69
|
+
],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
expect(pane?.paneId).toBe("w32:p1");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("enriches pane list output with foreground process IDs", async () => {
|
|
76
|
+
const runner = vi
|
|
77
|
+
.fn<HerdrCommandRunner>()
|
|
78
|
+
.mockResolvedValueOnce(
|
|
79
|
+
jsonResponse({
|
|
80
|
+
result: {
|
|
81
|
+
panes: [
|
|
82
|
+
{ agent: "codex", cwd: "/repo", pane_id: "w31:p1" },
|
|
83
|
+
{ agent: "codex", cwd: "/repo", pane_id: "w32:p1" },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
)
|
|
88
|
+
.mockResolvedValueOnce(
|
|
89
|
+
jsonResponse({
|
|
90
|
+
result: {
|
|
91
|
+
process_info: {
|
|
92
|
+
pane_id: "w31:p1",
|
|
93
|
+
shell_pid: 111,
|
|
94
|
+
foreground_processes: [{ pid: 44290 }],
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
)
|
|
99
|
+
.mockResolvedValueOnce(
|
|
100
|
+
jsonResponse({
|
|
101
|
+
result: {
|
|
102
|
+
process_info: {
|
|
103
|
+
pane_id: "w32:p1",
|
|
104
|
+
shell_pid: 222,
|
|
105
|
+
foreground_processes: [{ pid: 64904 }],
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const panes = await fetchHerdrAgentPanes(runner);
|
|
112
|
+
|
|
113
|
+
expect(runner).toHaveBeenNthCalledWith(1, "herdr", ["pane", "list"]);
|
|
114
|
+
expect(runner).toHaveBeenNthCalledWith(2, "herdr", [
|
|
115
|
+
"pane",
|
|
116
|
+
"process-info",
|
|
117
|
+
"--pane",
|
|
118
|
+
"w31:p1",
|
|
119
|
+
]);
|
|
120
|
+
expect(runner).toHaveBeenNthCalledWith(3, "herdr", [
|
|
121
|
+
"pane",
|
|
122
|
+
"process-info",
|
|
123
|
+
"--pane",
|
|
124
|
+
"w32:p1",
|
|
125
|
+
]);
|
|
126
|
+
expect(panes).toMatchObject([
|
|
127
|
+
{ paneId: "w31:p1", foregroundPids: [44290], shellPid: 111 },
|
|
128
|
+
{ paneId: "w32:p1", foregroundPids: [64904], shellPid: 222 },
|
|
129
|
+
]);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -186,11 +186,30 @@ describe("HerdrAgentRuntime", () => {
|
|
|
186
186
|
agentResponse(),
|
|
187
187
|
processInfoResponse({ shellPid: 12000 }),
|
|
188
188
|
]);
|
|
189
|
-
const runtime = new HerdrAgentRuntime({ runner, env: {} });
|
|
189
|
+
const runtime = new HerdrAgentRuntime({ runner, env: {}, processInfoPollMs: 0 });
|
|
190
190
|
|
|
191
191
|
await expect(runtime.startAgent(startInput)).resolves.toMatchObject({ pid: 12000 });
|
|
192
192
|
});
|
|
193
193
|
|
|
194
|
+
it("waits for a foreground process pid before falling back to the pane shell pid", async () => {
|
|
195
|
+
const runner = createRunner([
|
|
196
|
+
workspaceResponse(),
|
|
197
|
+
agentResponse(),
|
|
198
|
+
processInfoResponse({ shellPid: 12000 }),
|
|
199
|
+
processInfoResponse({ shellPid: 12000, pid: 12345, processName: "node" }),
|
|
200
|
+
]);
|
|
201
|
+
const runtime = new HerdrAgentRuntime({
|
|
202
|
+
runner,
|
|
203
|
+
env: {},
|
|
204
|
+
processInfoPollMs: 100,
|
|
205
|
+
processInfoPollIntervalMs: 0,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
await expect(runtime.startAgent(startInput)).resolves.toMatchObject({ pid: 12345 });
|
|
209
|
+
expect(runner).toHaveBeenNthCalledWith(3, "herdr", ["pane", "process-info", "--pane", "w1:p1"]);
|
|
210
|
+
expect(runner).toHaveBeenNthCalledWith(4, "herdr", ["pane", "process-info", "--pane", "w1:p1"]);
|
|
211
|
+
});
|
|
212
|
+
|
|
194
213
|
it("passes only explicit extra agent args after the Herdr start separator", async () => {
|
|
195
214
|
const runner = createRunner([
|
|
196
215
|
workspaceResponse(),
|