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.
- package/README.md +47 -0
- package/dist/a2a-server.d.ts +14 -0
- package/dist/a2a-server.js +44 -0
- package/dist/agent-executor.d.ts +15 -0
- package/dist/agent-executor.js +147 -0
- package/dist/agent-loader.d.ts +8 -0
- package/dist/agent-loader.js +89 -0
- package/dist/agent-registry.d.ts +20 -0
- package/dist/agent-registry.js +39 -0
- package/dist/claude-bridge.d.ts +11 -0
- package/dist/claude-bridge.js +124 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +134 -0
- package/dist/context-manager.d.ts +5 -0
- package/dist/context-manager.js +30 -0
- package/dist/event-translator.d.ts +16 -0
- package/dist/event-translator.js +142 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +7 -0
- package/dist/manifest-schema.d.ts +163 -0
- package/dist/manifest-schema.js +94 -0
- package/dist/metrics.d.ts +5 -0
- package/dist/metrics.js +29 -0
- package/dist/monitor-bus.d.ts +40 -0
- package/dist/monitor-bus.js +19 -0
- package/dist/monitor-ws.d.ts +25 -0
- package/dist/monitor-ws.js +61 -0
- package/dist/part-mapper.d.ts +31 -0
- package/dist/part-mapper.js +21 -0
- package/dist/reload-api.d.ts +3 -0
- package/dist/reload-api.js +51 -0
- package/dist/server.d.ts +16 -0
- package/dist/server.js +39 -0
- package/dist/types.d.ts +106 -0
- package/dist/types.js +1 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# AgentsDojo
|
|
2
|
+
|
|
3
|
+
A Claude Code SDK-based Agent framework with A2A protocol support and a pixel-art Monitor GUI.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g agents-dojo
|
|
9
|
+
|
|
10
|
+
# Create a project with a sample agent
|
|
11
|
+
mkdir my-project && cd my-project
|
|
12
|
+
agents-dojo init
|
|
13
|
+
|
|
14
|
+
# Start the server
|
|
15
|
+
agents-dojo
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
To add your own agent, create a directory under `agents/` with two files:
|
|
19
|
+
|
|
20
|
+
- `manifest.jsonc` — agent identity, model, A2A card
|
|
21
|
+
- `context.md` — system prompt (role, rules, output format)
|
|
22
|
+
|
|
23
|
+
See `agents/_template_echo/` for a complete manifest reference.
|
|
24
|
+
|
|
25
|
+
## Monitor GUI
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
agents-dojo --monitor-port=41242
|
|
29
|
+
cd monitor && npm install && npm run dev
|
|
30
|
+
# Open http://localhost:5173
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Architecture
|
|
34
|
+
|
|
35
|
+
- Framework (`src/`): A2A server, Claude Code SDK bridge, monitor event bus
|
|
36
|
+
- Monitor (`monitor/`): React + Pixi.js GUI
|
|
37
|
+
|
|
38
|
+
## Testing
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm test # unit + component + integration
|
|
42
|
+
npm run test:e2e # E2E (requires ANTHROPIC_API_KEY)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## License
|
|
46
|
+
|
|
47
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Express } from 'express';
|
|
2
|
+
import type { AgentRegistry } from './agent-registry.js';
|
|
3
|
+
import type { MonitorBus } from './monitor-bus.js';
|
|
4
|
+
export interface A2AServerOptions {
|
|
5
|
+
registry: AgentRegistry;
|
|
6
|
+
singleAgent?: string;
|
|
7
|
+
port?: number;
|
|
8
|
+
monitorBus?: MonitorBus;
|
|
9
|
+
}
|
|
10
|
+
export interface A2AServerHandle {
|
|
11
|
+
app: Express;
|
|
12
|
+
close: () => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export declare function createA2AServer(opts: A2AServerOptions): A2AServerHandle;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// src/a2a-server.ts
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import { DefaultRequestHandler, InMemoryTaskStore, } from '@a2a-js/sdk/server';
|
|
4
|
+
import { agentCardHandler, jsonRpcHandler, UserBuilder, } from '@a2a-js/sdk/server/express';
|
|
5
|
+
import { AGENT_CARD_PATH } from '@a2a-js/sdk';
|
|
6
|
+
import { DojoAgentExecutor } from './agent-executor.js';
|
|
7
|
+
export function createA2AServer(opts) {
|
|
8
|
+
const app = express();
|
|
9
|
+
app.use(express.json());
|
|
10
|
+
const agents = opts.singleAgent
|
|
11
|
+
? [opts.registry.get(opts.singleAgent)].filter((a) => a !== undefined)
|
|
12
|
+
: opts.registry.list().map((id) => opts.registry.get(id)).filter((a) => a !== undefined);
|
|
13
|
+
for (const loaded of agents) {
|
|
14
|
+
const card = {
|
|
15
|
+
name: loaded.manifest.name,
|
|
16
|
+
description: loaded.manifest.description,
|
|
17
|
+
url: `http://localhost:${opts.port ?? 41241}/a2a/${loaded.manifest.id}`,
|
|
18
|
+
version: loaded.manifest.version,
|
|
19
|
+
protocolVersion: '1.0',
|
|
20
|
+
capabilities: {
|
|
21
|
+
streaming: true,
|
|
22
|
+
pushNotifications: false,
|
|
23
|
+
stateTransitionHistory: true,
|
|
24
|
+
},
|
|
25
|
+
defaultInputModes: ['text'],
|
|
26
|
+
defaultOutputModes: ['text'],
|
|
27
|
+
skills: (loaded.manifest.a2aCard?.skills ?? []),
|
|
28
|
+
};
|
|
29
|
+
const taskStore = new InMemoryTaskStore();
|
|
30
|
+
const executor = new DojoAgentExecutor(loaded, { monitorBus: opts.monitorBus });
|
|
31
|
+
const requestHandler = new DefaultRequestHandler(card, taskStore, executor);
|
|
32
|
+
const userBuilder = UserBuilder.noAuthentication;
|
|
33
|
+
app.use(`/a2a/${loaded.manifest.id}`, jsonRpcHandler({ requestHandler, userBuilder }));
|
|
34
|
+
app.use(`/a2a/${loaded.manifest.id}${AGENT_CARD_PATH}`, agentCardHandler({ agentCardProvider: requestHandler }));
|
|
35
|
+
app.use(`/a2a/${loaded.manifest.id}/card`, agentCardHandler({ agentCardProvider: requestHandler }));
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
app,
|
|
39
|
+
close: () => new Promise((resolve) => {
|
|
40
|
+
// No actual server here; createServer() handles listening
|
|
41
|
+
resolve();
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AgentExecutor, RequestContext, ExecutionEventBus } from '@a2a-js/sdk/server';
|
|
2
|
+
import type { MonitorBus } from './monitor-bus.js';
|
|
3
|
+
import type { LoadedAgent } from './types.js';
|
|
4
|
+
export interface AgentExecutorOptions {
|
|
5
|
+
monitorBus?: MonitorBus;
|
|
6
|
+
}
|
|
7
|
+
export declare class DojoAgentExecutor implements AgentExecutor {
|
|
8
|
+
private agent;
|
|
9
|
+
private options;
|
|
10
|
+
private controllers;
|
|
11
|
+
private contextIds;
|
|
12
|
+
constructor(agent: LoadedAgent, options?: AgentExecutorOptions);
|
|
13
|
+
cancelTask: (taskId: string, eventBus: ExecutionEventBus) => Promise<void>;
|
|
14
|
+
execute(requestContext: RequestContext, eventBus: ExecutionEventBus): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// src/agent-executor.ts
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
import { a2aToContentBlocks } from './part-mapper.js';
|
|
4
|
+
import { runClaude } from './claude-bridge.js';
|
|
5
|
+
import { createTranslator } from './event-translator.js';
|
|
6
|
+
import { recordTaskStart, recordTaskEnd } from './metrics.js';
|
|
7
|
+
export class DojoAgentExecutor {
|
|
8
|
+
agent;
|
|
9
|
+
options;
|
|
10
|
+
controllers = new Map();
|
|
11
|
+
contextIds = new Map();
|
|
12
|
+
constructor(agent, options = {}) {
|
|
13
|
+
this.agent = agent;
|
|
14
|
+
this.options = options;
|
|
15
|
+
}
|
|
16
|
+
cancelTask = async (taskId, eventBus) => {
|
|
17
|
+
const controller = this.controllers.get(taskId);
|
|
18
|
+
if (!controller)
|
|
19
|
+
return;
|
|
20
|
+
const contextId = this.contextIds.get(taskId);
|
|
21
|
+
controller.abort();
|
|
22
|
+
this.controllers.delete(taskId);
|
|
23
|
+
this.contextIds.delete(taskId);
|
|
24
|
+
// Publish canceled state to the bus so the client sees the transition.
|
|
25
|
+
// Guard against publishing if the bus is no longer accepting (best-effort).
|
|
26
|
+
try {
|
|
27
|
+
eventBus.publish({
|
|
28
|
+
kind: 'status-update',
|
|
29
|
+
taskId,
|
|
30
|
+
contextId,
|
|
31
|
+
status: {
|
|
32
|
+
state: 'canceled',
|
|
33
|
+
timestamp: new Date().toISOString(),
|
|
34
|
+
},
|
|
35
|
+
final: true,
|
|
36
|
+
});
|
|
37
|
+
eventBus.finished();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// ignore — the SDK subprocess may already be torn down
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
async execute(requestContext, eventBus) {
|
|
44
|
+
const { taskId, contextId, userMessage } = requestContext;
|
|
45
|
+
recordTaskStart();
|
|
46
|
+
// Publish the initial task snapshot. The A2A SDK's ResultManager only
|
|
47
|
+
// initializes its currentTask from a `kind: 'task'` event; without it,
|
|
48
|
+
// subsequent status-update / artifact-update events are dropped as
|
|
49
|
+
// "unknown task" and the SDK reports "no task context found".
|
|
50
|
+
eventBus.publish({
|
|
51
|
+
kind: 'task',
|
|
52
|
+
id: taskId,
|
|
53
|
+
contextId,
|
|
54
|
+
status: {
|
|
55
|
+
state: 'submitted',
|
|
56
|
+
timestamp: new Date().toISOString(),
|
|
57
|
+
},
|
|
58
|
+
history: [userMessage],
|
|
59
|
+
artifacts: [],
|
|
60
|
+
});
|
|
61
|
+
// Emit task_created to monitor bus. A2A has no real "sender" concept beyond
|
|
62
|
+
// user/agent, so we use the convention from: 'user' for any inbound client.
|
|
63
|
+
this.options.monitorBus?.emit({
|
|
64
|
+
type: 'task_created',
|
|
65
|
+
taskId,
|
|
66
|
+
contextId,
|
|
67
|
+
from: 'user',
|
|
68
|
+
to: this.agent.manifest.id,
|
|
69
|
+
preview: extractPreview(userMessage.parts),
|
|
70
|
+
});
|
|
71
|
+
// 1. Extract text content from A2A message
|
|
72
|
+
const parts = userMessage.parts;
|
|
73
|
+
const contentBlocks = a2aToContentBlocks(parts);
|
|
74
|
+
// 2. Set up event translator
|
|
75
|
+
const translator = createTranslator({
|
|
76
|
+
agentId: this.agent.manifest.id,
|
|
77
|
+
taskId,
|
|
78
|
+
contextId,
|
|
79
|
+
bus: eventBus,
|
|
80
|
+
monitorBus: this.options.monitorBus,
|
|
81
|
+
});
|
|
82
|
+
// 3. Wire up cancellation: each task gets its own AbortController. We pass
|
|
83
|
+
// it to the SDK so abort() can tear down the subprocess, and we keep a
|
|
84
|
+
// reference so cancelTask() can find it by taskId.
|
|
85
|
+
const controller = new AbortController();
|
|
86
|
+
this.controllers.set(taskId, controller);
|
|
87
|
+
this.contextIds.set(taskId, contextId);
|
|
88
|
+
// 4. Run Claude (iterate events)
|
|
89
|
+
try {
|
|
90
|
+
for await (const sdkMsg of runClaude({
|
|
91
|
+
agent: this.agent,
|
|
92
|
+
contentBlocks,
|
|
93
|
+
contextId,
|
|
94
|
+
onEvent: (m) => translator.onSdkEvent(m),
|
|
95
|
+
abortController: controller,
|
|
96
|
+
})) {
|
|
97
|
+
// events are published via onEvent
|
|
98
|
+
}
|
|
99
|
+
recordTaskEnd(true);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
recordTaskEnd(false);
|
|
103
|
+
// If the controller was aborted, the SDK throws an AbortError. We don't
|
|
104
|
+
// want to report it as a generic failure — cancelTask already published
|
|
105
|
+
// the canceled state. Suppress the publish in that case.
|
|
106
|
+
const isAbort = controller.signal.aborted || (err instanceof Error && /abort/i.test(err.message));
|
|
107
|
+
if (!isAbort) {
|
|
108
|
+
// SDK subprocess crash, network failure, etc.
|
|
109
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
110
|
+
eventBus.publish({
|
|
111
|
+
kind: 'status-update',
|
|
112
|
+
taskId,
|
|
113
|
+
contextId,
|
|
114
|
+
status: {
|
|
115
|
+
state: 'failed',
|
|
116
|
+
message: {
|
|
117
|
+
kind: 'message',
|
|
118
|
+
role: 'agent',
|
|
119
|
+
messageId: uuidv4(),
|
|
120
|
+
parts: [{ kind: 'text', text: `Subprocess error: ${reason}` }],
|
|
121
|
+
taskId,
|
|
122
|
+
contextId,
|
|
123
|
+
},
|
|
124
|
+
timestamp: new Date().toISOString(),
|
|
125
|
+
},
|
|
126
|
+
final: true,
|
|
127
|
+
});
|
|
128
|
+
eventBus.finished();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
this.controllers.delete(taskId);
|
|
133
|
+
this.contextIds.delete(taskId);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function extractPreview(parts) {
|
|
138
|
+
if (!Array.isArray(parts))
|
|
139
|
+
return '';
|
|
140
|
+
for (const p of parts) {
|
|
141
|
+
if (p && typeof p === 'object' && p.kind === 'text' && typeof p.text === 'string') {
|
|
142
|
+
const t = p.text;
|
|
143
|
+
return t.length > 80 ? t.slice(0, 80) + '…' : t;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return '';
|
|
147
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LoadedAgent } from './types.js';
|
|
2
|
+
export declare class AgentLoadError extends Error {
|
|
3
|
+
agentDir: string;
|
|
4
|
+
cause?: unknown | undefined;
|
|
5
|
+
constructor(agentDir: string, message: string, cause?: unknown | undefined);
|
|
6
|
+
}
|
|
7
|
+
export declare function loadAgent(agentDir: string): LoadedAgent;
|
|
8
|
+
export declare function loadAgents(agentsDir: string): Map<string, LoadedAgent>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// src/agent-loader.ts
|
|
2
|
+
import { readFileSync, readdirSync, existsSync, statSync } from 'fs';
|
|
3
|
+
import { join, resolve, isAbsolute } from 'path';
|
|
4
|
+
import stripJsonComments from 'strip-json-comments';
|
|
5
|
+
import { AgentManifestSchema } from './manifest-schema.js';
|
|
6
|
+
import { loadContext } from './context-manager.js';
|
|
7
|
+
export class AgentLoadError extends Error {
|
|
8
|
+
agentDir;
|
|
9
|
+
cause;
|
|
10
|
+
constructor(agentDir, message, cause) {
|
|
11
|
+
super(`[${agentDir}] ${message}`);
|
|
12
|
+
this.agentDir = agentDir;
|
|
13
|
+
this.cause = cause;
|
|
14
|
+
this.name = 'AgentLoadError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Resolve the manifest file path. Prefers `.jsonc`, falls back to `.json`. */
|
|
18
|
+
function resolveManifestPath(agentDir) {
|
|
19
|
+
const jsonc = join(agentDir, 'manifest.jsonc');
|
|
20
|
+
if (existsSync(jsonc))
|
|
21
|
+
return jsonc;
|
|
22
|
+
const json = join(agentDir, 'manifest.json');
|
|
23
|
+
if (existsSync(json))
|
|
24
|
+
return json;
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
function loadManifest(agentDir) {
|
|
28
|
+
const path = resolveManifestPath(agentDir);
|
|
29
|
+
if (!path) {
|
|
30
|
+
throw new AgentLoadError(agentDir, 'manifest.jsonc (or manifest.json) not found');
|
|
31
|
+
}
|
|
32
|
+
const raw = JSON.parse(stripJsonComments(readFileSync(path, 'utf-8')));
|
|
33
|
+
const result = AgentManifestSchema.safeParse(raw);
|
|
34
|
+
if (!result.success) {
|
|
35
|
+
const issues = result.error.issues.map(i => ` - ${i.path.join('.')}: ${i.message}`).join('\n');
|
|
36
|
+
throw new AgentLoadError(agentDir, `invalid manifest:\n${issues}`);
|
|
37
|
+
}
|
|
38
|
+
return result.data;
|
|
39
|
+
}
|
|
40
|
+
function resolveRef(agentDir, relPath) {
|
|
41
|
+
if (!relPath)
|
|
42
|
+
return undefined;
|
|
43
|
+
const abs = isAbsolute(relPath) ? relPath : join(agentDir, relPath);
|
|
44
|
+
if (!existsSync(abs)) {
|
|
45
|
+
throw new AgentLoadError(agentDir, `referenced file not found: ${relPath} (resolved to ${abs})`);
|
|
46
|
+
}
|
|
47
|
+
return abs;
|
|
48
|
+
}
|
|
49
|
+
export function loadAgent(agentDir) {
|
|
50
|
+
const manifest = loadManifest(agentDir);
|
|
51
|
+
const fixedContextContent = loadContext(agentDir, manifest.fixedContext);
|
|
52
|
+
return {
|
|
53
|
+
manifest,
|
|
54
|
+
agentDir,
|
|
55
|
+
fixedContextContent,
|
|
56
|
+
mcpServersPath: resolveRef(agentDir, manifest.mcpServers),
|
|
57
|
+
hooksPath: resolveRef(agentDir, manifest.hooks),
|
|
58
|
+
settingsPath: resolveRef(agentDir, manifest.settings),
|
|
59
|
+
agentsPath: resolveRef(agentDir, manifest.agents),
|
|
60
|
+
sandboxPath: resolveRef(agentDir, manifest.sandbox),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function loadAgents(agentsDir) {
|
|
64
|
+
const result = new Map();
|
|
65
|
+
const absDir = resolve(agentsDir);
|
|
66
|
+
if (!existsSync(absDir))
|
|
67
|
+
return result;
|
|
68
|
+
for (const name of readdirSync(absDir)) {
|
|
69
|
+
// skip templates and scaffolds: underscore-prefixed directories are not loaded as agents
|
|
70
|
+
if (name.startsWith('_'))
|
|
71
|
+
continue;
|
|
72
|
+
const path = join(absDir, name);
|
|
73
|
+
if (!statSync(path).isDirectory())
|
|
74
|
+
continue;
|
|
75
|
+
if (!resolveManifestPath(path))
|
|
76
|
+
continue;
|
|
77
|
+
try {
|
|
78
|
+
const agent = loadAgent(path);
|
|
79
|
+
if (result.has(agent.manifest.id)) {
|
|
80
|
+
throw new Error(`Duplicate agent id "${agent.manifest.id}"`);
|
|
81
|
+
}
|
|
82
|
+
result.set(agent.manifest.id, agent);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
console.error(`[agent-loader] Failed to load ${name}:`, err instanceof Error ? err.message : err);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import type { LoadedAgent } from './types.js';
|
|
3
|
+
export type ReloadErrorCode = 'not_found' | 'load_error';
|
|
4
|
+
export type ReloadResult = {
|
|
5
|
+
ok: true;
|
|
6
|
+
agent: LoadedAgent;
|
|
7
|
+
} | {
|
|
8
|
+
ok: false;
|
|
9
|
+
code: ReloadErrorCode;
|
|
10
|
+
message: string;
|
|
11
|
+
};
|
|
12
|
+
export declare class AgentRegistry extends EventEmitter {
|
|
13
|
+
private agentsDir;
|
|
14
|
+
private agents;
|
|
15
|
+
constructor(agentsDir: string);
|
|
16
|
+
load(): void;
|
|
17
|
+
list(): string[];
|
|
18
|
+
get(id: string): LoadedAgent | undefined;
|
|
19
|
+
reload(id: string): ReloadResult;
|
|
20
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// src/agent-registry.ts
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
import { loadAgent, loadAgents } from './agent-loader.js';
|
|
4
|
+
export class AgentRegistry extends EventEmitter {
|
|
5
|
+
agentsDir;
|
|
6
|
+
agents = new Map();
|
|
7
|
+
constructor(agentsDir) {
|
|
8
|
+
super();
|
|
9
|
+
this.agentsDir = agentsDir;
|
|
10
|
+
}
|
|
11
|
+
load() {
|
|
12
|
+
this.agents = loadAgents(this.agentsDir);
|
|
13
|
+
for (const [id, agent] of this.agents) {
|
|
14
|
+
this.emit('agent_loaded', { agentId: id, position: agent.manifest.monitor?.position });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
list() {
|
|
18
|
+
return Array.from(this.agents.keys()).sort();
|
|
19
|
+
}
|
|
20
|
+
get(id) {
|
|
21
|
+
return this.agents.get(id);
|
|
22
|
+
}
|
|
23
|
+
reload(id) {
|
|
24
|
+
const existing = this.agents.get(id);
|
|
25
|
+
if (!existing) {
|
|
26
|
+
return { ok: false, code: 'not_found', message: `Agent "${id}" not found` };
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const updated = loadAgent(existing.agentDir);
|
|
30
|
+
this.agents.set(id, updated);
|
|
31
|
+
this.emit('agent_reloaded', { agentId: id });
|
|
32
|
+
return { ok: true, agent: updated };
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
36
|
+
return { ok: false, code: 'load_error', message: msg };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type SDKMessage } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import type { LoadedAgent } from './types.js';
|
|
3
|
+
import type { AnthropicContentBlock } from './part-mapper.js';
|
|
4
|
+
export interface RunClaudeParams {
|
|
5
|
+
agent: LoadedAgent;
|
|
6
|
+
contentBlocks: AnthropicContentBlock[];
|
|
7
|
+
contextId: string;
|
|
8
|
+
onEvent: (event: SDKMessage) => void;
|
|
9
|
+
abortController?: AbortController;
|
|
10
|
+
}
|
|
11
|
+
export declare function runClaude(params: RunClaudeParams): AsyncGenerator<SDKMessage>;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// src/claude-bridge.ts
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
4
|
+
export async function* runClaude(params) {
|
|
5
|
+
const { agent, contentBlocks, onEvent, abortController } = params;
|
|
6
|
+
const m = agent.manifest;
|
|
7
|
+
const systemPrompt = m.systemPromptAppend
|
|
8
|
+
? `${agent.fixedContextContent}\n\n${m.systemPromptAppend}`
|
|
9
|
+
: agent.fixedContextContent;
|
|
10
|
+
const env = {
|
|
11
|
+
...(m.env ?? {}),
|
|
12
|
+
};
|
|
13
|
+
// Only override CLAUDE_CONFIG_DIR if the user explicitly specified configDir.
|
|
14
|
+
// Otherwise, let the subprocess inherit the parent's config (preserving auth).
|
|
15
|
+
if (m.configDir) {
|
|
16
|
+
env.CLAUDE_CONFIG_DIR = m.configDir.startsWith('/')
|
|
17
|
+
? m.configDir
|
|
18
|
+
: join(agent.agentDir, m.configDir);
|
|
19
|
+
}
|
|
20
|
+
// Build options. Note: we do NOT pass `resume: contextId` here — the Claude
|
|
21
|
+
// SDK treats resume as a previously-issued session ID, and A2A's contextId
|
|
22
|
+
// is a fresh UUID with no corresponding SDK session. Treating every send
|
|
23
|
+
// as a brand-new Claude session is the safe default. (Future: persist a
|
|
24
|
+
// contextId→sessionId mapping if cross-call continuity is required.)
|
|
25
|
+
const options = {
|
|
26
|
+
systemPrompt,
|
|
27
|
+
cwd: agent.agentDir,
|
|
28
|
+
env,
|
|
29
|
+
};
|
|
30
|
+
if (abortController) {
|
|
31
|
+
options.abortController = abortController;
|
|
32
|
+
}
|
|
33
|
+
if (m.model)
|
|
34
|
+
options.model = m.model;
|
|
35
|
+
if (m.fallbackModel)
|
|
36
|
+
options.fallbackModel = m.fallbackModel;
|
|
37
|
+
if (m.executable)
|
|
38
|
+
options.executable = m.executable;
|
|
39
|
+
if (m.executableArgs)
|
|
40
|
+
options.executableArgs = m.executableArgs;
|
|
41
|
+
if (m.pathToClaudeCodeExecutable)
|
|
42
|
+
options.pathToClaudeCodeExecutable = m.pathToClaudeCodeExecutable;
|
|
43
|
+
if (m.extraArgs)
|
|
44
|
+
options.extraArgs = m.extraArgs;
|
|
45
|
+
if (m.tools)
|
|
46
|
+
options.tools = m.tools;
|
|
47
|
+
if (m.allowedTools)
|
|
48
|
+
options.allowedTools = m.allowedTools;
|
|
49
|
+
if (m.disallowedTools)
|
|
50
|
+
options.disallowedTools = m.disallowedTools;
|
|
51
|
+
if (m.toolAliases)
|
|
52
|
+
options.toolAliases = m.toolAliases;
|
|
53
|
+
if (m.permissionMode)
|
|
54
|
+
options.permissionMode = m.permissionMode;
|
|
55
|
+
if (m.planModeInstructions)
|
|
56
|
+
options.planModeInstructions = m.planModeInstructions;
|
|
57
|
+
if (m.allowDangerouslySkipPermissions !== undefined)
|
|
58
|
+
options.allowDangerouslySkipPermissions = m.allowDangerouslySkipPermissions;
|
|
59
|
+
if (m.permissionPromptToolName)
|
|
60
|
+
options.permissionPromptToolName = m.permissionPromptToolName;
|
|
61
|
+
if (m.strictMcpConfig !== undefined)
|
|
62
|
+
options.strictMcpConfig = m.strictMcpConfig;
|
|
63
|
+
if (m.additionalDirectories)
|
|
64
|
+
options.additionalDirectories = m.additionalDirectories;
|
|
65
|
+
if (m.includeHookEvents !== undefined)
|
|
66
|
+
options.includeHookEvents = m.includeHookEvents;
|
|
67
|
+
if (m.skills)
|
|
68
|
+
options.skills = m.skills;
|
|
69
|
+
if (m.settingSources)
|
|
70
|
+
options.settingSources = m.settingSources;
|
|
71
|
+
if (m.thinking)
|
|
72
|
+
options.thinking = m.thinking;
|
|
73
|
+
if (m.effort)
|
|
74
|
+
options.effort = m.effort;
|
|
75
|
+
if (m.maxThinkingTokens !== undefined)
|
|
76
|
+
options.maxThinkingTokens = m.maxThinkingTokens;
|
|
77
|
+
if (m.maxTurns !== undefined)
|
|
78
|
+
options.maxTurns = m.maxTurns;
|
|
79
|
+
if (m.maxBudgetUsd !== undefined)
|
|
80
|
+
options.maxBudgetUsd = m.maxBudgetUsd;
|
|
81
|
+
if (m.taskBudget)
|
|
82
|
+
options.taskBudget = m.taskBudget;
|
|
83
|
+
if (m.betas)
|
|
84
|
+
options.betas = m.betas;
|
|
85
|
+
if (m.outputFormat)
|
|
86
|
+
options.outputFormat = m.outputFormat;
|
|
87
|
+
if (m.forkSession !== undefined)
|
|
88
|
+
options.forkSession = m.forkSession;
|
|
89
|
+
if (agent.agentsPath)
|
|
90
|
+
options.agents = agent.agentsPath;
|
|
91
|
+
if (m.agent)
|
|
92
|
+
options.agent = m.agent;
|
|
93
|
+
if (m.agentProgressSummaries !== undefined)
|
|
94
|
+
options.agentProgressSummaries = m.agentProgressSummaries;
|
|
95
|
+
if (m.forwardSubagentText !== undefined)
|
|
96
|
+
options.forwardSubagentText = m.forwardSubagentText;
|
|
97
|
+
if (m.plugins)
|
|
98
|
+
options.plugins = m.plugins;
|
|
99
|
+
if (agent.settingsPath)
|
|
100
|
+
options.settings = agent.settingsPath;
|
|
101
|
+
// Load MCP servers from file if path is set
|
|
102
|
+
if (agent.mcpServersPath) {
|
|
103
|
+
const { readFileSync } = await import('fs');
|
|
104
|
+
options.mcpServers = JSON.parse(readFileSync(agent.mcpServersPath, 'utf-8'));
|
|
105
|
+
}
|
|
106
|
+
// Load hooks from file if path is set
|
|
107
|
+
if (agent.hooksPath) {
|
|
108
|
+
const { readFileSync } = await import('fs');
|
|
109
|
+
options.hooks = JSON.parse(readFileSync(agent.hooksPath, 'utf-8'));
|
|
110
|
+
}
|
|
111
|
+
// Build async iterable prompt
|
|
112
|
+
async function* promptIterable() {
|
|
113
|
+
yield {
|
|
114
|
+
type: 'user',
|
|
115
|
+
message: { role: 'user', content: contentBlocks },
|
|
116
|
+
parent_tool_use_id: null,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const q = query({ prompt: promptIterable(), options });
|
|
120
|
+
for await (const msg of q) {
|
|
121
|
+
onEvent(msg);
|
|
122
|
+
yield msg;
|
|
123
|
+
}
|
|
124
|
+
}
|