homarus 0.1.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/LICENSE +21 -0
- package/README.md +114 -0
- package/dist/agent-manager.d.ts +35 -0
- package/dist/agent-manager.js +127 -0
- package/dist/agent-worker.d.ts +2 -0
- package/dist/agent-worker.js +141 -0
- package/dist/agent.d.ts +33 -0
- package/dist/agent.js +197 -0
- package/dist/browser-manager.d.ts +33 -0
- package/dist/browser-manager.js +170 -0
- package/dist/channel-adapter.d.ts +29 -0
- package/dist/channel-adapter.js +94 -0
- package/dist/channel-manager.d.ts +17 -0
- package/dist/channel-manager.js +84 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +212 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.js +185 -0
- package/dist/embedding-provider.d.ts +35 -0
- package/dist/embedding-provider.js +103 -0
- package/dist/event-bus.d.ts +18 -0
- package/dist/event-bus.js +46 -0
- package/dist/event-queue.d.ts +18 -0
- package/dist/event-queue.js +77 -0
- package/dist/execution-strategy.d.ts +26 -0
- package/dist/execution-strategy.js +20 -0
- package/dist/homarus.d.ts +36 -0
- package/dist/homarus.js +308 -0
- package/dist/http-api.d.ts +16 -0
- package/dist/http-api.js +82 -0
- package/dist/identity-manager.d.ts +28 -0
- package/dist/identity-manager.js +123 -0
- package/dist/memory-index.d.ts +52 -0
- package/dist/memory-index.js +286 -0
- package/dist/model-provider.d.ts +33 -0
- package/dist/model-provider.js +255 -0
- package/dist/model-router.d.ts +32 -0
- package/dist/model-router.js +148 -0
- package/dist/setup-wizard.d.ts +28 -0
- package/dist/setup-wizard.js +240 -0
- package/dist/skill-manager.d.ts +26 -0
- package/dist/skill-manager.js +171 -0
- package/dist/skill-transport.d.ts +51 -0
- package/dist/skill-transport.js +116 -0
- package/dist/skill.d.ts +22 -0
- package/dist/skill.js +118 -0
- package/dist/subprocess-strategy.d.ts +54 -0
- package/dist/subprocess-strategy.js +106 -0
- package/dist/telegram-adapter.d.ts +34 -0
- package/dist/telegram-adapter.js +165 -0
- package/dist/timer-service.d.ts +30 -0
- package/dist/timer-service.js +142 -0
- package/dist/tool-registry.d.ts +29 -0
- package/dist/tool-registry.js +100 -0
- package/dist/tools/bash.d.ts +3 -0
- package/dist/tools/bash.js +48 -0
- package/dist/tools/browser.d.ts +4 -0
- package/dist/tools/browser.js +47 -0
- package/dist/tools/edit.d.ts +3 -0
- package/dist/tools/edit.js +48 -0
- package/dist/tools/git.d.ts +3 -0
- package/dist/tools/git.js +109 -0
- package/dist/tools/glob.d.ts +3 -0
- package/dist/tools/glob.js +86 -0
- package/dist/tools/grep.d.ts +3 -0
- package/dist/tools/grep.js +169 -0
- package/dist/tools/index.d.ts +6 -0
- package/dist/tools/index.js +46 -0
- package/dist/tools/lsp.d.ts +3 -0
- package/dist/tools/lsp.js +216 -0
- package/dist/tools/memory.d.ts +4 -0
- package/dist/tools/memory.js +64 -0
- package/dist/tools/read.d.ts +3 -0
- package/dist/tools/read.js +49 -0
- package/dist/tools/web-fetch.d.ts +3 -0
- package/dist/tools/web-fetch.js +51 -0
- package/dist/tools/web-search.d.ts +3 -0
- package/dist/tools/web-search.js +73 -0
- package/dist/tools/write.d.ts +3 -0
- package/dist/tools/write.js +31 -0
- package/dist/types.d.ts +240 -0
- package/dist/types.js +14 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Max Ross
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# HomarUS
|
|
2
|
+
|
|
3
|
+
> **Why "HomarUS"?** *Homarus* is the genus of true clawed lobsters — the evolved successor to the claw. The **US** isn't just taxonomy. It's the whole point: human and AI, working together. There is no artificial intelligence without *us*.
|
|
4
|
+
|
|
5
|
+
Event-driven AI agent coordinator. Receives events from channels, skills, timers, and webhooks — spawns parallel agents to handle the work.
|
|
6
|
+
|
|
7
|
+
Model-agnostic. Skill-based. Built for people who want to run their own AI infrastructure.
|
|
8
|
+
|
|
9
|
+
## What it does
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
Channels (Telegram, Discord, CLI, ...)
|
|
13
|
+
Skills (web UIs, scripts, services) → Event Loop → Agents (parallel)
|
|
14
|
+
Timers (cron, intervals, one-shots) ↓
|
|
15
|
+
Webhooks (HTTP callbacks) Tools, Memory, Models
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The loop is a **scheduler and router**, not an AI agent. It receives events, decides what work needs to happen, spawns agents to do it, and handles the results. Agents run in parallel with configurable concurrency, backpressure, and failover.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g homarus
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or use without installing:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx homarus init
|
|
30
|
+
npx homarus start
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
homarus init # Interactive wizard: pick provider, enter API key, configure Telegram
|
|
37
|
+
homarus start # Start the event loop
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The setup wizard walks you through provider selection (Anthropic, OpenAI, OpenRouter, or Ollama), API key entry, default model choice, and optional Telegram bot setup. Your config is ready to use immediately — no manual JSON editing required.
|
|
41
|
+
|
|
42
|
+
Use `--no-wizard` to skip the wizard and generate a default config for manual editing.
|
|
43
|
+
|
|
44
|
+
See the **[Setup & Usage Guide](docs/guide.md)** for detailed configuration and more.
|
|
45
|
+
|
|
46
|
+
## Architecture
|
|
47
|
+
|
|
48
|
+
- **Homarus** — central event loop, single-threaded coordinator
|
|
49
|
+
- **EventBus / EventQueue** — typed pub/sub with priority ordering and backpressure
|
|
50
|
+
- **AgentManager / Agent** — spawns and tracks parallel AI agents
|
|
51
|
+
- **ModelRouter / ModelProvider** — model-agnostic with failover chains (Anthropic, OpenAI, OpenRouter, Ollama, any OpenAI-compatible endpoint)
|
|
52
|
+
- **SkillManager / Skill** — open plugin system via HTTP, stdio, or in-process transports
|
|
53
|
+
- **ChannelManager / ChannelAdapter** — normalized message ingestion from any platform
|
|
54
|
+
- **MemoryIndex** — vector + full-text hybrid search for long-term memory
|
|
55
|
+
- **IdentityManager** — layered soul/user/overlay identity system
|
|
56
|
+
- **TimerService** — cron expressions, intervals, one-shots via croner
|
|
57
|
+
- **HttpApi** — REST API for status, skill callbacks, and external integrations
|
|
58
|
+
- **BrowserManager** — optional Playwright-based headless browser (lazy-loaded, requires `npm install playwright`)
|
|
59
|
+
- **Config** — JSON config with JSON Schema validation and hot reload
|
|
60
|
+
|
|
61
|
+
## CLI
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
homarus start [config] Start the event loop (foreground)
|
|
65
|
+
homarus init [--no-wizard] Interactive setup wizard (or defaults)
|
|
66
|
+
homarus status [port] Show status of running instance
|
|
67
|
+
homarus config [config] Validate config file
|
|
68
|
+
homarus skills List loaded skills
|
|
69
|
+
homarus install-daemon Install systemd or launchd service
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Config
|
|
73
|
+
|
|
74
|
+
Primary: `~/.homarus/config.json`
|
|
75
|
+
Per-project override: `./homarus.json`
|
|
76
|
+
|
|
77
|
+
Skills go in `~/.homarus/skills/` or `./skills/`.
|
|
78
|
+
Identity files in `~/.homarus/identity/`.
|
|
79
|
+
Memory index at `~/.homarus/memory/`.
|
|
80
|
+
|
|
81
|
+
## Built-in tools
|
|
82
|
+
|
|
83
|
+
Agents get access to: `bash`, `read`, `write`, `edit`, `glob`, `grep`, `git`, `web_fetch`, `web_search`, `lsp`, `memory_search`, `memory_get`, `memory_store`, and optionally `browser`.
|
|
84
|
+
|
|
85
|
+
Browser support requires Playwright as an optional dependency:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npm install playwright # only needed if browser.enabled is set in config
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Requirements
|
|
92
|
+
|
|
93
|
+
- Node.js >= 22
|
|
94
|
+
- Unix-like system (Linux, macOS)
|
|
95
|
+
|
|
96
|
+
## Built with mini-spec
|
|
97
|
+
|
|
98
|
+
HomarUS was designed and implemented using [mini-spec](https://github.com/zot/mini-spec), an 8-phase methodology for AI-assisted software development. Every source file traces back through the full chain: reference materials → natural language specs → requirements → CRC cards → sequence diagrams → code with traceability comments.
|
|
99
|
+
|
|
100
|
+
The `refs/`, `specs/`, and `design/` directories are the living design artifacts, not just documentation.
|
|
101
|
+
|
|
102
|
+
## Status
|
|
103
|
+
|
|
104
|
+
Core architecture implemented (19 source files, 70 requirements, 20 CRC cards, 7 sequence diagrams, 37 tests passing across 5 test suites). Built-in tool suite complete. Currently pre-release.
|
|
105
|
+
|
|
106
|
+
## Roadmap
|
|
107
|
+
|
|
108
|
+
- [ ] npm publish + SEA binaries + Homebrew tap
|
|
109
|
+
- [ ] OAuth support for Google/Gemini (the only major provider with third-party OAuth)
|
|
110
|
+
- [x] ~~`homarus auth` onboarding command~~ (shipped as interactive `homarus init` wizard)
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AgentConfig, Event, ExecutionStrategyType, Logger } from "./types.js";
|
|
2
|
+
import { Agent } from "./agent.js";
|
|
3
|
+
import type { ModelRouter } from "./model-router.js";
|
|
4
|
+
import type { ToolRegistry } from "./tool-registry.js";
|
|
5
|
+
import type { IdentityManager } from "./identity-manager.js";
|
|
6
|
+
export declare class AgentManager {
|
|
7
|
+
private agents;
|
|
8
|
+
private maxConcurrent;
|
|
9
|
+
private defaultTimeout;
|
|
10
|
+
private defaultMaxTurns;
|
|
11
|
+
private defaultExecutionStrategy;
|
|
12
|
+
private modelRouter;
|
|
13
|
+
private toolRegistry;
|
|
14
|
+
private identityManager;
|
|
15
|
+
private logger;
|
|
16
|
+
private emitFn;
|
|
17
|
+
private strategies;
|
|
18
|
+
constructor(logger: Logger, modelRouter: ModelRouter, toolRegistry: ToolRegistry, identityManager: IdentityManager, options?: {
|
|
19
|
+
maxConcurrent?: number;
|
|
20
|
+
defaultTimeout?: number;
|
|
21
|
+
defaultMaxTurns?: number;
|
|
22
|
+
defaultExecutionStrategy?: ExecutionStrategyType;
|
|
23
|
+
});
|
|
24
|
+
setEmitter(fn: (event: Event) => void): void;
|
|
25
|
+
spawn(config: AgentConfig): Promise<string>;
|
|
26
|
+
cancel(agentId: string): void;
|
|
27
|
+
cancelAll(): void;
|
|
28
|
+
getAgent(agentId: string): Agent | undefined;
|
|
29
|
+
getActive(): Agent[];
|
|
30
|
+
activeCount(): number;
|
|
31
|
+
canSpawn(): boolean;
|
|
32
|
+
waitForAll(timeout?: number): Promise<void>;
|
|
33
|
+
private onAgentComplete;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=agent-manager.d.ts.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { EmbeddedStrategy } from "./execution-strategy.js";
|
|
2
|
+
import { SubprocessStrategy } from "./subprocess-strategy.js";
|
|
3
|
+
export class AgentManager {
|
|
4
|
+
agents = new Map();
|
|
5
|
+
maxConcurrent;
|
|
6
|
+
defaultTimeout;
|
|
7
|
+
defaultMaxTurns;
|
|
8
|
+
defaultExecutionStrategy;
|
|
9
|
+
modelRouter;
|
|
10
|
+
toolRegistry;
|
|
11
|
+
identityManager;
|
|
12
|
+
logger;
|
|
13
|
+
emitFn = null;
|
|
14
|
+
strategies = {
|
|
15
|
+
embedded: new EmbeddedStrategy(),
|
|
16
|
+
subprocess: new SubprocessStrategy(),
|
|
17
|
+
};
|
|
18
|
+
constructor(logger, modelRouter, toolRegistry, identityManager, options) {
|
|
19
|
+
this.logger = logger;
|
|
20
|
+
this.modelRouter = modelRouter;
|
|
21
|
+
this.toolRegistry = toolRegistry;
|
|
22
|
+
this.identityManager = identityManager;
|
|
23
|
+
this.maxConcurrent = options?.maxConcurrent ?? 5;
|
|
24
|
+
this.defaultTimeout = options?.defaultTimeout ?? 300_000;
|
|
25
|
+
this.defaultMaxTurns = options?.defaultMaxTurns ?? 20;
|
|
26
|
+
this.defaultExecutionStrategy = options?.defaultExecutionStrategy ?? "embedded";
|
|
27
|
+
}
|
|
28
|
+
setEmitter(fn) {
|
|
29
|
+
this.emitFn = fn;
|
|
30
|
+
}
|
|
31
|
+
// CRC: crc-AgentManager.md — spawn()
|
|
32
|
+
async spawn(config) {
|
|
33
|
+
if (!this.canSpawn()) {
|
|
34
|
+
throw new Error(`Max concurrent agents reached (${this.maxConcurrent})`);
|
|
35
|
+
}
|
|
36
|
+
const agentConfig = {
|
|
37
|
+
...config,
|
|
38
|
+
timeout: config.timeout ?? this.defaultTimeout,
|
|
39
|
+
maxTurns: config.maxTurns ?? this.defaultMaxTurns,
|
|
40
|
+
};
|
|
41
|
+
const strategyType = config.executionStrategy ?? this.defaultExecutionStrategy;
|
|
42
|
+
const strategy = this.strategies[strategyType];
|
|
43
|
+
const handle = strategy.execute(agentConfig, {
|
|
44
|
+
modelRouter: this.modelRouter,
|
|
45
|
+
toolRegistry: this.toolRegistry,
|
|
46
|
+
identityManager: this.identityManager,
|
|
47
|
+
logger: this.logger,
|
|
48
|
+
});
|
|
49
|
+
if (this.emitFn) {
|
|
50
|
+
handle.onEvent(this.emitFn);
|
|
51
|
+
}
|
|
52
|
+
const activeAgent = {
|
|
53
|
+
handle,
|
|
54
|
+
cancel: () => handle.cancel(),
|
|
55
|
+
agent: handle.agent,
|
|
56
|
+
};
|
|
57
|
+
this.agents.set(handle.agentId, activeAgent);
|
|
58
|
+
this.logger.info("Spawning agent", {
|
|
59
|
+
id: handle.agentId,
|
|
60
|
+
model: config.model,
|
|
61
|
+
strategy: strategyType,
|
|
62
|
+
});
|
|
63
|
+
// Run async — don't await, let it complete in background
|
|
64
|
+
handle.result
|
|
65
|
+
.then((result) => this.onAgentComplete(handle.agentId, result))
|
|
66
|
+
.catch((err) => {
|
|
67
|
+
this.logger.error("Agent run failed", { id: handle.agentId, error: String(err) });
|
|
68
|
+
this.agents.delete(handle.agentId);
|
|
69
|
+
});
|
|
70
|
+
return handle.agentId;
|
|
71
|
+
}
|
|
72
|
+
// CRC: crc-AgentManager.md — cancel()
|
|
73
|
+
cancel(agentId) {
|
|
74
|
+
const active = this.agents.get(agentId);
|
|
75
|
+
if (!active)
|
|
76
|
+
return;
|
|
77
|
+
active.cancel();
|
|
78
|
+
this.logger.info("Agent cancel requested", { id: agentId });
|
|
79
|
+
}
|
|
80
|
+
// CRC: crc-AgentManager.md — cancelAll()
|
|
81
|
+
cancelAll() {
|
|
82
|
+
for (const active of this.agents.values()) {
|
|
83
|
+
active.cancel();
|
|
84
|
+
}
|
|
85
|
+
this.logger.info("All agents cancel requested", { count: this.agents.size });
|
|
86
|
+
}
|
|
87
|
+
// CRC: crc-AgentManager.md — getAgent() — returns undefined for subprocess agents
|
|
88
|
+
getAgent(agentId) {
|
|
89
|
+
return this.agents.get(agentId)?.agent;
|
|
90
|
+
}
|
|
91
|
+
// CRC: crc-AgentManager.md — getActive()
|
|
92
|
+
getActive() {
|
|
93
|
+
return [...this.agents.values()]
|
|
94
|
+
.filter((a) => a.agent?.getState() === "running")
|
|
95
|
+
.map((a) => a.agent);
|
|
96
|
+
}
|
|
97
|
+
// CRC: crc-AgentManager.md — activeCount()
|
|
98
|
+
activeCount() {
|
|
99
|
+
return this.agents.size;
|
|
100
|
+
}
|
|
101
|
+
// CRC: crc-AgentManager.md — canSpawn()
|
|
102
|
+
canSpawn() {
|
|
103
|
+
return this.activeCount() < this.maxConcurrent;
|
|
104
|
+
}
|
|
105
|
+
// CRC: crc-AgentManager.md — waitForAll()
|
|
106
|
+
async waitForAll(timeout = 30_000) {
|
|
107
|
+
const start = Date.now();
|
|
108
|
+
while (this.agents.size > 0) {
|
|
109
|
+
if (Date.now() - start > timeout) {
|
|
110
|
+
this.logger.warn("Wait for agents timed out, force cancelling", { remaining: this.agents.size });
|
|
111
|
+
this.cancelAll();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// CRC: crc-AgentManager.md — onAgentComplete()
|
|
118
|
+
onAgentComplete(agentId, result) {
|
|
119
|
+
this.agents.delete(agentId);
|
|
120
|
+
this.logger.info("Agent complete", {
|
|
121
|
+
id: agentId,
|
|
122
|
+
toolCalls: result.toolCalls.length,
|
|
123
|
+
tokens: result.usage.inputTokens + result.usage.outputTokens,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=agent-manager.js.map
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// CRC: design.md O4 — Agent subprocess worker entry point
|
|
2
|
+
// This file runs as a child process forked by SubprocessStrategy.
|
|
3
|
+
// It receives an init message, reconstructs deps, runs an Agent, and reports back via IPC.
|
|
4
|
+
import { v4 as uuid } from "uuid";
|
|
5
|
+
import { Agent } from "./agent.js";
|
|
6
|
+
import { ModelRouter } from "./model-router.js";
|
|
7
|
+
import { OpenAICompatibleProvider } from "./model-provider.js";
|
|
8
|
+
// Minimal logger that writes to stderr (stdout is reserved for IPC)
|
|
9
|
+
const logger = {
|
|
10
|
+
debug: (msg, meta) => console.error(`[worker:debug] ${msg}`, meta ?? ""),
|
|
11
|
+
info: (msg, meta) => console.error(`[worker:info] ${msg}`, meta ?? ""),
|
|
12
|
+
warn: (msg, meta) => console.error(`[worker:warn] ${msg}`, meta ?? ""),
|
|
13
|
+
error: (msg, meta) => console.error(`[worker:error] ${msg}`, meta ?? ""),
|
|
14
|
+
};
|
|
15
|
+
// Pending tool requests awaiting parent response
|
|
16
|
+
const pendingToolCalls = new Map();
|
|
17
|
+
let agent = null;
|
|
18
|
+
function send(msg) {
|
|
19
|
+
process.send(msg);
|
|
20
|
+
}
|
|
21
|
+
// Build a proxy ToolRegistry that delegates execution to the parent via IPC
|
|
22
|
+
function buildProxyToolRegistry(toolSchemas) {
|
|
23
|
+
const proxyTools = new Map();
|
|
24
|
+
for (const schema of toolSchemas) {
|
|
25
|
+
proxyTools.set(schema.name, {
|
|
26
|
+
name: schema.name,
|
|
27
|
+
description: schema.description,
|
|
28
|
+
parameters: schema.parameters,
|
|
29
|
+
source: "subprocess-proxy",
|
|
30
|
+
execute: async (params, context) => {
|
|
31
|
+
const callId = uuid();
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
pendingToolCalls.set(callId, { resolve, reject });
|
|
34
|
+
send({
|
|
35
|
+
type: "tool_request",
|
|
36
|
+
callId,
|
|
37
|
+
toolName: schema.name,
|
|
38
|
+
params,
|
|
39
|
+
agentId: context.agentId,
|
|
40
|
+
sandbox: context.sandbox,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
// Return an object that satisfies the ToolRegistry interface used by Agent
|
|
47
|
+
return {
|
|
48
|
+
getForAgent(_allowedTools) {
|
|
49
|
+
return [...proxyTools.values()];
|
|
50
|
+
},
|
|
51
|
+
execute: async (name, params, context) => {
|
|
52
|
+
const tool = proxyTools.get(name);
|
|
53
|
+
if (!tool)
|
|
54
|
+
return { output: "", error: `Unknown tool: ${name}` };
|
|
55
|
+
return tool.execute(params, context);
|
|
56
|
+
},
|
|
57
|
+
// Stubs for interface compliance — not used by Agent.run()
|
|
58
|
+
register() { },
|
|
59
|
+
unregister() { },
|
|
60
|
+
get(name) { return proxyTools.get(name); },
|
|
61
|
+
getAll() { return [...proxyTools.values()]; },
|
|
62
|
+
registerGroup() { },
|
|
63
|
+
resolveGroup() { return []; },
|
|
64
|
+
addPolicy() { },
|
|
65
|
+
checkPolicy() { return true; },
|
|
66
|
+
toSchemas() {
|
|
67
|
+
return [...proxyTools.values()].map((t) => ({
|
|
68
|
+
name: t.name, description: t.description, parameters: t.parameters,
|
|
69
|
+
}));
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Build a minimal IdentityManager that returns the pre-built system prompt
|
|
74
|
+
function buildProxyIdentityManager(systemPrompt) {
|
|
75
|
+
return {
|
|
76
|
+
buildSystemPrompt: () => systemPrompt,
|
|
77
|
+
load() { },
|
|
78
|
+
reload() { },
|
|
79
|
+
getSoul() { return ""; },
|
|
80
|
+
getUser() { return ""; },
|
|
81
|
+
getOverlay() { return undefined; },
|
|
82
|
+
getWorkspaceFile() { return undefined; },
|
|
83
|
+
listOverlays() { return []; },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// Build a ModelRouter from serialized provider configs
|
|
87
|
+
function buildModelRouter(providers, defaultModel, aliases, fallbackChain) {
|
|
88
|
+
const router = new ModelRouter(logger, defaultModel);
|
|
89
|
+
for (const pc of providers) {
|
|
90
|
+
const provider = new OpenAICompatibleProvider(pc.id, pc.baseUrl, { apiKey: pc.apiKey }, logger);
|
|
91
|
+
router.registerProvider(provider);
|
|
92
|
+
}
|
|
93
|
+
router.setAliases(aliases);
|
|
94
|
+
router.setFallbackChain(fallbackChain);
|
|
95
|
+
return router;
|
|
96
|
+
}
|
|
97
|
+
async function handleInit(msg) {
|
|
98
|
+
const modelRouter = buildModelRouter(msg.providerConfigs, msg.defaultModel, msg.aliases, msg.fallbackChain);
|
|
99
|
+
const toolRegistry = buildProxyToolRegistry(msg.toolSchemas);
|
|
100
|
+
const identityManager = buildProxyIdentityManager(msg.systemPrompt);
|
|
101
|
+
agent = new Agent(msg.agentConfig, modelRouter, toolRegistry, identityManager, logger);
|
|
102
|
+
agent.setEmitter((event) => {
|
|
103
|
+
send({ type: "event", event });
|
|
104
|
+
});
|
|
105
|
+
try {
|
|
106
|
+
const result = await agent.run();
|
|
107
|
+
send({ type: "result", result });
|
|
108
|
+
process.exit(0);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
send({ type: "error", error: String(err.message ?? err) });
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
process.on("message", (msg) => {
|
|
116
|
+
switch (msg.type) {
|
|
117
|
+
case "init":
|
|
118
|
+
handleInit(msg).catch((err) => {
|
|
119
|
+
send({ type: "error", error: String(err) });
|
|
120
|
+
process.exit(1);
|
|
121
|
+
});
|
|
122
|
+
break;
|
|
123
|
+
case "tool_result": {
|
|
124
|
+
const pending = pendingToolCalls.get(msg.callId);
|
|
125
|
+
if (pending) {
|
|
126
|
+
pendingToolCalls.delete(msg.callId);
|
|
127
|
+
pending.resolve(msg.result);
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case "cancel":
|
|
132
|
+
agent?.cancel();
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
// Clean exit on disconnect
|
|
137
|
+
process.on("disconnect", () => {
|
|
138
|
+
agent?.cancel();
|
|
139
|
+
process.exit(0);
|
|
140
|
+
});
|
|
141
|
+
//# sourceMappingURL=agent-worker.js.map
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AgentConfig, AgentState, AgentResult, Event, Logger } from "./types.js";
|
|
2
|
+
import type { ModelRouter } from "./model-router.js";
|
|
3
|
+
import type { ToolRegistry } from "./tool-registry.js";
|
|
4
|
+
import type { IdentityManager } from "./identity-manager.js";
|
|
5
|
+
export declare class Agent {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
private state;
|
|
8
|
+
private config;
|
|
9
|
+
private turns;
|
|
10
|
+
private startTime;
|
|
11
|
+
private result;
|
|
12
|
+
private cancelled;
|
|
13
|
+
private totalUsage;
|
|
14
|
+
private toolCallLog;
|
|
15
|
+
private modelRouter;
|
|
16
|
+
private toolRegistry;
|
|
17
|
+
private identityManager;
|
|
18
|
+
private logger;
|
|
19
|
+
private emitFn;
|
|
20
|
+
constructor(config: AgentConfig, modelRouter: ModelRouter, toolRegistry: ToolRegistry, identityManager: IdentityManager, logger: Logger);
|
|
21
|
+
setEmitter(fn: (event: Event) => void): void;
|
|
22
|
+
getState(): AgentState;
|
|
23
|
+
getResult(): AgentResult | null;
|
|
24
|
+
run(): Promise<AgentResult>;
|
|
25
|
+
cancel(): void;
|
|
26
|
+
isTimedOut(): boolean;
|
|
27
|
+
isOverTurns(): boolean;
|
|
28
|
+
private buildResult;
|
|
29
|
+
private emitProgress;
|
|
30
|
+
private emitResult;
|
|
31
|
+
private emitError;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=agent.d.ts.map
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// CRC: crc-Agent.md | Seq: seq-agent-execution.md
|
|
2
|
+
import { v4 as uuid } from "uuid";
|
|
3
|
+
export class Agent {
|
|
4
|
+
id;
|
|
5
|
+
state = "pending";
|
|
6
|
+
config;
|
|
7
|
+
turns = 0;
|
|
8
|
+
startTime = 0;
|
|
9
|
+
result = null;
|
|
10
|
+
cancelled = false;
|
|
11
|
+
totalUsage = { inputTokens: 0, outputTokens: 0 };
|
|
12
|
+
toolCallLog = [];
|
|
13
|
+
modelRouter;
|
|
14
|
+
toolRegistry;
|
|
15
|
+
identityManager;
|
|
16
|
+
logger;
|
|
17
|
+
emitFn = null;
|
|
18
|
+
constructor(config, modelRouter, toolRegistry, identityManager, logger) {
|
|
19
|
+
this.id = uuid();
|
|
20
|
+
this.config = config;
|
|
21
|
+
this.modelRouter = modelRouter;
|
|
22
|
+
this.toolRegistry = toolRegistry;
|
|
23
|
+
this.identityManager = identityManager;
|
|
24
|
+
this.logger = logger;
|
|
25
|
+
}
|
|
26
|
+
setEmitter(fn) {
|
|
27
|
+
this.emitFn = fn;
|
|
28
|
+
}
|
|
29
|
+
getState() {
|
|
30
|
+
return this.state;
|
|
31
|
+
}
|
|
32
|
+
getResult() {
|
|
33
|
+
return this.result;
|
|
34
|
+
}
|
|
35
|
+
// CRC: crc-Agent.md — run()
|
|
36
|
+
async run() {
|
|
37
|
+
this.state = "running";
|
|
38
|
+
this.startTime = Date.now();
|
|
39
|
+
this.emitProgress("started");
|
|
40
|
+
const maxTurns = this.config.maxTurns ?? 20;
|
|
41
|
+
const systemPrompt = this.identityManager.buildSystemPrompt({
|
|
42
|
+
channel: this.config.channel,
|
|
43
|
+
taskOverlay: this.config.taskOverlay,
|
|
44
|
+
taskPrompt: this.config.systemPrompt,
|
|
45
|
+
});
|
|
46
|
+
const messages = [
|
|
47
|
+
{ role: "system", content: systemPrompt },
|
|
48
|
+
{ role: "user", content: this.config.prompt },
|
|
49
|
+
];
|
|
50
|
+
const tools = this.toolRegistry.getForAgent(this.config.tools);
|
|
51
|
+
const toolSchemas = tools.length > 0
|
|
52
|
+
? tools.map((t) => ({ name: t.name, description: t.description, parameters: t.parameters }))
|
|
53
|
+
: undefined;
|
|
54
|
+
try {
|
|
55
|
+
while (this.turns < maxTurns && !this.cancelled) {
|
|
56
|
+
if (this.isTimedOut()) {
|
|
57
|
+
this.state = "failed";
|
|
58
|
+
this.emitError("Agent timed out");
|
|
59
|
+
return this.buildResult("Agent timed out after " + (this.config.timeout ?? 300_000) + "ms");
|
|
60
|
+
}
|
|
61
|
+
this.turns++;
|
|
62
|
+
let fullContent = "";
|
|
63
|
+
const pendingToolCalls = [];
|
|
64
|
+
// Stream inference
|
|
65
|
+
for await (const chunk of this.modelRouter.chat({
|
|
66
|
+
model: this.modelRouter.resolve(this.config.model),
|
|
67
|
+
messages,
|
|
68
|
+
tools: toolSchemas,
|
|
69
|
+
maxTokens: 4096,
|
|
70
|
+
})) {
|
|
71
|
+
if (chunk.content)
|
|
72
|
+
fullContent += chunk.content;
|
|
73
|
+
if (chunk.toolCalls)
|
|
74
|
+
pendingToolCalls.push(...chunk.toolCalls);
|
|
75
|
+
if (chunk.usage) {
|
|
76
|
+
this.totalUsage.inputTokens += chunk.usage.inputTokens;
|
|
77
|
+
this.totalUsage.outputTokens += chunk.usage.outputTokens;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// No tool calls — agent is done
|
|
81
|
+
if (pendingToolCalls.length === 0) {
|
|
82
|
+
this.state = "complete";
|
|
83
|
+
this.result = this.buildResult(fullContent);
|
|
84
|
+
this.emitResult();
|
|
85
|
+
return this.result;
|
|
86
|
+
}
|
|
87
|
+
// Record assistant message with tool calls
|
|
88
|
+
messages.push({
|
|
89
|
+
role: "assistant",
|
|
90
|
+
content: fullContent,
|
|
91
|
+
toolCalls: pendingToolCalls,
|
|
92
|
+
});
|
|
93
|
+
// Execute tool calls
|
|
94
|
+
for (const tc of pendingToolCalls) {
|
|
95
|
+
if (this.cancelled)
|
|
96
|
+
break;
|
|
97
|
+
const start = Date.now();
|
|
98
|
+
let params;
|
|
99
|
+
try {
|
|
100
|
+
params = JSON.parse(tc.arguments);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
params = tc.arguments;
|
|
104
|
+
}
|
|
105
|
+
const result = await this.toolRegistry.execute(tc.name, params, {
|
|
106
|
+
agentId: this.id,
|
|
107
|
+
sandbox: this.config.sandbox ?? false,
|
|
108
|
+
workingDir: process.cwd(),
|
|
109
|
+
});
|
|
110
|
+
this.toolCallLog.push({
|
|
111
|
+
tool: tc.name,
|
|
112
|
+
params,
|
|
113
|
+
result: result.error ?? result.output,
|
|
114
|
+
durationMs: Date.now() - start,
|
|
115
|
+
});
|
|
116
|
+
messages.push({
|
|
117
|
+
role: "tool",
|
|
118
|
+
content: result.error ?? result.output,
|
|
119
|
+
toolCallId: tc.id,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
this.emitProgress(`turn ${this.turns}/${maxTurns}`);
|
|
123
|
+
}
|
|
124
|
+
// Exhausted turns
|
|
125
|
+
if (this.cancelled) {
|
|
126
|
+
this.state = "cancelled";
|
|
127
|
+
this.result = this.buildResult("Agent cancelled");
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
this.state = "complete";
|
|
131
|
+
this.result = this.buildResult("Agent reached max turns");
|
|
132
|
+
}
|
|
133
|
+
this.emitResult();
|
|
134
|
+
return this.result;
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
this.state = "failed";
|
|
138
|
+
const errorMsg = String(err.message ?? err);
|
|
139
|
+
this.emitError(errorMsg);
|
|
140
|
+
return this.buildResult(errorMsg);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// CRC: crc-Agent.md — cancel()
|
|
144
|
+
cancel() {
|
|
145
|
+
this.cancelled = true;
|
|
146
|
+
this.logger.info("Agent cancellation requested", { id: this.id });
|
|
147
|
+
}
|
|
148
|
+
// CRC: crc-Agent.md — isTimedOut()
|
|
149
|
+
isTimedOut() {
|
|
150
|
+
if (!this.config.timeout)
|
|
151
|
+
return false;
|
|
152
|
+
return Date.now() - this.startTime > this.config.timeout;
|
|
153
|
+
}
|
|
154
|
+
// CRC: crc-Agent.md — isOverTurns()
|
|
155
|
+
isOverTurns() {
|
|
156
|
+
return this.turns >= (this.config.maxTurns ?? 20);
|
|
157
|
+
}
|
|
158
|
+
buildResult(output) {
|
|
159
|
+
return {
|
|
160
|
+
output,
|
|
161
|
+
toolCalls: [...this.toolCallLog],
|
|
162
|
+
usage: { ...this.totalUsage },
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
// CRC: crc-Agent.md — emitProgress()
|
|
166
|
+
emitProgress(progress) {
|
|
167
|
+
this.emitFn?.({
|
|
168
|
+
id: uuid(),
|
|
169
|
+
type: "agent_progress",
|
|
170
|
+
source: `agent:${this.id}`,
|
|
171
|
+
timestamp: Date.now(),
|
|
172
|
+
payload: { agentId: this.id, progress, turns: this.turns },
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
// CRC: crc-Agent.md — emitResult()
|
|
176
|
+
emitResult() {
|
|
177
|
+
this.emitFn?.({
|
|
178
|
+
id: uuid(),
|
|
179
|
+
type: "agent_complete",
|
|
180
|
+
source: `agent:${this.id}`,
|
|
181
|
+
timestamp: Date.now(),
|
|
182
|
+
replyTo: this.config.replyTo,
|
|
183
|
+
payload: { agentId: this.id, result: this.result, state: this.state },
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
// CRC: crc-Agent.md — emitError()
|
|
187
|
+
emitError(error) {
|
|
188
|
+
this.emitFn?.({
|
|
189
|
+
id: uuid(),
|
|
190
|
+
type: "agent_error",
|
|
191
|
+
source: `agent:${this.id}`,
|
|
192
|
+
timestamp: Date.now(),
|
|
193
|
+
payload: { agentId: this.id, error },
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=agent.js.map
|