agent-kanban 1.0.0 → 1.0.2
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/client.d.ts +17 -20
- package/dist/client.js +41 -44
- package/dist/commands/link.js +2 -2
- package/dist/daemon.js +9 -3
- package/dist/index.js +14 -14
- package/dist/processManager.d.ts +2 -3
- package/dist/processManager.js +6 -29
- package/package.json +1 -1
- package/src/client.ts +49 -50
- package/src/commands/link.ts +2 -2
- package/src/daemon.ts +10 -3
- package/src/index.ts +14 -14
- package/src/processManager.ts +9 -30
package/dist/client.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { UsageInfo } from "./types.js";
|
|
2
|
-
export declare class ApiClient {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
export declare abstract class ApiClient {
|
|
3
|
+
protected baseUrl: string;
|
|
4
|
+
constructor(baseUrl: string);
|
|
5
|
+
protected abstract authorize(): Promise<string>;
|
|
6
6
|
private request;
|
|
7
7
|
createTask(input: Record<string, unknown>): Promise<unknown>;
|
|
8
8
|
listTasks(params?: Record<string, string>): Promise<unknown>;
|
|
@@ -56,23 +56,20 @@ export declare class ApiClient {
|
|
|
56
56
|
}): Promise<unknown>;
|
|
57
57
|
getMessages(taskId: string, since?: string): Promise<any[]>;
|
|
58
58
|
}
|
|
59
|
-
export declare class
|
|
60
|
-
private
|
|
59
|
+
export declare class MachineClient extends ApiClient {
|
|
60
|
+
private apiKey;
|
|
61
|
+
constructor();
|
|
62
|
+
protected authorize(): Promise<string>;
|
|
63
|
+
}
|
|
64
|
+
export declare class AgentClient extends ApiClient {
|
|
61
65
|
private agentId;
|
|
62
66
|
private privateKey;
|
|
63
67
|
constructor(baseUrl: string, agentId: string, privateKey: CryptoKey);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
updateAgentUsage(usage: {
|
|
67
|
-
input_tokens: number;
|
|
68
|
-
output_tokens: number;
|
|
69
|
-
cache_read_tokens: number;
|
|
70
|
-
cache_creation_tokens: number;
|
|
71
|
-
cost_micro_usd: number;
|
|
72
|
-
}): Promise<unknown>;
|
|
73
|
-
sendMessage(taskId: string, body: {
|
|
74
|
-
agent_id: string;
|
|
75
|
-
role: string;
|
|
76
|
-
content: string;
|
|
77
|
-
}): Promise<unknown>;
|
|
68
|
+
static fromEnv(): Promise<AgentClient | null>;
|
|
69
|
+
protected authorize(): Promise<string>;
|
|
78
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Returns AgentClient if running as a spawned agent (AK_AGENT_* env vars),
|
|
73
|
+
* otherwise MachineClient (API key from config).
|
|
74
|
+
*/
|
|
75
|
+
export declare function createClient(): Promise<ApiClient>;
|
package/dist/client.js
CHANGED
|
@@ -3,24 +3,16 @@ import { randomUUID } from "crypto";
|
|
|
3
3
|
import { getConfigValue } from "./config.js";
|
|
4
4
|
export class ApiClient {
|
|
5
5
|
baseUrl;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const url = getConfigValue("api-url");
|
|
9
|
-
const key = getConfigValue("api-key");
|
|
10
|
-
if (!url)
|
|
11
|
-
throw new Error("API URL not configured. Run: agent-kanban config set api-url <url>");
|
|
12
|
-
if (!key)
|
|
13
|
-
throw new Error("API key not configured. Run: agent-kanban config set api-key <key>");
|
|
14
|
-
this.baseUrl = url.replace(/\/$/, "");
|
|
15
|
-
this.apiKey = key;
|
|
6
|
+
constructor(baseUrl) {
|
|
7
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
16
8
|
}
|
|
17
9
|
async request(method, path, body) {
|
|
18
|
-
const
|
|
19
|
-
const res = await fetch(
|
|
10
|
+
const authorization = await this.authorize();
|
|
11
|
+
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
20
12
|
method,
|
|
21
13
|
headers: {
|
|
22
14
|
"Content-Type": "application/json",
|
|
23
|
-
Authorization:
|
|
15
|
+
Authorization: authorization,
|
|
24
16
|
},
|
|
25
17
|
body: body ? JSON.stringify(body) : undefined,
|
|
26
18
|
signal: AbortSignal.timeout(10000),
|
|
@@ -97,47 +89,52 @@ export class ApiClient {
|
|
|
97
89
|
return this.request("GET", `/api/tasks/${taskId}/messages${qs}`);
|
|
98
90
|
}
|
|
99
91
|
}
|
|
100
|
-
export class
|
|
101
|
-
|
|
92
|
+
export class MachineClient extends ApiClient {
|
|
93
|
+
apiKey;
|
|
94
|
+
constructor() {
|
|
95
|
+
const url = getConfigValue("api-url");
|
|
96
|
+
const key = getConfigValue("api-key");
|
|
97
|
+
if (!url)
|
|
98
|
+
throw new Error("API URL not configured. Run: agent-kanban config set api-url <url>");
|
|
99
|
+
if (!key)
|
|
100
|
+
throw new Error("API key not configured. Run: agent-kanban config set api-key <key>");
|
|
101
|
+
super(url);
|
|
102
|
+
this.apiKey = key;
|
|
103
|
+
}
|
|
104
|
+
async authorize() {
|
|
105
|
+
return `Bearer ${this.apiKey}`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export class AgentClient extends ApiClient {
|
|
102
109
|
agentId;
|
|
103
110
|
privateKey;
|
|
104
111
|
constructor(baseUrl, agentId, privateKey) {
|
|
105
|
-
|
|
112
|
+
super(baseUrl);
|
|
106
113
|
this.agentId = agentId;
|
|
107
114
|
this.privateKey = privateKey;
|
|
108
115
|
}
|
|
109
|
-
async
|
|
116
|
+
static async fromEnv() {
|
|
117
|
+
const agentId = process.env.AK_AGENT_ID;
|
|
118
|
+
const keyJson = process.env.AK_AGENT_KEY;
|
|
119
|
+
const apiUrl = process.env.AK_API_URL;
|
|
120
|
+
if (!agentId || !keyJson || !apiUrl)
|
|
121
|
+
return null;
|
|
122
|
+
const privateKey = await crypto.subtle.importKey("jwk", JSON.parse(keyJson), { name: "Ed25519" }, false, ["sign"]);
|
|
123
|
+
return new AgentClient(apiUrl, agentId, privateKey);
|
|
124
|
+
}
|
|
125
|
+
async authorize() {
|
|
110
126
|
const jwt = await new SignJWT({ sub: this.agentId, jti: randomUUID(), aud: this.baseUrl })
|
|
111
127
|
.setProtectedHeader({ alg: "EdDSA", typ: "agent+jwt" })
|
|
112
128
|
.setIssuedAt()
|
|
113
129
|
.setExpirationTime("60s")
|
|
114
130
|
.sign(this.privateKey);
|
|
115
|
-
|
|
116
|
-
method,
|
|
117
|
-
headers: {
|
|
118
|
-
"Content-Type": "application/json",
|
|
119
|
-
Authorization: `Bearer ${jwt}`,
|
|
120
|
-
},
|
|
121
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
122
|
-
signal: AbortSignal.timeout(10000),
|
|
123
|
-
});
|
|
124
|
-
const data = await res.json();
|
|
125
|
-
if (!res.ok) {
|
|
126
|
-
const msg = data.error?.message || `HTTP ${res.status}`;
|
|
127
|
-
throw new Error(msg);
|
|
128
|
-
}
|
|
129
|
-
return data;
|
|
130
|
-
}
|
|
131
|
-
// Tasks
|
|
132
|
-
releaseTask(id) {
|
|
133
|
-
return this.request("POST", `/api/tasks/${id}/release`);
|
|
134
|
-
}
|
|
135
|
-
// Agent usage
|
|
136
|
-
updateAgentUsage(usage) {
|
|
137
|
-
return this.request("PATCH", `/api/agents/${this.agentId}/usage`, usage);
|
|
138
|
-
}
|
|
139
|
-
// Messages
|
|
140
|
-
sendMessage(taskId, body) {
|
|
141
|
-
return this.request("POST", `/api/tasks/${taskId}/messages`, body);
|
|
131
|
+
return `Bearer ${jwt}`;
|
|
142
132
|
}
|
|
143
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Returns AgentClient if running as a spawned agent (AK_AGENT_* env vars),
|
|
136
|
+
* otherwise MachineClient (API key from config).
|
|
137
|
+
*/
|
|
138
|
+
export async function createClient() {
|
|
139
|
+
return await AgentClient.fromEnv() ?? new MachineClient();
|
|
140
|
+
}
|
package/dist/commands/link.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
2
|
import { basename } from "path";
|
|
3
|
-
import {
|
|
3
|
+
import { MachineClient } from "../client.js";
|
|
4
4
|
import { setLink } from "../links.js";
|
|
5
5
|
function getGitRepoRoot() {
|
|
6
6
|
return execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
|
|
@@ -31,7 +31,7 @@ export function registerLinkCommand(program) {
|
|
|
31
31
|
console.error("No git remote found. Add an origin remote first.");
|
|
32
32
|
process.exit(1);
|
|
33
33
|
}
|
|
34
|
-
const client = new
|
|
34
|
+
const client = new MachineClient();
|
|
35
35
|
let repo;
|
|
36
36
|
try {
|
|
37
37
|
repo = await client.createRepository({
|
package/dist/daemon.js
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from "path";
|
|
|
3
3
|
import { hostname, platform, arch, release } from "os";
|
|
4
4
|
import { execSync } from "child_process";
|
|
5
5
|
import { randomUUID } from "crypto";
|
|
6
|
-
import {
|
|
6
|
+
import { MachineClient, AgentClient } from "./client.js";
|
|
7
7
|
import { ProcessManager } from "./processManager.js";
|
|
8
8
|
import { getLinks, findPathForRepository } from "./links.js";
|
|
9
9
|
import { getConfigValue, setConfigValue, PID_FILE } from "./config.js";
|
|
@@ -22,7 +22,7 @@ export async function startDaemon(opts) {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
writeFileSync(PID_FILE, String(process.pid));
|
|
25
|
-
const client = new
|
|
25
|
+
const client = new MachineClient();
|
|
26
26
|
const links = getLinks();
|
|
27
27
|
const linkedRepoCount = Object.keys(links).length;
|
|
28
28
|
if (linkedRepoCount === 0) {
|
|
@@ -108,6 +108,7 @@ export async function startDaemon(opts) {
|
|
|
108
108
|
const sessionId = randomUUID();
|
|
109
109
|
const { publicKey, privateKey } = await crypto.subtle.generateKey({ name: "Ed25519" }, true, ["sign", "verify"]);
|
|
110
110
|
const pubKeyJwk = await crypto.subtle.exportKey("jwk", publicKey);
|
|
111
|
+
const privKeyJwk = await crypto.subtle.exportKey("jwk", privateKey);
|
|
111
112
|
const pubKeyBase64 = pubKeyJwk.x;
|
|
112
113
|
try {
|
|
113
114
|
await client.registerAgent(sessionId, pubKeyBase64, opts.agentCli);
|
|
@@ -128,8 +129,13 @@ export async function startDaemon(opts) {
|
|
|
128
129
|
return;
|
|
129
130
|
}
|
|
130
131
|
const agentClient = new AgentClient(getConfigValue("api-url"), sessionId, privateKey);
|
|
132
|
+
const agentEnv = {
|
|
133
|
+
AK_AGENT_ID: sessionId,
|
|
134
|
+
AK_AGENT_KEY: JSON.stringify(privKeyJwk),
|
|
135
|
+
AK_API_URL: getConfigValue("api-url"),
|
|
136
|
+
};
|
|
131
137
|
const prompt = `You have a new task assigned to you. Task ID: ${task.id}\nFollow the agent-kanban skill workflow: claim the task, do the work, create a PR with gh, then submit for review with ak task review --pr-url <url>. Do NOT call task complete — only humans can complete tasks.`;
|
|
132
|
-
await pm.spawnAgent(task.id, sessionId, repoDir, prompt, agentClient);
|
|
138
|
+
await pm.spawnAgent(task.id, sessionId, repoDir, prompt, agentClient, agentEnv);
|
|
133
139
|
backoffMs = baseInterval;
|
|
134
140
|
schedulePoll(1000);
|
|
135
141
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { setConfigValue, getConfigValue } from "./config.js";
|
|
4
|
-
import {
|
|
4
|
+
import { MachineClient, createClient } from "./client.js";
|
|
5
5
|
import { getFormat, output, formatTaskList, formatBoard, formatAgentList, formatBoardList, formatRepositoryList } from "./output.js";
|
|
6
6
|
import { registerLinkCommand } from "./commands/link.js";
|
|
7
7
|
import { registerStartCommand } from "./commands/start.js";
|
|
@@ -44,7 +44,7 @@ taskCmd
|
|
|
44
44
|
.option("--depends-on <ids>", "Comma-separated task IDs this depends on")
|
|
45
45
|
.option("--format <format>", "Output format (json, text)")
|
|
46
46
|
.action(async (opts) => {
|
|
47
|
-
const client =
|
|
47
|
+
const client = await createClient();
|
|
48
48
|
const body = { title: opts.title };
|
|
49
49
|
if (opts.description)
|
|
50
50
|
body.description = opts.description;
|
|
@@ -82,7 +82,7 @@ taskCmd
|
|
|
82
82
|
.option("--parent <id>", "Filter subtasks of a parent task")
|
|
83
83
|
.option("--format <format>", "Output format (json, text)")
|
|
84
84
|
.action(async (opts) => {
|
|
85
|
-
const client =
|
|
85
|
+
const client = await createClient();
|
|
86
86
|
const params = {};
|
|
87
87
|
if (opts.repo)
|
|
88
88
|
params.repository_id = opts.repo;
|
|
@@ -102,7 +102,7 @@ taskCmd
|
|
|
102
102
|
.option("--agent-name <name>", "Agent identity")
|
|
103
103
|
.option("--format <format>", "Output format (json, text)")
|
|
104
104
|
.action(async (id, opts) => {
|
|
105
|
-
const client =
|
|
105
|
+
const client = await createClient();
|
|
106
106
|
const task = await client.claimTask(id, opts.agentName);
|
|
107
107
|
const fmt = getFormat(opts.format);
|
|
108
108
|
output(task, fmt, (t) => `Claimed task ${t.id}: ${t.title} (now in progress)`);
|
|
@@ -112,7 +112,7 @@ taskCmd
|
|
|
112
112
|
.description("Add a log entry to a task")
|
|
113
113
|
.option("--agent-name <name>", "Agent identity")
|
|
114
114
|
.action(async (id, message, opts) => {
|
|
115
|
-
const client =
|
|
115
|
+
const client = await createClient();
|
|
116
116
|
await client.addLog(id, message, opts.agentName);
|
|
117
117
|
console.log("Log entry added.");
|
|
118
118
|
});
|
|
@@ -122,7 +122,7 @@ taskCmd
|
|
|
122
122
|
.option("--agent-name <name>", "Agent identity")
|
|
123
123
|
.option("--format <format>", "Output format (json, text)")
|
|
124
124
|
.action(async (id, opts) => {
|
|
125
|
-
const client =
|
|
125
|
+
const client = await createClient();
|
|
126
126
|
const body = {};
|
|
127
127
|
if (opts.agentName)
|
|
128
128
|
body.agent_name = opts.agentName;
|
|
@@ -137,7 +137,7 @@ taskCmd
|
|
|
137
137
|
.option("--agent-name <name>", "Agent identity")
|
|
138
138
|
.option("--format <format>", "Output format (json, text)")
|
|
139
139
|
.action(async (id, opts) => {
|
|
140
|
-
const client =
|
|
140
|
+
const client = await createClient();
|
|
141
141
|
const body = {};
|
|
142
142
|
if (opts.prUrl)
|
|
143
143
|
body.pr_url = opts.prUrl;
|
|
@@ -155,7 +155,7 @@ taskCmd
|
|
|
155
155
|
.option("--agent-name <name>", "Agent identity")
|
|
156
156
|
.option("--format <format>", "Output format (json, text)")
|
|
157
157
|
.action(async (id, opts) => {
|
|
158
|
-
const client =
|
|
158
|
+
const client = await createClient();
|
|
159
159
|
const body = {};
|
|
160
160
|
if (opts.result)
|
|
161
161
|
body.result = opts.result;
|
|
@@ -174,7 +174,7 @@ agentCmd
|
|
|
174
174
|
.description("List all agents")
|
|
175
175
|
.option("--format <format>", "Output format (json, text)")
|
|
176
176
|
.action(async (opts) => {
|
|
177
|
-
const client = new
|
|
177
|
+
const client = new MachineClient();
|
|
178
178
|
const agents = await client.listAgents();
|
|
179
179
|
const fmt = getFormat(opts.format);
|
|
180
180
|
output(agents, fmt, formatAgentList);
|
|
@@ -188,7 +188,7 @@ boardCmd
|
|
|
188
188
|
.option("--description <desc>", "Board description")
|
|
189
189
|
.option("--format <format>", "Output format (json, text)")
|
|
190
190
|
.action(async (opts) => {
|
|
191
|
-
const client = new
|
|
191
|
+
const client = new MachineClient();
|
|
192
192
|
const board = await client.createBoard({ name: opts.name, description: opts.description });
|
|
193
193
|
const fmt = getFormat(opts.format);
|
|
194
194
|
output(board, fmt, (b) => `Created board ${b.id}: ${b.name}`);
|
|
@@ -198,7 +198,7 @@ boardCmd
|
|
|
198
198
|
.description("List all boards")
|
|
199
199
|
.option("--format <format>", "Output format (json, text)")
|
|
200
200
|
.action(async (opts) => {
|
|
201
|
-
const client = new
|
|
201
|
+
const client = new MachineClient();
|
|
202
202
|
const boards = await client.listBoards();
|
|
203
203
|
const fmt = getFormat(opts.format);
|
|
204
204
|
output(boards, fmt, formatBoardList);
|
|
@@ -209,7 +209,7 @@ boardCmd
|
|
|
209
209
|
.option("--board <name-or-id>", "Board name or ID (uses first if omitted)")
|
|
210
210
|
.option("--format <format>", "Output format (json, text)")
|
|
211
211
|
.action(async (opts) => {
|
|
212
|
-
const client = new
|
|
212
|
+
const client = new MachineClient();
|
|
213
213
|
let boardId;
|
|
214
214
|
if (opts.board) {
|
|
215
215
|
boardId = await resolveBoardId(client, opts.board);
|
|
@@ -244,7 +244,7 @@ repoCmd
|
|
|
244
244
|
.requiredOption("--url <url>", "Clone URL")
|
|
245
245
|
.option("--format <format>", "Output format (json, text)")
|
|
246
246
|
.action(async (opts) => {
|
|
247
|
-
const client = new
|
|
247
|
+
const client = new MachineClient();
|
|
248
248
|
const repo = await client.createRepository({ name: opts.name, url: opts.url });
|
|
249
249
|
const fmt = getFormat(opts.format);
|
|
250
250
|
output(repo, fmt, (r) => `Added repository ${r.id}: ${r.name}`);
|
|
@@ -254,7 +254,7 @@ repoCmd
|
|
|
254
254
|
.description("List repositories")
|
|
255
255
|
.option("--format <format>", "Output format (json, text)")
|
|
256
256
|
.action(async (opts) => {
|
|
257
|
-
const client = new
|
|
257
|
+
const client = new MachineClient();
|
|
258
258
|
const repos = await client.listRepositories();
|
|
259
259
|
const fmt = getFormat(opts.format);
|
|
260
260
|
output(repos, fmt, formatRepositoryList);
|
package/dist/processManager.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export interface AgentProcess {
|
|
|
4
4
|
taskId: string;
|
|
5
5
|
sessionId: string;
|
|
6
6
|
process: ChildProcess;
|
|
7
|
-
agentClient
|
|
7
|
+
agentClient: AgentClient;
|
|
8
8
|
timeoutTimer?: ReturnType<typeof setTimeout>;
|
|
9
9
|
}
|
|
10
10
|
export declare class ProcessManager {
|
|
@@ -16,9 +16,8 @@ export declare class ProcessManager {
|
|
|
16
16
|
constructor(client: ApiClient, agentCli: string, onSlotFreed: () => void, taskTimeoutMs?: number);
|
|
17
17
|
get activeCount(): number;
|
|
18
18
|
hasTask(taskId: string): boolean;
|
|
19
|
-
spawnAgent(taskId: string, sessionId: string, cwd: string, taskContext: string, agentClient
|
|
19
|
+
spawnAgent(taskId: string, sessionId: string, cwd: string, taskContext: string, agentClient: AgentClient, agentEnv: Record<string, string>): Promise<void>;
|
|
20
20
|
killAll(): Promise<void>;
|
|
21
21
|
private handleEvent;
|
|
22
|
-
private postMessage;
|
|
23
22
|
private releaseTask;
|
|
24
23
|
}
|
package/dist/processManager.js
CHANGED
|
@@ -17,7 +17,7 @@ export class ProcessManager {
|
|
|
17
17
|
hasTask(taskId) {
|
|
18
18
|
return this.agents.has(taskId);
|
|
19
19
|
}
|
|
20
|
-
async spawnAgent(taskId, sessionId, cwd, taskContext, agentClient) {
|
|
20
|
+
async spawnAgent(taskId, sessionId, cwd, taskContext, agentClient, agentEnv) {
|
|
21
21
|
const args = [
|
|
22
22
|
"--print",
|
|
23
23
|
"--verbose",
|
|
@@ -32,7 +32,7 @@ export class ProcessManager {
|
|
|
32
32
|
proc = spawn(this.agentCli, args, {
|
|
33
33
|
cwd,
|
|
34
34
|
stdio: ["pipe", "pipe", "pipe"],
|
|
35
|
-
env: { ...process.env },
|
|
35
|
+
env: { ...process.env, ...agentEnv },
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
38
|
catch (err) {
|
|
@@ -47,14 +47,12 @@ export class ProcessManager {
|
|
|
47
47
|
}
|
|
48
48
|
const agent = { taskId, sessionId, process: proc, agentClient };
|
|
49
49
|
this.agents.set(taskId, agent);
|
|
50
|
-
// Kill agent if it exceeds the task timeout
|
|
51
50
|
if (this.taskTimeoutMs > 0) {
|
|
52
51
|
agent.timeoutTimer = setTimeout(() => {
|
|
53
52
|
console.warn(`[WARN] Agent for task ${taskId} exceeded timeout (${Math.round(this.taskTimeoutMs / 60000)}m), killing`);
|
|
54
53
|
proc.kill("SIGTERM");
|
|
55
54
|
}, this.taskTimeoutMs);
|
|
56
55
|
}
|
|
57
|
-
// Send task context as JSON message via stdin, then close
|
|
58
56
|
proc.on("spawn", () => {
|
|
59
57
|
const payload = JSON.stringify({
|
|
60
58
|
type: "user",
|
|
@@ -63,7 +61,6 @@ export class ProcessManager {
|
|
|
63
61
|
proc.stdin?.write(payload + "\n");
|
|
64
62
|
proc.stdin?.end();
|
|
65
63
|
});
|
|
66
|
-
// Parse stdout (stream-json): each line is a JSON event
|
|
67
64
|
let stdoutBuffer = "";
|
|
68
65
|
proc.stdout?.on("data", (chunk) => {
|
|
69
66
|
stdoutBuffer += chunk.toString();
|
|
@@ -79,19 +76,15 @@ export class ProcessManager {
|
|
|
79
76
|
catch { /* non-JSON line, skip */ }
|
|
80
77
|
}
|
|
81
78
|
});
|
|
82
|
-
// Capture stderr for crash diagnostics
|
|
83
79
|
let stderrBuffer = "";
|
|
84
80
|
proc.stderr?.on("data", (chunk) => {
|
|
85
81
|
stderrBuffer += chunk.toString();
|
|
86
82
|
if (stderrBuffer.length > 10000)
|
|
87
83
|
stderrBuffer = stderrBuffer.slice(-5000);
|
|
88
84
|
});
|
|
89
|
-
// Handle exit
|
|
90
85
|
proc.on("close", async (code) => {
|
|
91
|
-
// Clear timeout timer
|
|
92
86
|
if (agent.timeoutTimer)
|
|
93
87
|
clearTimeout(agent.timeoutTimer);
|
|
94
|
-
// Flush remaining stdout
|
|
95
88
|
if (stdoutBuffer.trim()) {
|
|
96
89
|
try {
|
|
97
90
|
const event = JSON.parse(stdoutBuffer);
|
|
@@ -135,41 +128,25 @@ export class ProcessManager {
|
|
|
135
128
|
}
|
|
136
129
|
}
|
|
137
130
|
handleEvent(taskId, sessionId, event, agentClient) {
|
|
138
|
-
// Extract text from assistant messages → post as agent chat messages
|
|
139
131
|
if (event.type === "assistant" && Array.isArray(event.message?.content)) {
|
|
140
132
|
for (const block of event.message.content) {
|
|
141
133
|
if (block.type === "text" && block.text) {
|
|
142
|
-
|
|
134
|
+
agentClient.sendMessage(taskId, { agent_id: sessionId, role: "agent", content: block.text })
|
|
135
|
+
.catch((err) => console.error(`[ERROR] Failed to send message for task ${taskId}: ${err.message}`));
|
|
143
136
|
}
|
|
144
137
|
}
|
|
145
138
|
}
|
|
146
|
-
// Report usage on result event
|
|
147
139
|
if (event.type === "result") {
|
|
148
140
|
const cost = event.total_cost_usd || 0;
|
|
149
141
|
const usage = event.usage || {};
|
|
150
142
|
console.log(`[INFO] Agent result for task ${taskId}: cost=$${cost.toFixed(4)}`);
|
|
151
|
-
|
|
143
|
+
agentClient.updateAgentUsage(sessionId, {
|
|
152
144
|
input_tokens: usage.input_tokens || 0,
|
|
153
145
|
output_tokens: usage.output_tokens || 0,
|
|
154
146
|
cache_read_tokens: usage.cache_read_input_tokens || 0,
|
|
155
147
|
cache_creation_tokens: usage.cache_creation_input_tokens || 0,
|
|
156
148
|
cost_micro_usd: Math.round(cost * 1_000_000),
|
|
157
|
-
};
|
|
158
|
-
if (agentClient) {
|
|
159
|
-
agentClient.updateAgentUsage(usageData).catch(() => { });
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
this.client.updateAgentUsage(sessionId, usageData).catch(() => { });
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
async postMessage(taskId, sessionId, role, content, agentClient) {
|
|
167
|
-
const body = { agent_id: sessionId, role, content };
|
|
168
|
-
if (agentClient) {
|
|
169
|
-
await agentClient.sendMessage(taskId, body);
|
|
170
|
-
}
|
|
171
|
-
else {
|
|
172
|
-
await this.client.sendMessage(taskId, body);
|
|
149
|
+
}).catch((err) => console.error(`[ERROR] Failed to report usage for task ${taskId}: ${err.message}`));
|
|
173
150
|
}
|
|
174
151
|
}
|
|
175
152
|
async releaseTask(taskId, retries = 3) {
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -3,26 +3,22 @@ import { randomUUID } from "crypto";
|
|
|
3
3
|
import type { UsageInfo } from "./types.js";
|
|
4
4
|
import { getConfigValue } from "./config.js";
|
|
5
5
|
|
|
6
|
-
export class ApiClient {
|
|
7
|
-
|
|
8
|
-
private apiKey: string;
|
|
6
|
+
export abstract class ApiClient {
|
|
7
|
+
protected baseUrl: string;
|
|
9
8
|
|
|
10
|
-
constructor() {
|
|
11
|
-
|
|
12
|
-
const key = getConfigValue("api-key");
|
|
13
|
-
if (!url) throw new Error("API URL not configured. Run: agent-kanban config set api-url <url>");
|
|
14
|
-
if (!key) throw new Error("API key not configured. Run: agent-kanban config set api-key <key>");
|
|
15
|
-
this.baseUrl = url.replace(/\/$/, "");
|
|
16
|
-
this.apiKey = key;
|
|
9
|
+
constructor(baseUrl: string) {
|
|
10
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
17
11
|
}
|
|
18
12
|
|
|
13
|
+
protected abstract authorize(): Promise<string>;
|
|
14
|
+
|
|
19
15
|
private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
20
|
-
const
|
|
21
|
-
const res = await fetch(
|
|
16
|
+
const authorization = await this.authorize();
|
|
17
|
+
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
22
18
|
method,
|
|
23
19
|
headers: {
|
|
24
20
|
"Content-Type": "application/json",
|
|
25
|
-
Authorization:
|
|
21
|
+
Authorization: authorization,
|
|
26
22
|
},
|
|
27
23
|
body: body ? JSON.stringify(body) : undefined,
|
|
28
24
|
signal: AbortSignal.timeout(10000),
|
|
@@ -110,56 +106,59 @@ export class ApiClient {
|
|
|
110
106
|
}
|
|
111
107
|
}
|
|
112
108
|
|
|
113
|
-
export class
|
|
114
|
-
private
|
|
109
|
+
export class MachineClient extends ApiClient {
|
|
110
|
+
private apiKey: string;
|
|
111
|
+
|
|
112
|
+
constructor() {
|
|
113
|
+
const url = getConfigValue("api-url");
|
|
114
|
+
const key = getConfigValue("api-key");
|
|
115
|
+
if (!url) throw new Error("API URL not configured. Run: agent-kanban config set api-url <url>");
|
|
116
|
+
if (!key) throw new Error("API key not configured. Run: agent-kanban config set api-key <key>");
|
|
117
|
+
super(url);
|
|
118
|
+
this.apiKey = key;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
protected async authorize(): Promise<string> {
|
|
122
|
+
return `Bearer ${this.apiKey}`;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export class AgentClient extends ApiClient {
|
|
115
127
|
private agentId: string;
|
|
116
128
|
private privateKey: CryptoKey;
|
|
117
129
|
|
|
118
130
|
constructor(baseUrl: string, agentId: string, privateKey: CryptoKey) {
|
|
119
|
-
|
|
131
|
+
super(baseUrl);
|
|
120
132
|
this.agentId = agentId;
|
|
121
133
|
this.privateKey = privateKey;
|
|
122
134
|
}
|
|
123
135
|
|
|
124
|
-
|
|
136
|
+
static async fromEnv(): Promise<AgentClient | null> {
|
|
137
|
+
const agentId = process.env.AK_AGENT_ID;
|
|
138
|
+
const keyJson = process.env.AK_AGENT_KEY;
|
|
139
|
+
const apiUrl = process.env.AK_API_URL;
|
|
140
|
+
if (!agentId || !keyJson || !apiUrl) return null;
|
|
141
|
+
|
|
142
|
+
const privateKey = await crypto.subtle.importKey(
|
|
143
|
+
"jwk", JSON.parse(keyJson), { name: "Ed25519" } as any, false, ["sign"],
|
|
144
|
+
);
|
|
145
|
+
return new AgentClient(apiUrl, agentId, privateKey);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
protected async authorize(): Promise<string> {
|
|
125
149
|
const jwt = await new SignJWT({ sub: this.agentId, jti: randomUUID(), aud: this.baseUrl })
|
|
126
150
|
.setProtectedHeader({ alg: "EdDSA", typ: "agent+jwt" })
|
|
127
151
|
.setIssuedAt()
|
|
128
152
|
.setExpirationTime("60s")
|
|
129
153
|
.sign(this.privateKey);
|
|
130
|
-
|
|
131
|
-
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
132
|
-
method,
|
|
133
|
-
headers: {
|
|
134
|
-
"Content-Type": "application/json",
|
|
135
|
-
Authorization: `Bearer ${jwt}`,
|
|
136
|
-
},
|
|
137
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
138
|
-
signal: AbortSignal.timeout(10000),
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
const data = await res.json() as T & { error?: { code: string; message: string } };
|
|
142
|
-
|
|
143
|
-
if (!res.ok) {
|
|
144
|
-
const msg = (data as any).error?.message || `HTTP ${res.status}`;
|
|
145
|
-
throw new Error(msg);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return data;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Tasks
|
|
152
|
-
releaseTask(id: string) {
|
|
153
|
-
return this.request("POST", `/api/tasks/${id}/release`);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Agent usage
|
|
157
|
-
updateAgentUsage(usage: { input_tokens: number; output_tokens: number; cache_read_tokens: number; cache_creation_tokens: number; cost_micro_usd: number }) {
|
|
158
|
-
return this.request("PATCH", `/api/agents/${this.agentId}/usage`, usage);
|
|
154
|
+
return `Bearer ${jwt}`;
|
|
159
155
|
}
|
|
156
|
+
}
|
|
160
157
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
158
|
+
/**
|
|
159
|
+
* Returns AgentClient if running as a spawned agent (AK_AGENT_* env vars),
|
|
160
|
+
* otherwise MachineClient (API key from config).
|
|
161
|
+
*/
|
|
162
|
+
export async function createClient(): Promise<ApiClient> {
|
|
163
|
+
return await AgentClient.fromEnv() ?? new MachineClient();
|
|
165
164
|
}
|
package/src/commands/link.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
2
|
import { basename } from "path";
|
|
3
3
|
import type { Command } from "commander";
|
|
4
|
-
import {
|
|
4
|
+
import { MachineClient } from "../client.js";
|
|
5
5
|
import { setLink } from "../links.js";
|
|
6
6
|
|
|
7
7
|
function getGitRepoRoot(): string {
|
|
@@ -35,7 +35,7 @@ export function registerLinkCommand(program: Command) {
|
|
|
35
35
|
process.exit(1);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
const client = new
|
|
38
|
+
const client = new MachineClient();
|
|
39
39
|
let repo: any;
|
|
40
40
|
try {
|
|
41
41
|
repo = await client.createRepository({
|
package/src/daemon.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from "path";
|
|
|
3
3
|
import { hostname, platform, arch, release } from "os";
|
|
4
4
|
import { execSync } from "child_process";
|
|
5
5
|
import { randomUUID } from "crypto";
|
|
6
|
-
import {
|
|
6
|
+
import { MachineClient, AgentClient } from "./client.js";
|
|
7
7
|
import { ProcessManager } from "./processManager.js";
|
|
8
8
|
import { getLinks, findPathForRepository } from "./links.js";
|
|
9
9
|
import { getConfigValue, setConfigValue, PID_FILE } from "./config.js";
|
|
@@ -37,7 +37,7 @@ export async function startDaemon(opts: DaemonOptions): Promise<void> {
|
|
|
37
37
|
}
|
|
38
38
|
writeFileSync(PID_FILE, String(process.pid));
|
|
39
39
|
|
|
40
|
-
const client = new
|
|
40
|
+
const client = new MachineClient();
|
|
41
41
|
const links = getLinks();
|
|
42
42
|
const linkedRepoCount = Object.keys(links).length;
|
|
43
43
|
|
|
@@ -135,6 +135,7 @@ export async function startDaemon(opts: DaemonOptions): Promise<void> {
|
|
|
135
135
|
{ name: "Ed25519" } as any, true, ["sign", "verify"]
|
|
136
136
|
);
|
|
137
137
|
const pubKeyJwk = await crypto.subtle.exportKey("jwk", publicKey);
|
|
138
|
+
const privKeyJwk = await crypto.subtle.exportKey("jwk", privateKey);
|
|
138
139
|
const pubKeyBase64 = pubKeyJwk.x;
|
|
139
140
|
|
|
140
141
|
try {
|
|
@@ -165,8 +166,14 @@ export async function startDaemon(opts: DaemonOptions): Promise<void> {
|
|
|
165
166
|
privateKey,
|
|
166
167
|
);
|
|
167
168
|
|
|
169
|
+
const agentEnv = {
|
|
170
|
+
AK_AGENT_ID: sessionId,
|
|
171
|
+
AK_AGENT_KEY: JSON.stringify(privKeyJwk),
|
|
172
|
+
AK_API_URL: getConfigValue("api-url")!,
|
|
173
|
+
};
|
|
174
|
+
|
|
168
175
|
const prompt = `You have a new task assigned to you. Task ID: ${task.id}\nFollow the agent-kanban skill workflow: claim the task, do the work, create a PR with gh, then submit for review with ak task review --pr-url <url>. Do NOT call task complete — only humans can complete tasks.`;
|
|
169
|
-
await pm.spawnAgent(task.id, sessionId, repoDir, prompt, agentClient);
|
|
176
|
+
await pm.spawnAgent(task.id, sessionId, repoDir, prompt, agentClient, agentEnv);
|
|
170
177
|
|
|
171
178
|
backoffMs = baseInterval;
|
|
172
179
|
schedulePoll(1000);
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { setConfigValue, getConfigValue } from "./config.js";
|
|
4
|
-
import { ApiClient } from "./client.js";
|
|
4
|
+
import { type ApiClient, MachineClient, createClient } from "./client.js";
|
|
5
5
|
import { getFormat, output, formatTaskList, formatBoard, formatAgentList, formatBoardList, formatRepositoryList } from "./output.js";
|
|
6
6
|
import { registerLinkCommand } from "./commands/link.js";
|
|
7
7
|
import { registerStartCommand } from "./commands/start.js";
|
|
@@ -51,7 +51,7 @@ taskCmd
|
|
|
51
51
|
.option("--depends-on <ids>", "Comma-separated task IDs this depends on")
|
|
52
52
|
.option("--format <format>", "Output format (json, text)")
|
|
53
53
|
.action(async (opts) => {
|
|
54
|
-
const client =
|
|
54
|
+
const client = await createClient();
|
|
55
55
|
|
|
56
56
|
const body: Record<string, unknown> = { title: opts.title };
|
|
57
57
|
if (opts.description) body.description = opts.description;
|
|
@@ -80,7 +80,7 @@ taskCmd
|
|
|
80
80
|
.option("--parent <id>", "Filter subtasks of a parent task")
|
|
81
81
|
.option("--format <format>", "Output format (json, text)")
|
|
82
82
|
.action(async (opts) => {
|
|
83
|
-
const client =
|
|
83
|
+
const client = await createClient();
|
|
84
84
|
const params: Record<string, string> = {};
|
|
85
85
|
if (opts.repo) params.repository_id = opts.repo;
|
|
86
86
|
if (opts.status) params.status = opts.status;
|
|
@@ -98,7 +98,7 @@ taskCmd
|
|
|
98
98
|
.option("--agent-name <name>", "Agent identity")
|
|
99
99
|
.option("--format <format>", "Output format (json, text)")
|
|
100
100
|
.action(async (id, opts) => {
|
|
101
|
-
const client =
|
|
101
|
+
const client = await createClient();
|
|
102
102
|
const task = await client.claimTask(id, opts.agentName);
|
|
103
103
|
const fmt = getFormat(opts.format);
|
|
104
104
|
output(task, fmt, (t: any) => `Claimed task ${t.id}: ${t.title} (now in progress)`);
|
|
@@ -109,7 +109,7 @@ taskCmd
|
|
|
109
109
|
.description("Add a log entry to a task")
|
|
110
110
|
.option("--agent-name <name>", "Agent identity")
|
|
111
111
|
.action(async (id, message, opts) => {
|
|
112
|
-
const client =
|
|
112
|
+
const client = await createClient();
|
|
113
113
|
await client.addLog(id, message, opts.agentName);
|
|
114
114
|
console.log("Log entry added.");
|
|
115
115
|
});
|
|
@@ -120,7 +120,7 @@ taskCmd
|
|
|
120
120
|
.option("--agent-name <name>", "Agent identity")
|
|
121
121
|
.option("--format <format>", "Output format (json, text)")
|
|
122
122
|
.action(async (id, opts) => {
|
|
123
|
-
const client =
|
|
123
|
+
const client = await createClient();
|
|
124
124
|
const body: Record<string, unknown> = {};
|
|
125
125
|
if (opts.agentName) body.agent_name = opts.agentName;
|
|
126
126
|
const task = await client.cancelTask(id, body);
|
|
@@ -135,7 +135,7 @@ taskCmd
|
|
|
135
135
|
.option("--agent-name <name>", "Agent identity")
|
|
136
136
|
.option("--format <format>", "Output format (json, text)")
|
|
137
137
|
.action(async (id, opts) => {
|
|
138
|
-
const client =
|
|
138
|
+
const client = await createClient();
|
|
139
139
|
const body: Record<string, unknown> = {};
|
|
140
140
|
if (opts.prUrl) body.pr_url = opts.prUrl;
|
|
141
141
|
if (opts.agentName) body.agent_name = opts.agentName;
|
|
@@ -152,7 +152,7 @@ taskCmd
|
|
|
152
152
|
.option("--agent-name <name>", "Agent identity")
|
|
153
153
|
.option("--format <format>", "Output format (json, text)")
|
|
154
154
|
.action(async (id, opts) => {
|
|
155
|
-
const client =
|
|
155
|
+
const client = await createClient();
|
|
156
156
|
const body: Record<string, unknown> = {};
|
|
157
157
|
if (opts.result) body.result = opts.result;
|
|
158
158
|
if (opts.prUrl) body.pr_url = opts.prUrl;
|
|
@@ -171,7 +171,7 @@ agentCmd
|
|
|
171
171
|
.description("List all agents")
|
|
172
172
|
.option("--format <format>", "Output format (json, text)")
|
|
173
173
|
.action(async (opts) => {
|
|
174
|
-
const client = new
|
|
174
|
+
const client = new MachineClient();
|
|
175
175
|
const agents = await client.listAgents();
|
|
176
176
|
const fmt = getFormat(opts.format);
|
|
177
177
|
output(agents, fmt, formatAgentList);
|
|
@@ -188,7 +188,7 @@ boardCmd
|
|
|
188
188
|
.option("--description <desc>", "Board description")
|
|
189
189
|
.option("--format <format>", "Output format (json, text)")
|
|
190
190
|
.action(async (opts) => {
|
|
191
|
-
const client = new
|
|
191
|
+
const client = new MachineClient();
|
|
192
192
|
const board = await client.createBoard({ name: opts.name, description: opts.description });
|
|
193
193
|
const fmt = getFormat(opts.format);
|
|
194
194
|
output(board, fmt, (b) => `Created board ${b.id}: ${b.name}`);
|
|
@@ -199,7 +199,7 @@ boardCmd
|
|
|
199
199
|
.description("List all boards")
|
|
200
200
|
.option("--format <format>", "Output format (json, text)")
|
|
201
201
|
.action(async (opts) => {
|
|
202
|
-
const client = new
|
|
202
|
+
const client = new MachineClient();
|
|
203
203
|
const boards = await client.listBoards();
|
|
204
204
|
const fmt = getFormat(opts.format);
|
|
205
205
|
output(boards, fmt, formatBoardList);
|
|
@@ -211,7 +211,7 @@ boardCmd
|
|
|
211
211
|
.option("--board <name-or-id>", "Board name or ID (uses first if omitted)")
|
|
212
212
|
.option("--format <format>", "Output format (json, text)")
|
|
213
213
|
.action(async (opts) => {
|
|
214
|
-
const client = new
|
|
214
|
+
const client = new MachineClient();
|
|
215
215
|
let boardId: string;
|
|
216
216
|
|
|
217
217
|
if (opts.board) {
|
|
@@ -251,7 +251,7 @@ repoCmd
|
|
|
251
251
|
.requiredOption("--url <url>", "Clone URL")
|
|
252
252
|
.option("--format <format>", "Output format (json, text)")
|
|
253
253
|
.action(async (opts) => {
|
|
254
|
-
const client = new
|
|
254
|
+
const client = new MachineClient();
|
|
255
255
|
const repo = await client.createRepository({ name: opts.name, url: opts.url });
|
|
256
256
|
const fmt = getFormat(opts.format);
|
|
257
257
|
output(repo, fmt, (r) => `Added repository ${r.id}: ${r.name}`);
|
|
@@ -262,7 +262,7 @@ repoCmd
|
|
|
262
262
|
.description("List repositories")
|
|
263
263
|
.option("--format <format>", "Output format (json, text)")
|
|
264
264
|
.action(async (opts) => {
|
|
265
|
-
const client = new
|
|
265
|
+
const client = new MachineClient();
|
|
266
266
|
const repos = await client.listRepositories();
|
|
267
267
|
const fmt = getFormat(opts.format);
|
|
268
268
|
output(repos, fmt, formatRepositoryList);
|
package/src/processManager.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface AgentProcess {
|
|
|
13
13
|
taskId: string;
|
|
14
14
|
sessionId: string;
|
|
15
15
|
process: ChildProcess;
|
|
16
|
-
agentClient
|
|
16
|
+
agentClient: AgentClient;
|
|
17
17
|
timeoutTimer?: ReturnType<typeof setTimeout>;
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -44,7 +44,8 @@ export class ProcessManager {
|
|
|
44
44
|
sessionId: string,
|
|
45
45
|
cwd: string,
|
|
46
46
|
taskContext: string,
|
|
47
|
-
agentClient
|
|
47
|
+
agentClient: AgentClient,
|
|
48
|
+
agentEnv: Record<string, string>,
|
|
48
49
|
): Promise<void> {
|
|
49
50
|
const args = [
|
|
50
51
|
"--print",
|
|
@@ -61,7 +62,7 @@ export class ProcessManager {
|
|
|
61
62
|
proc = spawn(this.agentCli, args, {
|
|
62
63
|
cwd,
|
|
63
64
|
stdio: ["pipe", "pipe", "pipe"],
|
|
64
|
-
env: { ...process.env },
|
|
65
|
+
env: { ...process.env, ...agentEnv },
|
|
65
66
|
});
|
|
66
67
|
} catch (err: any) {
|
|
67
68
|
console.error(`[ERROR] Failed to spawn ${this.agentCli}: ${err.message}`);
|
|
@@ -78,7 +79,6 @@ export class ProcessManager {
|
|
|
78
79
|
const agent: AgentProcess = { taskId, sessionId, process: proc, agentClient };
|
|
79
80
|
this.agents.set(taskId, agent);
|
|
80
81
|
|
|
81
|
-
// Kill agent if it exceeds the task timeout
|
|
82
82
|
if (this.taskTimeoutMs > 0) {
|
|
83
83
|
agent.timeoutTimer = setTimeout(() => {
|
|
84
84
|
console.warn(`[WARN] Agent for task ${taskId} exceeded timeout (${Math.round(this.taskTimeoutMs / 60000)}m), killing`);
|
|
@@ -86,7 +86,6 @@ export class ProcessManager {
|
|
|
86
86
|
}, this.taskTimeoutMs);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
// Send task context as JSON message via stdin, then close
|
|
90
89
|
proc.on("spawn", () => {
|
|
91
90
|
const payload = JSON.stringify({
|
|
92
91
|
type: "user",
|
|
@@ -96,7 +95,6 @@ export class ProcessManager {
|
|
|
96
95
|
proc.stdin?.end();
|
|
97
96
|
});
|
|
98
97
|
|
|
99
|
-
// Parse stdout (stream-json): each line is a JSON event
|
|
100
98
|
let stdoutBuffer = "";
|
|
101
99
|
proc.stdout?.on("data", (chunk: Buffer) => {
|
|
102
100
|
stdoutBuffer += chunk.toString();
|
|
@@ -111,19 +109,15 @@ export class ProcessManager {
|
|
|
111
109
|
}
|
|
112
110
|
});
|
|
113
111
|
|
|
114
|
-
// Capture stderr for crash diagnostics
|
|
115
112
|
let stderrBuffer = "";
|
|
116
113
|
proc.stderr?.on("data", (chunk: Buffer) => {
|
|
117
114
|
stderrBuffer += chunk.toString();
|
|
118
115
|
if (stderrBuffer.length > 10000) stderrBuffer = stderrBuffer.slice(-5000);
|
|
119
116
|
});
|
|
120
117
|
|
|
121
|
-
// Handle exit
|
|
122
118
|
proc.on("close", async (code) => {
|
|
123
|
-
// Clear timeout timer
|
|
124
119
|
if (agent.timeoutTimer) clearTimeout(agent.timeoutTimer);
|
|
125
120
|
|
|
126
|
-
// Flush remaining stdout
|
|
127
121
|
if (stdoutBuffer.trim()) {
|
|
128
122
|
try {
|
|
129
123
|
const event = JSON.parse(stdoutBuffer);
|
|
@@ -169,41 +163,26 @@ export class ProcessManager {
|
|
|
169
163
|
}
|
|
170
164
|
}
|
|
171
165
|
|
|
172
|
-
private handleEvent(taskId: string, sessionId: string, event: any, agentClient
|
|
173
|
-
// Extract text from assistant messages → post as agent chat messages
|
|
166
|
+
private handleEvent(taskId: string, sessionId: string, event: any, agentClient: AgentClient): void {
|
|
174
167
|
if (event.type === "assistant" && Array.isArray(event.message?.content)) {
|
|
175
168
|
for (const block of event.message.content) {
|
|
176
169
|
if (block.type === "text" && block.text) {
|
|
177
|
-
|
|
170
|
+
agentClient.sendMessage(taskId, { agent_id: sessionId, role: "agent", content: block.text })
|
|
171
|
+
.catch((err: any) => console.error(`[ERROR] Failed to send message for task ${taskId}: ${err.message}`));
|
|
178
172
|
}
|
|
179
173
|
}
|
|
180
174
|
}
|
|
181
|
-
// Report usage on result event
|
|
182
175
|
if (event.type === "result") {
|
|
183
176
|
const cost = event.total_cost_usd || 0;
|
|
184
177
|
const usage = event.usage || {};
|
|
185
178
|
console.log(`[INFO] Agent result for task ${taskId}: cost=$${cost.toFixed(4)}`);
|
|
186
|
-
|
|
179
|
+
agentClient.updateAgentUsage(sessionId, {
|
|
187
180
|
input_tokens: usage.input_tokens || 0,
|
|
188
181
|
output_tokens: usage.output_tokens || 0,
|
|
189
182
|
cache_read_tokens: usage.cache_read_input_tokens || 0,
|
|
190
183
|
cache_creation_tokens: usage.cache_creation_input_tokens || 0,
|
|
191
184
|
cost_micro_usd: Math.round(cost * 1_000_000),
|
|
192
|
-
};
|
|
193
|
-
if (agentClient) {
|
|
194
|
-
agentClient.updateAgentUsage(usageData).catch(() => {});
|
|
195
|
-
} else {
|
|
196
|
-
this.client.updateAgentUsage(sessionId, usageData).catch(() => {});
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
private async postMessage(taskId: string, sessionId: string, role: string, content: string, agentClient?: AgentClient): Promise<void> {
|
|
202
|
-
const body = { agent_id: sessionId, role, content };
|
|
203
|
-
if (agentClient) {
|
|
204
|
-
await agentClient.sendMessage(taskId, body);
|
|
205
|
-
} else {
|
|
206
|
-
await this.client.sendMessage(taskId, body);
|
|
185
|
+
}).catch((err: any) => console.error(`[ERROR] Failed to report usage for task ${taskId}: ${err.message}`));
|
|
207
186
|
}
|
|
208
187
|
}
|
|
209
188
|
|