@agentbean/daemon 0.1.15 → 0.1.17

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.
@@ -12,10 +12,23 @@ function renderPayload(input, systemPrompt) {
12
12
  function stripAnsi(s) {
13
13
  return s.replace(/\x1b\[[0-9;]*m/g, '');
14
14
  }
15
- function extractReply(output) {
16
- const clean = stripAnsi(output).replace(/\r\n/g, '\n');
15
+ function removeEchoedPayload(output, payload) {
16
+ if (!payload)
17
+ return output;
18
+ const normalizedPayload = payload.replace(/\r\n/g, '\n');
19
+ const idx = output.lastIndexOf(normalizedPayload);
20
+ if (idx < 0)
21
+ return output;
22
+ const boundaryBefore = idx === 0 || output[idx - 1] === '\n' || output[idx - 1] === '\r';
23
+ if (!boundaryBefore)
24
+ return output;
25
+ const tail = output.slice(idx + normalizedPayload.length).trim();
26
+ return tail || output;
27
+ }
28
+ export function extractCodexReply(output, payload) {
29
+ const clean = removeEchoedPayload(stripAnsi(output).replace(/\r\n/g, '\n'), payload);
17
30
  // Match "codex" label followed by reply content, ending before next hook or end
18
- const match = clean.match(/\ncodex\n([\s\S]*?)(?:\nhook:|$)/i);
31
+ const match = clean.match(/(?:^|\n)codex\n([\s\S]*?)(?:\nhook:|$)/i);
19
32
  if (match)
20
33
  return match[1].trim();
21
34
  // Fallback: everything after last "user" prompt
@@ -35,6 +48,10 @@ function normalizeExecArgs(args) {
35
48
  }
36
49
  return baseArgs;
37
50
  }
51
+ function adapterTimeoutMs() {
52
+ const fromEnv = Number.parseInt(process.env.AGENTBEAN_CODEX_TIMEOUT_MS ?? '', 10);
53
+ return Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : 900_000;
54
+ }
38
55
  export class CodexAdapter {
39
56
  opts;
40
57
  kind = 'codex';
@@ -61,7 +78,7 @@ export class CodexAdapter {
61
78
  });
62
79
  const chunks = [];
63
80
  let finished = false;
64
- const MAX_EXEC_MS = 600_000;
81
+ const MAX_EXEC_MS = adapterTimeoutMs();
65
82
  const onAbort = () => {
66
83
  if (finished)
67
84
  return;
@@ -100,7 +117,7 @@ export class CodexAdapter {
100
117
  const detail = stripAnsi(raw).trim();
101
118
  return reject(new Error(detail ? `codex exit ${exitCode}: ${detail}` : `codex exit ${exitCode}`));
102
119
  }
103
- const reply = extractReply(raw);
120
+ const reply = extractCodexReply(raw, payload);
104
121
  resolve(reply || '(Codex 已完成处理)');
105
122
  });
106
123
  });
@@ -32,6 +32,35 @@ export function extractHermesReply(output) {
32
32
  .replace(ANSI_RE, '')
33
33
  .replace(/\r\n?/g, '\n')
34
34
  .split('\n');
35
+ let boxStart = -1;
36
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
37
+ if (lines[i]?.trim().startsWith('╭')) {
38
+ boxStart = i;
39
+ break;
40
+ }
41
+ }
42
+ if (boxStart >= 0) {
43
+ const boxLines = [];
44
+ for (const line of lines.slice(boxStart + 1)) {
45
+ const trimmed = line.trim();
46
+ if (trimmed.startsWith('╰'))
47
+ break;
48
+ if (BOX_ONLY_RE.test(trimmed))
49
+ continue;
50
+ boxLines.push(line);
51
+ }
52
+ const boxedReply = boxLines
53
+ .map((line) => line.trimEnd())
54
+ .filter((line) => {
55
+ const trimmed = line.trim();
56
+ return !(trimmed.startsWith('╭') || trimmed.startsWith('╰') || BOX_ONLY_RE.test(trimmed));
57
+ })
58
+ .map((line) => line.replace(/^[│┃]\s?/, '').replace(/\s?[│┃]$/, '').replace(/^\s{2,}/, ''))
59
+ .join('\n')
60
+ .trim();
61
+ if (boxedReply)
62
+ return boxedReply;
63
+ }
35
64
  const cleaned = lines
