agents-dojo 0.1.4 → 0.1.6

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.
@@ -1,4 +1,5 @@
1
1
  import { type Express } from 'express';
2
+ import { DojoAgentExecutor } from './agent-executor.js';
2
3
  import type { AgentRegistry } from './agent-registry.js';
3
4
  import type { MonitorBus } from './monitor-bus.js';
4
5
  export interface A2AServerOptions {
@@ -9,6 +10,7 @@ export interface A2AServerOptions {
9
10
  }
10
11
  export interface A2AServerHandle {
11
12
  app: Express;
13
+ executors: Map<string, DojoAgentExecutor>;
12
14
  close: () => Promise<void>;
13
15
  }
14
16
  export declare function createA2AServer(opts: A2AServerOptions): A2AServerHandle;
@@ -10,6 +10,7 @@ export function createA2AServer(opts) {
10
10
  const agents = opts.singleAgent
11
11
  ? [opts.registry.get(opts.singleAgent)].filter((a) => a !== undefined)
12
12
  : opts.registry.list().map((id) => opts.registry.get(id)).filter((a) => a !== undefined);
13
+ const executors = new Map();
13
14
  for (const loaded of agents) {
14
15
  const card = {
15
16
  name: loaded.manifest.name,
@@ -28,6 +29,7 @@ export function createA2AServer(opts) {
28
29
  };
29
30
  const taskStore = new InMemoryTaskStore();
30
31
  const executor = new DojoAgentExecutor(loaded, { monitorBus: opts.monitorBus });
32
+ executors.set(loaded.manifest.id, executor);
31
33
  const requestHandler = new DefaultRequestHandler(card, taskStore, executor);
32
34
  const userBuilder = UserBuilder.noAuthentication;
33
35
  app.use(`/a2a/${loaded.manifest.id}`, jsonRpcHandler({ requestHandler, userBuilder }));
@@ -36,6 +38,7 @@ export function createA2AServer(opts) {
36
38
  }
37
39
  return {
38
40
  app,
41
+ executors,
39
42
  close: () => new Promise((resolve) => {
40
43
  // No actual server here; createServer() handles listening
41
44
  resolve();
@@ -9,7 +9,14 @@ export declare class DojoAgentExecutor implements AgentExecutor {
9
9
  private options;
10
10
  private controllers;
11
11
  private contextIds;
12
+ /** Persistent session ID for this agent — enables cross-call memory. */
13
+ private sessionId;
12
14
  constructor(agent: LoadedAgent, options?: AgentExecutorOptions);
13
15
  cancelTask: (taskId: string, eventBus: ExecutionEventBus) => Promise<void>;
14
16
  execute(requestContext: RequestContext, eventBus: ExecutionEventBus): Promise<void>;
17
+ /**
18
+ * btw-style one-shot chat: forks the agent's current session, disables all tools,
19
+ * gets a text-only reply, then discards the fork. Does not affect the main session.
20
+ */
21
+ executeBtw(message: string): Promise<string>;
15
22
  }
@@ -29,6 +29,8 @@ export class DojoAgentExecutor {
29
29
  options;
30
30
  controllers = new Map();
31
31
  contextIds = new Map();
32
+ /** Persistent session ID for this agent — enables cross-call memory. */
33
+ sessionId;
32
34
  constructor(agent, options = {}) {
33
35
  this.agent = agent;
34
36
  this.options = options;
@@ -133,11 +135,16 @@ export class DojoAgentExecutor {
133
135
  agent: this.agent,
134
136
  contentBlocks,
135
137
  contextId,
138
+ resume: this.sessionId,
136
139
  onEvent: (m) => {
137
140
  translator.onSdkEvent(m);
138
141
  sdkLogger.onSdkMessage(m);
139
- // Capture final text for summary log
140
142
  const msg = m;
143
+ // Capture session_id for cross-call continuity
144
+ if (msg.session_id && typeof msg.session_id === 'string') {
145
+ this.sessionId = msg.session_id;
146
+ }
147
+ // Capture final text for summary log
141
148
  if (msg.type === 'assistant' && msg.message?.content) {
142
149
  for (const block of msg.message.content) {
143
150
  if (block.type === 'text')
@@ -198,14 +205,40 @@ export class DojoAgentExecutor {
198
205
  });
199
206
  }
200
207
  }
208
+ /**
209
+ * btw-style one-shot chat: forks the agent's current session, disables all tools,
210
+ * gets a text-only reply, then discards the fork. Does not affect the main session.
211
+ */
212
+ async executeBtw(message) {
213
+ // btw is only meaningful when the agent already has a session to fork from.
214
+ // Without a session, there's no context to branch — just run a plain ephemeral query.
215
+ let reply = '';
216
+ for await (const sdkMsg of runClaude({
217
+ agent: this.agent,
218
+ contentBlocks: [{ type: 'text', text: message }],
219
+ contextId: 'btw-' + Date.now(),
220
+ resume: this.sessionId, // undefined if no session yet — SDK creates a fresh one
221
+ btw: this.sessionId !== undefined, // only fork if there's a session to fork from
222
+ onEvent: (m) => {
223
+ const msg = m;
224
+ // Do NOT capture session_id — btw must never affect the main session
225
+ if (msg.type === 'assistant' && msg.message?.content) {
226
+ for (const block of msg.message.content) {
227
+ if (block.type === 'text')
228
+ reply = block.text;
229
+ }
230
+ }
231
+ },
232
+ })) { /* consume */ }
233
+ return reply || '(no response)';
234
+ }
201
235
  }
202
236
  function extractPreview(parts) {
203
237
  if (!Array.isArray(parts))
204
238
  return '';
205
239
  for (const p of parts) {
206
240
  if (p && typeof p === 'object' && p.kind === 'text' && typeof p.text === 'string') {
207
- const t = p.text;
208
- return t.length > 80 ? t.slice(0, 80) + '…' : t;
241
+ return p.text;
209
242
  }
210
243
  }
211
244
  return '';
@@ -7,5 +7,9 @@ export interface RunClaudeParams {
7
7
  contextId: string;
8
8
  onEvent: (event: SDKMessage) => void;
9
9
  abortController?: AbortController;
10
+ /** Resume an existing session (pass the session UUID from a previous query). */
11
+ resume?: string;
12
+ /** btw mode: fork the session, disable all tools, don't persist the fork. */
13
+ btw?: boolean;
10
14
  }
11
15
  export declare function runClaude(params: RunClaudeParams): AsyncGenerator<SDKMessage>;
@@ -1,5 +1,6 @@
1
1
  // src/claude-bridge.ts
2
2
  import { join } from 'path';
3
+ import { existsSync, mkdirSync, symlinkSync } from 'fs';
3
4
  import { query } from '@anthropic-ai/claude-agent-sdk';
4
5
  export async function* runClaude(params) {
5
6
  const { agent, contentBlocks, onEvent, abortController } = params;
@@ -12,23 +13,42 @@ export async function* runClaude(params) {
12
13
  PATH: process.env.PATH,
13
14
  ...(m.env ?? {}),
14
15
  };
15
- // Only override CLAUDE_CONFIG_DIR if the user explicitly specified configDir.
16
- // Otherwise, let the subprocess inherit the parent's config (preserving auth).
17
- if (m.configDir) {
18
- env.CLAUDE_CONFIG_DIR = m.configDir.startsWith('/')
19
- ? m.configDir
20
- : join(agent.agentDir, m.configDir);
16
+ // Each agent stores sessions/config in its own .claude directory by default.
17
+ // This keeps session data co-located with the agent rather than polluting ~/.claude.
18
+ const agentConfigDir = m.configDir
19
+ ? (m.configDir.startsWith('/') ? m.configDir : join(agent.agentDir, m.configDir))
20
+ : join(agent.agentDir, '.claude');
21
+ env.CLAUDE_CONFIG_DIR = agentConfigDir;
22
+ // Ensure the agent's .claude dir exists and symlink global settings (auth, permissions).
23
+ // Symlink keeps all agents in sync with the user's global config automatically.
24
+ mkdirSync(agentConfigDir, { recursive: true });
25
+ const globalSettings = join(process.env.HOME ?? '', '.claude', 'settings.json');
26
+ const localSettings = join(agentConfigDir, 'settings.json');
27
+ if (existsSync(globalSettings) && !existsSync(localSettings)) {
28
+ try {
29
+ symlinkSync(globalSettings, localSettings);
30
+ }
31
+ catch { /* race or permission — ignore */ }
21
32
  }
22
- // Build options. Note: we do NOT pass `resume: contextId` here — the Claude
23
- // SDK treats resume as a previously-issued session ID, and A2A's contextId
24
- // is a fresh UUID with no corresponding SDK session. Treating every send
25
- // as a brand-new Claude session is the safe default. (Future: persist a
26
- // contextId→sessionId mapping if cross-call continuity is required.)
27
33
  const options = {
28
34
  systemPrompt,
29
35
  cwd: agent.agentDir,
30
36
  env,
31
37
  };
38
+ // Session continuity: resume an existing session if provided
39
+ if (params.resume) {
40
+ options.resume = params.resume;
41
+ }
42
+ // btw mode: disable tools, don't persist. Fork only if resuming an existing session.
43
+ if (params.btw) {
44
+ options.tools = [];
45
+ options.allowedTools = [];
46
+ options.disallowedTools = [];
47
+ options.persistSession = false;
48
+ if (params.resume) {
49
+ options.forkSession = true;
50
+ }
51
+ }
32
52
  if (abortController) {
33
53
  options.abortController = abortController;
34
54
  }
@@ -44,12 +64,15 @@ export async function* runClaude(params) {
44
64
  options.pathToClaudeCodeExecutable = m.pathToClaudeCodeExecutable;
45
65
  if (m.extraArgs)
46
66
  options.extraArgs = m.extraArgs;
47
- if (m.tools)
48
- options.tools = m.tools;
49
- if (m.allowedTools)
50
- options.allowedTools = m.allowedTools;
51
- if (m.disallowedTools)
52
- options.disallowedTools = m.disallowedTools;
67
+ // In btw mode, tools are already disabled — don't let manifest override
68
+ if (!params.btw) {
69
+ if (m.tools)
70
+ options.tools = m.tools;
71
+ if (m.allowedTools)
72
+ options.allowedTools = m.allowedTools;
73
+ if (m.disallowedTools)
74
+ options.disallowedTools = m.disallowedTools;
75
+ }
53
76
  if (m.toolAliases)
54
77
  options.toolAliases = m.toolAliases;
55
78
  if (m.permissionMode)
@@ -86,7 +109,7 @@ export async function* runClaude(params) {
86
109
  options.betas = m.betas;
87
110
  if (m.outputFormat)
88
111
  options.outputFormat = m.outputFormat;
89
- if (m.forkSession !== undefined)
112
+ if (m.forkSession !== undefined && !params.btw)
90
113
  options.forkSession = m.forkSession;
91
114
  if (agent.agentsPath)
92
115
  options.agents = agent.agentsPath;
package/dist/cli.js CHANGED
@@ -188,63 +188,22 @@ Options:
188
188
  -h, --help Show this help
189
189
  `);
190
190
  }
191
- // ── chat command ─────────────────────────────────────────
191
+ // ── chat command (btw mode) ───────────────────────────────
192
192
  async function runChat(agentId, message, port) {
193
- const { v4: uuidv4 } = await import('uuid');
194
- const body = {
195
- jsonrpc: '2.0',
196
- id: 1,
197
- method: 'message/send',
198
- params: {
199
- message: {
200
- messageId: uuidv4(),
201
- kind: 'message',
202
- role: 'user',
203
- parts: [{ kind: 'text', text: message }],
204
- },
205
- },
206
- };
207
- const res = await fetch(`http://localhost:${port}/a2a/${agentId}`, {
193
+ const res = await fetch(`http://localhost:${port}/btw/${agentId}`, {
208
194
  method: 'POST',
209
195
  headers: { 'Content-Type': 'application/json' },
210
- body: JSON.stringify(body),
196
+ body: JSON.stringify({ message }),
211
197
  });
212
198
  if (!res.ok) {
213
- console.error(`Error: A2A server returned ${res.status}`);
199
+ console.error(`Error: server returned ${res.status}`);
214
200
  const text = await res.text();
215
201
  if (text)
216
202
  console.error(text);
217
203
  process.exit(1);
218
204
  }
219
205
  const json = await res.json();
220
- const result = json.result;
221
- if (!result) {
222
- console.error('Error: no result in response');
223
- process.exit(1);
224
- }
225
- // Extract reply text
226
- let replyText = '';
227
- for (const a of (result.artifacts ?? [])) {
228
- for (const p of (a.parts ?? [])) {
229
- if (p.kind === 'text')
230
- replyText += p.text;
231
- }
232
- }
233
- if (!replyText) {
234
- const statusMsg = result.status?.message;
235
- if (statusMsg) {
236
- for (const p of (statusMsg.parts ?? [])) {
237
- if (p.kind === 'text')
238
- replyText += p.text;
239
- }
240
- }
241
- }
242
- if (replyText) {
243
- console.log(replyText);
244
- }
245
- else {
246
- console.log('(no response)');
247
- }
206
+ console.log(json.reply ?? '(no response)');
248
207
  }
249
208
  // ── monitor GUI launcher ─────────────────────────────────
250
209
  /** Resolve the monitor/ directory shipped alongside the package. */
@@ -30,7 +30,7 @@ export function createTranslator(ctx) {
30
30
  type: 'task_status',
31
31
  taskId: ctx.taskId,
32
32
  state: 'working',
33
- message: text.length > 80 ? text.slice(0, 77) + '...' : text,
33
+ message: text,
34
34
  });
35
35
  }
36
36
  function publishCompleted() {
@@ -1,14 +1,15 @@
1
1
  import { WebSocketServer, WebSocket } from 'ws';
2
2
  import type { Server } from 'http';
3
3
  import type { AgentRegistry } from './agent-registry.js';
4
+ import type { DojoAgentExecutor } from './agent-executor.js';
4
5
  import type { MonitorBus } from './monitor-bus.js';
5
6
  export interface MonitorWsOptions {
6
7
  server: Server;
7
8
  bus: MonitorBus;
8
9
  path: string;
9
10
  registry: AgentRegistry;
10
- /** A2A server port — needed to proxy chat commands. */
11
- a2aPort: number;
11
+ /** Agent executors — needed for btw-style chat. */
12
+ executors: Map<string, DojoAgentExecutor>;
12
13
  }
13
14
  export type MonitorCommand = {
14
15
  type: 'reload';
@@ -1,6 +1,5 @@
1
1
  // src/monitor-ws.ts
2
2
  import { WebSocketServer, WebSocket } from 'ws';
3
- import { v4 as uuidv4 } from 'uuid';
4
3
  export function createMonitorWs(opts) {
5
4
  const clients = new Set();
6
5
  const wss = new WebSocketServer({ server: opts.server, path: opts.path });
@@ -37,56 +36,13 @@ export function createMonitorWs(opts) {
37
36
  });
38
37
  }
39
38
  }
40
- /** Proxy a chat message to the A2A endpoint and stream the reply back. */
39
+ /** btw-style chat: fork agent session, get text-only reply, discard fork. */
41
40
  async function handleChat(cmd, senderWs) {
42
- const body = {
43
- jsonrpc: '2.0',
44
- id: 1,
45
- method: 'message/send',
46
- params: {
47
- message: {
48
- messageId: uuidv4(),
49
- kind: 'message',
50
- role: 'user',
51
- parts: [{ kind: 'text', text: cmd.message }],
52
- },
53
- },
54
- };
55
- const res = await fetch(`http://localhost:${opts.a2aPort}/a2a/${cmd.agentId}`, {
56
- method: 'POST',
57
- headers: { 'Content-Type': 'application/json' },
58
- body: JSON.stringify(body),
59
- });
60
- if (!res.ok) {
61
- throw new Error(`A2A returned ${res.status}: ${await res.text()}`);
62
- }
63
- const json = await res.json();
64
- // Extract the agent's reply text from the A2A JSON-RPC response
65
- let replyText = '';
66
- const result = json.result;
67
- if (result) {
68
- // Try artifacts first (main response content)
69
- const artifacts = result.artifacts ?? [];
70
- for (const a of artifacts) {
71
- for (const p of (a.parts ?? [])) {
72
- if (p.kind === 'text')
73
- replyText += p.text;
74
- }
75
- }
76
- // Fall back to status message
77
- if (!replyText) {
78
- const statusMsg = result.status?.message;
79
- if (statusMsg) {
80
- for (const p of (statusMsg.parts ?? [])) {
81
- if (p.kind === 'text')
82
- replyText += p.text;
83
- }
84
- }
85
- }
41
+ const executor = opts.executors.get(cmd.agentId);
42
+ if (!executor) {
43
+ throw new Error(`Agent "${cmd.agentId}" not found`);
86
44
  }
87
- if (!replyText)
88
- replyText = '(no response)';
89
- // Send the response only to the requesting client
45
+ const replyText = await executor.executeBtw(cmd.message);
90
46
  const event = {
91
47
  type: 'chat_response',
92
48
  chatId: cmd.chatId,
package/dist/server.js CHANGED
@@ -38,6 +38,27 @@ export async function createServer(opts) {
38
38
  app.get('/logs/dates', (_req, res) => {
39
39
  res.json({ dates: listLogDates(logDir) });
40
40
  });
41
+ // btw-style one-shot chat endpoint (forks session, no tools, ephemeral)
42
+ app.post('/btw/:agentId', async (req, res) => {
43
+ const { agentId } = req.params;
44
+ const { message } = req.body;
45
+ if (!message) {
46
+ res.status(400).json({ error: 'message required' });
47
+ return;
48
+ }
49
+ const executor = a2a.executors.get(agentId);
50
+ if (!executor) {
51
+ res.status(404).json({ error: `agent "${agentId}" not found` });
52
+ return;
53
+ }
54
+ try {
55
+ const reply = await executor.executeBtw(message);
56
+ res.json({ reply });
57
+ }
58
+ catch (err) {
59
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
60
+ }
61
+ });
41
62
  const httpServer = createHttpServer(app);
42
63
  // Listen on main server first so we know the actual port (important when port=0)
43
64
  await new Promise((r) => httpServer.listen(opts.port ?? 41241, r));
@@ -47,7 +68,7 @@ export async function createServer(opts) {
47
68
  if (opts.monitorPort !== undefined) {
48
69
  // Bind monitor on a separate server (port 0 = OS picks a free port)
49
70
  monitorHttp = createHttpServer();
50
- createMonitorWs({ server: monitorHttp, bus, path: '/monitor', registry, a2aPort: actualA2APort });
71
+ createMonitorWs({ server: monitorHttp, bus, path: '/monitor', registry, executors: a2a.executors });
51
72
  await new Promise((r) => monitorHttp.listen(opts.monitorPort, r));
52
73
  actualMonitorPort = monitorHttp.address().port;
53
74
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-dojo",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "A2A-compatible Agent framework built on Claude Code SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",