@johpaz/hive-core 0.1.1
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/package.json +43 -0
- package/src/agent/compaction.ts +161 -0
- package/src/agent/context-guard.ts +91 -0
- package/src/agent/context.ts +148 -0
- package/src/agent/ethics.ts +102 -0
- package/src/agent/hooks.ts +166 -0
- package/src/agent/index.ts +67 -0
- package/src/agent/providers/index.ts +278 -0
- package/src/agent/providers.ts +1 -0
- package/src/agent/soul.ts +89 -0
- package/src/agent/stuck-loop.ts +133 -0
- package/src/agent/user.ts +86 -0
- package/src/channels/base.ts +91 -0
- package/src/channels/discord.ts +185 -0
- package/src/channels/index.ts +7 -0
- package/src/channels/manager.ts +204 -0
- package/src/channels/slack.ts +209 -0
- package/src/channels/telegram.ts +177 -0
- package/src/channels/webchat.ts +83 -0
- package/src/channels/whatsapp.ts +305 -0
- package/src/config/index.ts +1 -0
- package/src/config/loader.ts +508 -0
- package/src/gateway/index.ts +5 -0
- package/src/gateway/lane-queue.ts +169 -0
- package/src/gateway/router.ts +124 -0
- package/src/gateway/server.ts +347 -0
- package/src/gateway/session.ts +131 -0
- package/src/gateway/slash-commands.ts +176 -0
- package/src/heartbeat/index.ts +157 -0
- package/src/index.ts +21 -0
- package/src/memory/index.ts +1 -0
- package/src/memory/notes.ts +170 -0
- package/src/multi-agent/bindings.ts +171 -0
- package/src/multi-agent/index.ts +4 -0
- package/src/multi-agent/manager.ts +182 -0
- package/src/multi-agent/sandbox.ts +130 -0
- package/src/multi-agent/subagents.ts +302 -0
- package/src/security/index.ts +187 -0
- package/src/tools/cron.ts +156 -0
- package/src/tools/exec.ts +105 -0
- package/src/tools/index.ts +6 -0
- package/src/tools/memory.ts +176 -0
- package/src/tools/notify.ts +53 -0
- package/src/tools/read.ts +154 -0
- package/src/tools/registry.ts +115 -0
- package/src/tools/web.ts +186 -0
- package/src/utils/crypto.ts +73 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/logger.ts +254 -0
- package/src/utils/retry.ts +70 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { Config, AgentEntry } from "../config/loader.ts";
|
|
4
|
+
import { Agent } from "../agent/index.ts";
|
|
5
|
+
import { ToolRegistry } from "../tools/registry.ts";
|
|
6
|
+
import { logger } from "../utils/logger.ts";
|
|
7
|
+
|
|
8
|
+
export interface AgentInstance {
|
|
9
|
+
id: string;
|
|
10
|
+
agent: Agent;
|
|
11
|
+
toolRegistry: ToolRegistry;
|
|
12
|
+
workspacePath: string;
|
|
13
|
+
isDefault: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class AgentManager {
|
|
17
|
+
private config: Config;
|
|
18
|
+
private agents: Map<string, AgentInstance> = new Map();
|
|
19
|
+
private log = logger.child("agent-manager");
|
|
20
|
+
|
|
21
|
+
constructor(config: Config) {
|
|
22
|
+
this.config = config;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async initialize(): Promise<void> {
|
|
26
|
+
const agentList = this.config.agents?.list ?? [];
|
|
27
|
+
|
|
28
|
+
if (agentList.length === 0) {
|
|
29
|
+
await this.createDefaultAgent();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const entry of agentList) {
|
|
34
|
+
await this.createAgent(entry);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.log.info(`Initialized ${this.agents.size} agent(s)`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private async createDefaultAgent(): Promise<void> {
|
|
41
|
+
const defaultWorkspace = this.expandPath(
|
|
42
|
+
this.config.agent?.baseDir
|
|
43
|
+
? `${this.config.agent.baseDir}/main/workspace`
|
|
44
|
+
: "~/.hive/agents/main/workspace"
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
await this.createAgent({
|
|
48
|
+
id: "main",
|
|
49
|
+
default: true,
|
|
50
|
+
workspace: defaultWorkspace,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private async createAgent(entry: AgentEntry): Promise<void> {
|
|
55
|
+
const workspacePath = this.expandPath(entry.workspace);
|
|
56
|
+
|
|
57
|
+
this.ensureWorkspace(workspacePath);
|
|
58
|
+
|
|
59
|
+
const agent = new Agent({
|
|
60
|
+
agentId: entry.id,
|
|
61
|
+
config: this.config,
|
|
62
|
+
workspacePath,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await agent.initialize();
|
|
66
|
+
|
|
67
|
+
const toolRegistry = new ToolRegistry(this.config);
|
|
68
|
+
|
|
69
|
+
const instance: AgentInstance = {
|
|
70
|
+
id: entry.id,
|
|
71
|
+
agent,
|
|
72
|
+
toolRegistry,
|
|
73
|
+
workspacePath,
|
|
74
|
+
isDefault: entry.default ?? false,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
this.agents.set(entry.id, instance);
|
|
78
|
+
this.log.info(`Created agent: ${entry.id}`, { workspace: workspacePath });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private expandPath(p: string): string {
|
|
82
|
+
if (p.startsWith("~")) {
|
|
83
|
+
return path.join(process.env.HOME ?? "", p.slice(1));
|
|
84
|
+
}
|
|
85
|
+
return p;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private ensureWorkspace(workspacePath: string): void {
|
|
89
|
+
const dirs = [
|
|
90
|
+
workspacePath,
|
|
91
|
+
path.join(workspacePath, "skills"),
|
|
92
|
+
path.join(workspacePath, "memory"),
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
for (const dir of dirs) {
|
|
96
|
+
if (!fs.existsSync(dir)) {
|
|
97
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const soulPath = path.join(workspacePath, "SOUL.md");
|
|
102
|
+
if (!fs.existsSync(soulPath)) {
|
|
103
|
+
fs.writeFileSync(soulPath, `# Agent Identity\n\nThis is agent ${path.basename(workspacePath)}.\n`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const userPath = path.join(workspacePath, "USER.md");
|
|
107
|
+
if (!fs.existsSync(userPath)) {
|
|
108
|
+
fs.writeFileSync(userPath, `# User Profile\n\nName: User\nLanguage: English\n`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
getAgent(agentId: string): AgentInstance | undefined {
|
|
113
|
+
return this.agents.get(agentId);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
getDefaultAgent(): AgentInstance | undefined {
|
|
117
|
+
for (const agent of this.agents.values()) {
|
|
118
|
+
if (agent.isDefault) {
|
|
119
|
+
return agent;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return this.agents.values().next().value;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
listAgents(): Array<{ id: string; isDefault: boolean; workspace: string }> {
|
|
126
|
+
return Array.from(this.agents.values()).map((a) => ({
|
|
127
|
+
id: a.id,
|
|
128
|
+
isDefault: a.isDefault,
|
|
129
|
+
workspace: a.workspacePath,
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
hasAgent(agentId: string): boolean {
|
|
134
|
+
return this.agents.has(agentId);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async createNewAgent(
|
|
138
|
+
agentId: string,
|
|
139
|
+
options: { workspace?: string; isDefault?: boolean } = {}
|
|
140
|
+
): Promise<AgentInstance> {
|
|
141
|
+
if (this.agents.has(agentId)) {
|
|
142
|
+
throw new Error(`Agent already exists: ${agentId}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const baseDir = this.config.agent?.baseDir?.replace(/^~/, process.env.HOME ?? "")
|
|
146
|
+
?? `${process.env.HOME}/.hive/agents`;
|
|
147
|
+
|
|
148
|
+
const workspace = options.workspace ?? `${baseDir}/${agentId}/workspace`;
|
|
149
|
+
|
|
150
|
+
const entry: AgentEntry = {
|
|
151
|
+
id: agentId,
|
|
152
|
+
default: options.isDefault ?? false,
|
|
153
|
+
workspace,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
await this.createAgent(entry);
|
|
157
|
+
|
|
158
|
+
return this.agents.get(agentId)!;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
removeAgent(agentId: string): boolean {
|
|
162
|
+
if (!this.agents.has(agentId)) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (agentId === "main") {
|
|
167
|
+
throw new Error("Cannot remove the main agent");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this.agents.delete(agentId);
|
|
171
|
+
this.log.info(`Removed agent: ${agentId}`);
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
getAgentWorkspace(agentId: string): string | undefined {
|
|
176
|
+
return this.agents.get(agentId)?.workspacePath;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function createAgentManager(config: Config): AgentManager {
|
|
181
|
+
return new AgentManager(config);
|
|
182
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { Config } from "../config/loader.ts";
|
|
2
|
+
|
|
3
|
+
export type SessionType = "main" | "dm" | "group";
|
|
4
|
+
|
|
5
|
+
export interface SandboxPolicy {
|
|
6
|
+
deniedTools: string[];
|
|
7
|
+
restrictedPaths: string[];
|
|
8
|
+
maxExecutionTime: number;
|
|
9
|
+
allowNetwork: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class SandboxManager {
|
|
13
|
+
private config: Config;
|
|
14
|
+
|
|
15
|
+
constructor(config: Config) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getPolicy(sessionType: SessionType): SandboxPolicy {
|
|
20
|
+
const defaultDenied = ["exec", "write", "edit", "apply_patch"];
|
|
21
|
+
|
|
22
|
+
switch (sessionType) {
|
|
23
|
+
case "main":
|
|
24
|
+
return {
|
|
25
|
+
deniedTools: [],
|
|
26
|
+
restrictedPaths: [],
|
|
27
|
+
maxExecutionTime: 60000,
|
|
28
|
+
allowNetwork: true,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
case "dm":
|
|
32
|
+
return {
|
|
33
|
+
deniedTools: this.config.tools?.sandbox?.dm?.deny ?? defaultDenied,
|
|
34
|
+
restrictedPaths: [],
|
|
35
|
+
maxExecutionTime: 30000,
|
|
36
|
+
allowNetwork: true,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
case "group":
|
|
40
|
+
return {
|
|
41
|
+
deniedTools: this.config.tools?.sandbox?.group?.deny ?? defaultDenied,
|
|
42
|
+
restrictedPaths: [],
|
|
43
|
+
maxExecutionTime: 30000,
|
|
44
|
+
allowNetwork: false,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
default:
|
|
48
|
+
return {
|
|
49
|
+
deniedTools: defaultDenied,
|
|
50
|
+
restrictedPaths: [],
|
|
51
|
+
maxExecutionTime: 30000,
|
|
52
|
+
allowNetwork: false,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
isToolAllowed(toolName: string, sessionType: SessionType): boolean {
|
|
58
|
+
const policy = this.getPolicy(sessionType);
|
|
59
|
+
return !policy.deniedTools.includes(toolName);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getSessionTypeFromId(sessionId: string): SessionType {
|
|
63
|
+
const parts = sessionId.split(":");
|
|
64
|
+
|
|
65
|
+
if (parts.length < 4) {
|
|
66
|
+
return "main";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const kind = parts[3];
|
|
70
|
+
|
|
71
|
+
if (kind === "dm") {
|
|
72
|
+
return "dm";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (kind === "group") {
|
|
76
|
+
return "group";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return "main";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
filterAllowedTools(
|
|
83
|
+
tools: string[],
|
|
84
|
+
sessionType: SessionType
|
|
85
|
+
): string[] {
|
|
86
|
+
const policy = this.getPolicy(sessionType);
|
|
87
|
+
return tools.filter((tool) => !policy.deniedTools.includes(tool));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
validatePath(
|
|
91
|
+
path: string,
|
|
92
|
+
sessionType: SessionType,
|
|
93
|
+
workspacePath: string
|
|
94
|
+
): { allowed: boolean; reason?: string } {
|
|
95
|
+
const policy = this.getPolicy(sessionType);
|
|
96
|
+
|
|
97
|
+
if (policy.restrictedPaths.length === 0) {
|
|
98
|
+
const expandedWorkspace = workspacePath.replace(/^~/, process.env.HOME ?? "");
|
|
99
|
+
|
|
100
|
+
if (!path.startsWith(expandedWorkspace)) {
|
|
101
|
+
return {
|
|
102
|
+
allowed: false,
|
|
103
|
+
reason: "Path must be within workspace"
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { allowed: true };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
createSandboxedRegistry(
|
|
112
|
+
baseRegistry: Map<string, any>,
|
|
113
|
+
sessionType: SessionType
|
|
114
|
+
): Map<string, any> {
|
|
115
|
+
const policy = this.getPolicy(sessionType);
|
|
116
|
+
const sandboxed = new Map<string, any>();
|
|
117
|
+
|
|
118
|
+
for (const [name, tool] of baseRegistry) {
|
|
119
|
+
if (!policy.deniedTools.includes(name)) {
|
|
120
|
+
sandboxed.set(name, tool);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return sandboxed;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function createSandboxManager(config: Config): SandboxManager {
|
|
129
|
+
return new SandboxManager(config);
|
|
130
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import type { Tool, ToolResult } from "../tools/registry.ts";
|
|
2
|
+
import type { Config } from "../config/loader.ts";
|
|
3
|
+
import { Agent } from "../agent/index.ts";
|
|
4
|
+
import { logger } from "../utils/logger.ts";
|
|
5
|
+
import { generateId } from "../utils/crypto.ts";
|
|
6
|
+
|
|
7
|
+
export interface SubAgentTask {
|
|
8
|
+
id: string;
|
|
9
|
+
parentSessionId: string;
|
|
10
|
+
task: string;
|
|
11
|
+
delegatedTools: string[];
|
|
12
|
+
timeout: number;
|
|
13
|
+
status: "pending" | "running" | "completed" | "failed" | "timeout";
|
|
14
|
+
result?: unknown;
|
|
15
|
+
error?: string;
|
|
16
|
+
startedAt?: Date;
|
|
17
|
+
completedAt?: Date;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SubAgentOptions {
|
|
21
|
+
task: string;
|
|
22
|
+
delegatedTools?: string[];
|
|
23
|
+
timeout?: number;
|
|
24
|
+
maxOutputLength?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class SubAgentManager {
|
|
28
|
+
private config: Config;
|
|
29
|
+
private log = logger.child("subagents");
|
|
30
|
+
private activeTasks: Map<string, SubAgentTask> = new Map();
|
|
31
|
+
|
|
32
|
+
constructor(config: Config) {
|
|
33
|
+
this.config = config;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async spawn(
|
|
37
|
+
parentAgentId: string,
|
|
38
|
+
parentSessionId: string,
|
|
39
|
+
options: SubAgentOptions
|
|
40
|
+
): Promise<SubAgentTask> {
|
|
41
|
+
const taskId = `subagent-${generateId()}`;
|
|
42
|
+
|
|
43
|
+
const task: SubAgentTask = {
|
|
44
|
+
id: taskId,
|
|
45
|
+
parentSessionId,
|
|
46
|
+
task: options.task,
|
|
47
|
+
delegatedTools: options.delegatedTools ?? ["read", "web_search"],
|
|
48
|
+
timeout: options.timeout ?? 60000,
|
|
49
|
+
status: "pending",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
this.activeTasks.set(taskId, task);
|
|
53
|
+
|
|
54
|
+
this.log.info(`Spawning sub-agent`, {
|
|
55
|
+
taskId,
|
|
56
|
+
parentAgentId,
|
|
57
|
+
delegatedTools: task.delegatedTools
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
task.status = "running";
|
|
61
|
+
task.startedAt = new Date();
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const result = await this.executeWithTimeout(
|
|
65
|
+
task,
|
|
66
|
+
parentAgentId,
|
|
67
|
+
options.maxOutputLength ?? 5000
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
task.result = result;
|
|
71
|
+
task.status = "completed";
|
|
72
|
+
task.completedAt = new Date();
|
|
73
|
+
|
|
74
|
+
this.log.info(`Sub-agent completed`, { taskId });
|
|
75
|
+
} catch (error) {
|
|
76
|
+
const err = error as Error;
|
|
77
|
+
|
|
78
|
+
if (err.message === "TIMEOUT") {
|
|
79
|
+
task.status = "timeout";
|
|
80
|
+
task.error = "Sub-agent exceeded timeout";
|
|
81
|
+
} else {
|
|
82
|
+
task.status = "failed";
|
|
83
|
+
task.error = err.message;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
task.completedAt = new Date();
|
|
87
|
+
this.log.error(`Sub-agent failed`, { taskId, error: task.error });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return task;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private async executeWithTimeout(
|
|
94
|
+
task: SubAgentTask,
|
|
95
|
+
_parentAgentId: string,
|
|
96
|
+
maxOutputLength: number
|
|
97
|
+
): Promise<unknown> {
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
const timeoutId = setTimeout(() => {
|
|
100
|
+
reject(new Error("TIMEOUT"));
|
|
101
|
+
}, task.timeout);
|
|
102
|
+
|
|
103
|
+
this.executeTask(task, _parentAgentId, maxOutputLength)
|
|
104
|
+
.then((result) => {
|
|
105
|
+
clearTimeout(timeoutId);
|
|
106
|
+
resolve(result);
|
|
107
|
+
})
|
|
108
|
+
.catch((error) => {
|
|
109
|
+
clearTimeout(timeoutId);
|
|
110
|
+
reject(error);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private async executeTask(
|
|
116
|
+
task: SubAgentTask,
|
|
117
|
+
_parentAgentId: string,
|
|
118
|
+
maxOutputLength: number
|
|
119
|
+
): Promise<unknown> {
|
|
120
|
+
const agent = new Agent({
|
|
121
|
+
agentId: `sub-${task.id}`,
|
|
122
|
+
config: this.config,
|
|
123
|
+
workspacePath: this.getTempWorkspace(task.id),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await agent.initialize();
|
|
127
|
+
|
|
128
|
+
const response = await agent.buildPrompt();
|
|
129
|
+
|
|
130
|
+
let result = response;
|
|
131
|
+
|
|
132
|
+
if (typeof result === "string" && result.length > maxOutputLength) {
|
|
133
|
+
result = this.truncateOutput(result, maxOutputLength);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private getTempWorkspace(taskId: string): string {
|
|
140
|
+
const baseDir = this.config.agent?.baseDir?.replace(/^~/, process.env.HOME ?? "")
|
|
141
|
+
?? `${process.env.HOME}/.hive/agents`;
|
|
142
|
+
|
|
143
|
+
return `${baseDir}/subagents/${taskId}/workspace`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private truncateOutput(output: string, maxLength: number): string {
|
|
147
|
+
if (output.length <= maxLength) {
|
|
148
|
+
return output;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const half = Math.floor(maxLength / 2);
|
|
152
|
+
return output.slice(0, half) +
|
|
153
|
+
`\n... [truncated ${output.length - maxLength} characters] ...\n` +
|
|
154
|
+
output.slice(-half);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
getTask(taskId: string): SubAgentTask | undefined {
|
|
158
|
+
return this.activeTasks.get(taskId);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
cancelTask(taskId: string): boolean {
|
|
162
|
+
const task = this.activeTasks.get(taskId);
|
|
163
|
+
|
|
164
|
+
if (!task) return false;
|
|
165
|
+
if (task.status !== "running" && task.status !== "pending") return false;
|
|
166
|
+
|
|
167
|
+
task.status = "failed";
|
|
168
|
+
task.error = "Cancelled by user";
|
|
169
|
+
task.completedAt = new Date();
|
|
170
|
+
|
|
171
|
+
this.log.info(`Cancelled sub-agent task`, { taskId });
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
listActiveTasks(): SubAgentTask[] {
|
|
176
|
+
return Array.from(this.activeTasks.values()).filter(
|
|
177
|
+
(t) => t.status === "running" || t.status === "pending"
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
cleanup(olderThanMs: number = 3600000): number {
|
|
182
|
+
const now = Date.now();
|
|
183
|
+
let cleaned = 0;
|
|
184
|
+
|
|
185
|
+
for (const [id, task] of this.activeTasks) {
|
|
186
|
+
const completedAt = task.completedAt?.getTime() ?? 0;
|
|
187
|
+
|
|
188
|
+
if (task.status !== "running" && task.status !== "pending") {
|
|
189
|
+
if (now - completedAt > olderThanMs) {
|
|
190
|
+
this.activeTasks.delete(id);
|
|
191
|
+
cleaned++;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (cleaned > 0) {
|
|
197
|
+
this.log.debug(`Cleaned up ${cleaned} completed sub-agent tasks`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return cleaned;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function createSubAgentManager(config: Config): SubAgentManager {
|
|
205
|
+
return new SubAgentManager(config);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export const sessionsSpawnTool = (
|
|
209
|
+
subAgentManager: SubAgentManager,
|
|
210
|
+
agentId: string,
|
|
211
|
+
sessionId: string
|
|
212
|
+
): Tool => ({
|
|
213
|
+
name: "sessions_spawn",
|
|
214
|
+
description: "Spawn a sub-agent to complete a specific task",
|
|
215
|
+
parameters: {
|
|
216
|
+
type: "object",
|
|
217
|
+
properties: {
|
|
218
|
+
task: {
|
|
219
|
+
type: "string",
|
|
220
|
+
description: "The task for the sub-agent to complete",
|
|
221
|
+
},
|
|
222
|
+
tools: {
|
|
223
|
+
type: "array",
|
|
224
|
+
items: { type: "string" },
|
|
225
|
+
description: "Tools to delegate to the sub-agent (default: read, web_search)",
|
|
226
|
+
},
|
|
227
|
+
timeout: {
|
|
228
|
+
type: "number",
|
|
229
|
+
description: "Timeout in milliseconds (default: 60000)",
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
required: ["task"],
|
|
233
|
+
},
|
|
234
|
+
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
|
235
|
+
try {
|
|
236
|
+
const result = await subAgentManager.spawn(agentId, sessionId, {
|
|
237
|
+
task: params.task as string,
|
|
238
|
+
delegatedTools: params.tools as string[] | undefined,
|
|
239
|
+
timeout: params.timeout as number | undefined,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
success: result.status === "completed",
|
|
244
|
+
result: result.result,
|
|
245
|
+
error: result.error,
|
|
246
|
+
};
|
|
247
|
+
} catch (error) {
|
|
248
|
+
return {
|
|
249
|
+
success: false,
|
|
250
|
+
error: (error as Error).message,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
export const messageAgentTool = (
|
|
257
|
+
agentManager: any
|
|
258
|
+
): Tool => ({
|
|
259
|
+
name: "message_agent",
|
|
260
|
+
description: "Send a message to another agent",
|
|
261
|
+
parameters: {
|
|
262
|
+
type: "object",
|
|
263
|
+
properties: {
|
|
264
|
+
agentId: {
|
|
265
|
+
type: "string",
|
|
266
|
+
description: "The ID of the target agent",
|
|
267
|
+
},
|
|
268
|
+
message: {
|
|
269
|
+
type: "string",
|
|
270
|
+
description: "The message to send",
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
required: ["agentId", "message"],
|
|
274
|
+
},
|
|
275
|
+
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
|
276
|
+
const targetAgentId = params.agentId as string;
|
|
277
|
+
const message = params.message as string;
|
|
278
|
+
|
|
279
|
+
if (!agentManager.hasAgent(targetAgentId)) {
|
|
280
|
+
return {
|
|
281
|
+
success: false,
|
|
282
|
+
error: `Agent not found: ${targetAgentId}`,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
return {
|
|
288
|
+
success: true,
|
|
289
|
+
result: {
|
|
290
|
+
delivered: true,
|
|
291
|
+
targetAgent: targetAgentId,
|
|
292
|
+
messageLength: message.length,
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
} catch (error) {
|
|
296
|
+
return {
|
|
297
|
+
success: false,
|
|
298
|
+
error: (error as Error).message,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
});
|