agents-dojo 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.
@@ -0,0 +1,19 @@
1
+ export function createMonitorBus() {
2
+ const subs = new Set();
3
+ return {
4
+ subscribe(fn) {
5
+ subs.add(fn);
6
+ return () => subs.delete(fn);
7
+ },
8
+ emit(event) {
9
+ for (const fn of subs) {
10
+ try {
11
+ fn(event);
12
+ }
13
+ catch (err) {
14
+ console.error('[monitor-bus] subscriber threw:', err);
15
+ }
16
+ }
17
+ },
18
+ };
19
+ }
@@ -0,0 +1,25 @@
1
+ import { WebSocketServer, WebSocket } from 'ws';
2
+ import type { Server } from 'http';
3
+ import type { AgentRegistry } from './agent-registry.js';
4
+ import type { MonitorBus } from './monitor-bus.js';
5
+ export interface MonitorWsOptions {
6
+ server: Server;
7
+ bus: MonitorBus;
8
+ path: string;
9
+ registry: AgentRegistry;
10
+ }
11
+ export type MonitorCommand = {
12
+ type: 'reload';
13
+ agentId: string;
14
+ } | {
15
+ type: 'set_position';
16
+ agentId: string;
17
+ x: number;
18
+ y: number;
19
+ };
20
+ export type MonitorCommandHandler = (cmd: MonitorCommand) => void | Promise<void>;
21
+ export interface MonitorWsHandle {
22
+ wss: WebSocketServer;
23
+ clients: Set<WebSocket>;
24
+ }
25
+ export declare function createMonitorWs(opts: MonitorWsOptions): MonitorWsHandle;
@@ -0,0 +1,61 @@
1
+ // src/monitor-ws.ts
2
+ import { WebSocketServer, WebSocket } from 'ws';
3
+ export function createMonitorWs(opts) {
4
+ const clients = new Set();
5
+ const wss = new WebSocketServer({ server: opts.server, path: opts.path });
6
+ function handleCommand(cmd) {
7
+ if (cmd.type === 'reload') {
8
+ const result = opts.registry.reload(cmd.agentId);
9
+ // WS protocol is fire-and-forget for commands; on failure, log so the
10
+ // operator sees it in the server stderr rather than silently dropping.
11
+ if (!result.ok) {
12
+ console.error(`[monitor-ws] reload failed for "${cmd.agentId}": ${result.code} — ${result.message}`);
13
+ }
14
+ return;
15
+ }
16
+ if (cmd.type === 'set_position') {
17
+ const agent = opts.registry.get(cmd.agentId);
18
+ if (!agent)
19
+ return;
20
+ agent.manifest.monitor = { ...agent.manifest.monitor, position: { x: cmd.x, y: cmd.y } };
21
+ }
22
+ }
23
+ wss.on('connection', (ws) => {
24
+ clients.add(ws);
25
+ // Replay current agent list so late-connecting monitors see all loaded agents
26
+ for (const id of opts.registry.list()) {
27
+ const agent = opts.registry.get(id);
28
+ if (agent && ws.readyState === WebSocket.OPEN) {
29
+ ws.send(JSON.stringify({
30
+ type: 'agent_loaded',
31
+ agentId: id,
32
+ position: agent.manifest.monitor?.position,
33
+ }));
34
+ }
35
+ }
36
+ ws.on('close', () => clients.delete(ws));
37
+ ws.on('error', () => clients.delete(ws));
38
+ ws.on('message', (data) => {
39
+ try {
40
+ const parsed = JSON.parse(data.toString());
41
+ if (parsed &&
42
+ typeof parsed === 'object' &&
43
+ (parsed.type === 'reload' || parsed.type === 'set_position')) {
44
+ handleCommand(parsed);
45
+ }
46
+ }
47
+ catch {
48
+ // ignore malformed JSON
49
+ }
50
+ });
51
+ });
52
+ opts.bus.subscribe((event) => {
53
+ const msg = JSON.stringify(event);
54
+ for (const ws of clients) {
55
+ if (ws.readyState === WebSocket.OPEN) {
56
+ ws.send(msg);
57
+ }
58
+ }
59
+ });
60
+ return { wss, clients };
61
+ }
@@ -0,0 +1,31 @@
1
+ export type A2APart = {
2
+ kind: 'text';
3
+ text: string;
4
+ } | {
5
+ kind: 'file';
6
+ file: {
7
+ uri: string;
8
+ mimeType: string;
9
+ name: string;
10
+ };
11
+ } | {
12
+ kind: 'data';
13
+ data: unknown;
14
+ };
15
+ export type AnthropicContentBlock = {
16
+ type: 'text';
17
+ text: string;
18
+ } | {
19
+ type: 'image';
20
+ source: {
21
+ type: 'url';
22
+ url: string;
23
+ };
24
+ } | {
25
+ type: 'document';
26
+ source: {
27
+ type: 'url';
28
+ url: string;
29
+ };
30
+ };
31
+ export declare function a2aToContentBlocks(parts: A2APart[]): AnthropicContentBlock[];
@@ -0,0 +1,21 @@
1
+ const IMAGE_MIME_PREFIX = 'image/';
2
+ const PDF_MIME = 'application/pdf';
3
+ export function a2aToContentBlocks(parts) {
4
+ return parts.map(part => {
5
+ switch (part.kind) {
6
+ case 'text':
7
+ return { type: 'text', text: part.text };
8
+ case 'file': {
9
+ if (part.file.mimeType.startsWith(IMAGE_MIME_PREFIX)) {
10
+ return { type: 'image', source: { type: 'url', url: part.file.uri } };
11
+ }
12
+ if (part.file.mimeType === PDF_MIME) {
13
+ return { type: 'document', source: { type: 'url', url: part.file.uri } };
14
+ }
15
+ throw new Error(`Unsupported file mime type: ${part.file.mimeType}`);
16
+ }
17
+ case 'data':
18
+ return { type: 'text', text: JSON.stringify(part.data, null, 2) };
19
+ }
20
+ });
21
+ }
@@ -0,0 +1,3 @@
1
+ import { Router } from 'express';
2
+ import type { AgentRegistry } from './agent-registry.js';
3
+ export declare function createReloadApi(registry: AgentRegistry): Router;
@@ -0,0 +1,51 @@
1
+ // src/reload-api.ts
2
+ import { Router } from 'express';
3
+ export function createReloadApi(registry) {
4
+ const router = Router();
5
+ router.get('/', (_req, res) => {
6
+ const items = registry.list().map(id => {
7
+ const a = registry.get(id);
8
+ return {
9
+ id,
10
+ name: a?.manifest.name,
11
+ version: a?.manifest.version,
12
+ position: a?.manifest.monitor?.position,
13
+ };
14
+ });
15
+ res.json({ agents: items });
16
+ });
17
+ router.post('/:id/reload', (req, res) => {
18
+ const { id } = req.params;
19
+ const result = registry.reload(id);
20
+ if (!result.ok) {
21
+ // Discriminate on the error code from the registry rather than parsing
22
+ // the message string.
23
+ if (result.code === 'not_found') {
24
+ res.status(404).json({ error: result.message });
25
+ }
26
+ else {
27
+ // load_error: the on-disk manifest is invalid or a referenced file
28
+ // is missing. Surface the upstream error to the caller.
29
+ res.status(500).json({ error: result.message });
30
+ }
31
+ return;
32
+ }
33
+ res.json({ ok: true, agent: { id, name: result.agent.manifest.name } });
34
+ });
35
+ router.post('/:id/position', (req, res) => {
36
+ const { id } = req.params;
37
+ const { x, y } = req.body ?? {};
38
+ if (typeof x !== 'number' || typeof y !== 'number') {
39
+ res.status(400).json({ error: 'x and y must be numbers' });
40
+ return;
41
+ }
42
+ const agent = registry.get(id);
43
+ if (!agent) {
44
+ res.status(404).json({ error: `Agent "${id}" not found` });
45
+ return;
46
+ }
47
+ agent.manifest.monitor = { ...agent.manifest.monitor, position: { x, y } };
48
+ res.json({ ok: true });
49
+ });
50
+ return router;
51
+ }
@@ -0,0 +1,16 @@
1
+ import { createServer as createHttpServer } from 'http';
2
+ import { AgentRegistry } from './agent-registry.js';
3
+ import { createMonitorBus } from './monitor-bus.js';
4
+ export interface CreateServerOptions {
5
+ agentsDir: string;
6
+ port?: number;
7
+ monitorPort?: number;
8
+ singleAgent?: string;
9
+ }
10
+ export interface DojoServer {
11
+ registry: AgentRegistry;
12
+ bus: ReturnType<typeof createMonitorBus>;
13
+ httpServer: ReturnType<typeof createHttpServer>;
14
+ close: () => Promise<void>;
15
+ }
16
+ export declare function createServer(opts: CreateServerOptions): Promise<DojoServer>;
package/dist/server.js ADDED
@@ -0,0 +1,39 @@
1
+ import { createServer as createHttpServer } from 'http';
2
+ import { AgentRegistry } from './agent-registry.js';
3
+ import { createA2AServer } from './a2a-server.js';
4
+ import { createReloadApi } from './reload-api.js';
5
+ import { createMetricsRouter, setAgentsLoaded } from './metrics.js';
6
+ import { createMonitorBus } from './monitor-bus.js';
7
+ import { createMonitorWs } from './monitor-ws.js';
8
+ export async function createServer(opts) {
9
+ // Create bus FIRST so handlers can reference it
10
+ const bus = createMonitorBus();
11
+ const registry = new AgentRegistry(opts.agentsDir);
12
+ registry.load();
13
+ setAgentsLoaded(registry.list().length);
14
+ registry.on('agent_loaded', (e) => {
15
+ setAgentsLoaded(registry.list().length);
16
+ bus.emit({ type: 'agent_loaded', ...e });
17
+ });
18
+ registry.on('agent_reloaded', (e) => bus.emit({ type: 'agent_reloaded', ...e }));
19
+ const a2a = createA2AServer({ registry, singleAgent: opts.singleAgent, port: opts.port, monitorBus: bus });
20
+ const app = a2a.app;
21
+ app.use('/agents', createReloadApi(registry));
22
+ app.use('/metrics', createMetricsRouter());
23
+ const httpServer = createHttpServer(app);
24
+ if (opts.monitorPort) {
25
+ // Bind monitor on a separate server
26
+ const monitorHttp = createHttpServer();
27
+ createMonitorWs({ server: monitorHttp, bus, path: '/monitor', registry });
28
+ await new Promise((r) => monitorHttp.listen(opts.monitorPort, r));
29
+ }
30
+ await new Promise((r) => httpServer.listen(opts.port ?? 41241, r));
31
+ return {
32
+ registry,
33
+ bus,
34
+ httpServer,
35
+ close: () => new Promise((resolve) => {
36
+ httpServer.close(() => resolve());
37
+ }),
38
+ };
39
+ }
@@ -0,0 +1,106 @@
1
+ export interface A2ACardConfig {
2
+ provider?: {
3
+ organization: string;
4
+ url: string;
5
+ };
6
+ capabilities?: {
7
+ streaming?: boolean;
8
+ pushNotifications?: boolean;
9
+ stateTransitionHistory?: boolean;
10
+ };
11
+ defaultInputModes?: Array<'text' | 'file' | 'data'>;
12
+ defaultOutputModes?: Array<'text' | 'file' | 'task-status'>;
13
+ skills?: Array<{
14
+ id: string;
15
+ name: string;
16
+ description: string;
17
+ tags?: string[];
18
+ examples?: string[];
19
+ inputModes?: string[];
20
+ outputModes?: string[];
21
+ }>;
22
+ }
23
+ export interface MonitorConfig {
24
+ position?: {
25
+ x: number;
26
+ y: number;
27
+ };
28
+ sprite?: string;
29
+ }
30
+ export interface LoadedAgent {
31
+ manifest: AgentManifest;
32
+ agentDir: string;
33
+ fixedContextContent: string;
34
+ mcpServersPath?: string;
35
+ hooksPath?: string;
36
+ settingsPath?: string;
37
+ agentsPath?: string;
38
+ sandboxPath?: string;
39
+ }
40
+ export type AgentManifest = {
41
+ id: string;
42
+ name: string;
43
+ description: string;
44
+ version: string;
45
+ fixedContext: string;
46
+ systemPromptAppend?: string;
47
+ model?: string;
48
+ fallbackModel?: string;
49
+ executable?: 'bun' | 'deno' | 'node';
50
+ executableArgs?: string[];
51
+ pathToClaudeCodeExecutable?: string;
52
+ extraArgs?: Record<string, string | null>;
53
+ env?: Record<string, string | undefined>;
54
+ configDir?: string;
55
+ tools?: string[] | {
56
+ type: 'preset';
57
+ preset: 'claude_code';
58
+ };
59
+ allowedTools?: string[];
60
+ disallowedTools?: string[];
61
+ toolAliases?: Record<string, string>;
62
+ permissionMode?: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' | 'dontAsk' | 'auto';
63
+ planModeInstructions?: string;
64
+ allowDangerouslySkipPermissions?: boolean;
65
+ permissionPromptToolName?: string;
66
+ strictMcpConfig?: boolean;
67
+ additionalDirectories?: string[];
68
+ hooks?: string;
69
+ includeHookEvents?: boolean;
70
+ mcpServers?: string;
71
+ plugins?: Array<{
72
+ type: 'local';
73
+ path: string;
74
+ }>;
75
+ skills?: string[] | 'all';
76
+ settingSources?: Array<'user' | 'project' | 'local'>;
77
+ settings?: string;
78
+ agent?: string;
79
+ agents?: string;
80
+ agentProgressSummaries?: boolean;
81
+ forwardSubagentText?: boolean;
82
+ thinking?: {
83
+ type: 'adaptive';
84
+ } | {
85
+ type: 'enabled';
86
+ budgetTokens: number;
87
+ } | {
88
+ type: 'disabled';
89
+ };
90
+ effort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max';
91
+ maxThinkingTokens?: number;
92
+ maxTurns?: number;
93
+ maxBudgetUsd?: number;
94
+ taskBudget?: {
95
+ tokens: number;
96
+ };
97
+ betas?: string[];
98
+ outputFormat?: {
99
+ type: 'json_schema';
100
+ schema: object;
101
+ };
102
+ sandbox?: string;
103
+ forkSession?: boolean;
104
+ a2aCard?: A2ACardConfig;
105
+ monitor?: MonitorConfig;
106
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "agents-dojo",
3
+ "version": "0.1.0",
4
+ "description": "A2A-compatible Agent framework built on Claude Code SDK",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "agents-dojo": "dist/cli.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "prepublishOnly": "npm run build",
18
+ "test": "vitest run",
19
+ "test:watch": "vitest",
20
+ "test:e2e": "vitest run tests/e2e",
21
+ "start": "node dist/cli.js",
22
+ "dev": "tsc --watch"
23
+ },
24
+ "dependencies": {
25
+ "@a2a-js/sdk": "^0.3.13",
26
+ "@anthropic-ai/claude-agent-sdk": "^0.3.145",
27
+ "express": "^5.2.1",
28
+ "strip-json-comments": "^5.0.3",
29
+ "uuid": "^14.0.0",
30
+ "ws": "^8.18.0",
31
+ "zod": "^4.4.3"
32
+ },
33
+ "devDependencies": {
34
+ "@types/express": "^5.0.6",
35
+ "@types/node": "^25.9.1",
36
+ "@types/uuid": "^10.0.0",
37
+ "@types/ws": "^8.5.10",
38
+ "typescript": "^5.6.0",
39
+ "vitest": "^2.1.0"
40
+ },
41
+ "engines": {
42
+ "node": ">=18"
43
+ }
44
+ }