36
65
  .map((line) => line.trimEnd())
37
66
  .filter((line) => {
@@ -58,6 +58,7 @@ export class AgentInstance {
58
58
  name;
59
59
  role;
60
60
  visibility;
61
+ activeControllers = new Map();
61
62
  constructor(config, adapter) {
62
63
  this.config = config;
63
64
  this.adapter = adapter;
@@ -79,6 +80,7 @@ export class AgentInstance {
79
80
  async handleDispatch(opts) {
80
81
  const { socket, req, serverUrl, token, networkId, deviceId } = opts;
81
82
  const ctl = new AbortController();
83
+ this.activeControllers.set(req.requestId, ctl);
82
84
  const dispatchStart = Date.now();
83
85
  const teamId = req.teamId ?? req.networkId ?? networkId;
84
86
  const projectWorkspace = req.sandboxed ? getWorkspaceDir(this.id) : this.config.adapter.workspace;
@@ -109,7 +111,7 @@ export class AgentInstance {
109
111
  outputDirs: [run.outputDir, run.intermediateDir],
110
112
  });
111
113
  archivedFiles = archiveOutputFiles(run, processed.outputFiles);
112
- const replyText = formatWorkspaceReply(rawBody, archivedFiles);
114
+ const replyText = formatWorkspaceReply(rawBody, archivedFiles, { exposeLocalPaths: false });
113
115
  finishAgentWorkspaceRun(run, { replyText, files: archivedFiles, status: 'completed' });
114
116
  const artifactIds = [];
115
117
  if (archivedFiles.length > 0) {
@@ -161,5 +163,16 @@ export class AgentInstance {
161
163
  requestId: req.requestId,
162
164
  });
163
165
  }
166
+ finally {
167
+ this.activeControllers.delete(req.requestId);
168
+ }
169
+ }
170
+ cancelDispatch(requestId) {
171
+ const entries = requestId
172
+ ? [...this.activeControllers.entries()].filter(([id]) => id === requestId)
173
+ : [...this.activeControllers.entries()];
174
+ for (const [, ctl] of entries)
175
+ ctl.abort();
176
+ return entries.length;
164
177
  }
165
178
  }
@@ -54,6 +54,7 @@ export function createConnection(cfg, adapter) {
54
54
  let socket = null;
55
55
  let heartbeatTimer = null;
56
56
  let queue = Promise.resolve();
57
+ const activeControllers = new Map();
57
58
  return {
58
59
  async start() {
59
60
  const agentUrl = cfg.server.url.endsWith('/agent') ? cfg.server.url : cfg.server.url + '/agent';
@@ -91,6 +92,7 @@ export function createConnection(cfg, adapter) {
91
92
  }
92
93
  queue = queue.then(async () => {
93
94
  const ctl = new AbortController();
95
+ activeControllers.set(req.requestId, ctl);
94
96
  const dispatchStart = Date.now();
95
97
  const teamId = 'default';
96
98
  const run = beginAgentWorkspaceRun({
@@ -122,7 +124,7 @@ export function createConnection(cfg, adapter) {
122
124
  outputDirs: [run.outputDir, run.intermediateDir],
123
125
  });
124
126
  archivedFiles = archiveOutputFiles(run, processed.outputFiles);
125
- const replyText = formatWorkspaceReply(rawBody, archivedFiles);
127
+ const replyText = formatWorkspaceReply(rawBody, archivedFiles, { exposeLocalPaths: false });
126
128
  finishAgentWorkspaceRun(run, { replyText, files: archivedFiles, status: 'completed' });
127
129
  const artifactIds = [];
128
130
  if (archivedFiles.length > 0) {
@@ -171,10 +173,21 @@ export function createConnection(cfg, adapter) {
171
173
  requestId: req.requestId,
172
174
  });
173
175
  }
176
+ finally {
177
+ activeControllers.delete(req.requestId);
178
+ }
174
179
  }).catch((err) => {
175
180
  logger.error({ err: err?.message, requestId: req.requestId }, 'dispatch queue error');
176
181
  });
177
182
  });
183
+ socket.on('dispatch:cancel', (payload) => {
184
+ const entries = payload.requestId
185
+ ? [...activeControllers.entries()].filter(([id]) => id === payload.requestId)
186
+ : [...activeControllers.entries()];
187
+ for (const [, ctl] of entries)
188
+ ctl.abort();
189
+ logger.info({ requestId: payload.requestId, cancelled: entries.length, reason: payload.reason }, 'dispatch cancel requested');
190
+ });
178
191
  socket.on('disconnect', (reason) => {
179
192
  logger.warn({ reason }, 'disconnected');
180
193
  if (heartbeatTimer) {
@@ -315,6 +315,17 @@ export function createDeviceDaemon(cfg, agents) {
315
315
  });
316
316
  queues.set(req.agentId, next);
317
317
  });
318
+ socket.on('dispatch:cancel', (payload) => {
319
+ const targets = payload.agentId ? [payload.agentId] : [...agents.keys()];
320
+ let cancelled = 0;
321
+ for (const agentId of targets) {
322
+ const agent = agents.get(agentId);
323
+ if (!agent)
324
+ continue;
325
+ cancelled += agent.cancelDispatch(payload.requestId);
326
+ }
327
+ logger.info({ agentId: payload.agentId, requestId: payload.requestId, cancelled, reason: payload.reason }, 'dispatch cancel requested');
328
+ });
318
329
  socket.on('agents:discover', async () => {
319
330
  await scanAndRegister(socket, false);
320
331
  });
@@ -42,6 +42,11 @@ function replaceAllLiteral(value, search, replacement) {
42
42
  return value;
43
43
  return value.replace(new RegExp(escapeRegExp(search), 'g'), replacement);
44
44
  }
45
+ function stripDevicePath(value, replacement) {
46
+ let body = replaceAllLiteral(value, `file://${replacement}`, basename(replacement));
47
+ body = replaceAllLiteral(body, replacement, basename(replacement));
48
+ return body;
49
+ }
45
50
  export function beginAgentWorkspaceRun(input) {
46
51
  const teamId = safeSegment(input.teamId);
47
52
  const agentId = safeSegment(input.agentId);
@@ -122,17 +127,25 @@ export function archiveOutputFiles(run, files) {
122
127
  }
123
128
  return archived;
124
129
  }
125
- export function formatWorkspaceReply(reply, files) {
130
+ export function formatWorkspaceReply(reply, files, options = {}) {
131
+ const exposeLocalPaths = options.exposeLocalPaths ?? true;
126
132
  let body = reply;
127
133
  for (const file of files) {
128
- body = replaceAllLiteral(body, `file://${file.originalPath}`, `file://${file.archivedPath}`);
129
- body = replaceAllLiteral(body, file.originalPath, file.archivedPath);
134
+ if (exposeLocalPaths) {
135
+ body = replaceAllLiteral(body, `file://${file.originalPath}`, `file://${file.archivedPath}`);
136
+ body = replaceAllLiteral(body, file.originalPath, file.archivedPath);
137
+ }
138
+ else {
139
+ body = stripDevicePath(body, file.originalPath);
140
+ body = stripDevicePath(body, file.archivedPath);
141
+ }
130
142
  }
131
- const missingPaths = files
132
- .map((file) => file.archivedPath)
133
- .filter((path) => !body.includes(path));
143
+ const missingPaths = files.filter((file) => {
144
+ const marker = exposeLocalPaths ? file.archivedPath : basename(file.archivedPath);
145
+ return !body.includes(marker);
146
+ });
134
147
  if (missingPaths.length > 0) {
135
- body += '\n\n已生成文件:\n' + missingPaths.map((path) => `- ${path}`).join('\n');
148
+ body += '\n\n已生成文件:\n' + missingPaths.map((file) => `- ${exposeLocalPaths ? file.archivedPath : basename(file.archivedPath)}`).join('\n');
136
149
  }
137
150
  return body;
138
151
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agentbean/daemon",
3
3
  "private": false,
4
- "version": "0.1.15",
4
+ "version": "0.1.17",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "bin": {