@adecore/agents 0.48.3 → 0.49.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.
@@ -3,6 +3,7 @@ import { errorText } from '../error-text.js';
3
3
  import { claudeArgs, claudeEnv, promptPrefix } from '../providers/claude.js';
4
4
  import { ChatChild, MAX_LINE_LENGTH, readStreamLines } from './chat-process.js';
5
5
  import { ClaudeProtocol } from './claude-protocol.js';
6
+ import { claudeSystemPromptArgs } from './claude-system-prompt.js';
6
7
  import { buildUserMessage } from './input.js';
7
8
  // Claude may echo a steer only once a slow tool returns, so a running turn sets no deadline; after the turn ended, a message
8
9
  // it still holds opens the next turn at once or never arrives.
@@ -57,7 +58,12 @@ export class ClaudeBackend {
57
58
  })
58
59
  ];
59
60
  if (this.launch.instructions !== null) {
60
- args.push('--append-system-prompt', this.launch.instructions);
61
+ try {
62
+ args.push(...claudeSystemPromptArgs(this.launch.instructions));
63
+ }
64
+ catch (error) {
65
+ return Promise.reject(error instanceof Error ? error : new Error(errorText(error)));
66
+ }
61
67
  }
62
68
  this.stdinClosed = false;
63
69
  const child = new ChatChild({
@@ -22,6 +22,12 @@ function taskUsage(usage) {
22
22
  function contextTokens(usage) {
23
23
  return isRecord(usage) ? num(usage.input_tokens) + num(usage.cache_creation_input_tokens) + num(usage.cache_read_input_tokens) : 0;
24
24
  }
25
+ /*
26
+ * Claude Code 2.1.312 says `completed` for a subagent that stopped to wait on its own background work, and
27
+ * notifies the same task again when it reports. No field tells them apart: `handback` is also absent from a
28
+ * subagent that does not report through `SubagentHandback`, and `background_tasks_changed` drops the agent too.
29
+ */
30
+ const INTERIM_TASK_SUMMARY = 'This agent has not reported yet';
25
31
  // How long a prompt cache entry lives, by the field of `usage.cache_creation` that counts what a request wrote with it.
26
32
  const CACHE_TTL_MS = { ephemeral_5m_input_tokens: 5 * 60_000, ephemeral_1h_input_tokens: 60 * 60_000 };
27
33
  /*
@@ -409,10 +415,20 @@ export class ClaudeProtocol {
409
415
  }
410
416
  else if (frame.subtype === 'task_notification') {
411
417
  const ref = str(frame.tool_use_id);
418
+ const taskId = str(frame.task_id);
419
+ if (str(frame.status) === 'completed' && str(frame.summary)?.startsWith(INTERIM_TASK_SUMMARY)) {
420
+ // `task_updated` already took it off the list of running agents.
421
+ if (taskId) {
422
+ this.backgroundAgents.add(taskId);
423
+ }
424
+ if (ref) {
425
+ events.push({ type: 'task.progress', ref, taskId, summary: null, lastTool: null, usage: taskUsage(frame.usage) });
426
+ }
427
+ return;
428
+ }
412
429
  if (ref) {
413
430
  this.subagentModels.delete(ref);
414
431
  }
415
- const taskId = str(frame.task_id);
416
432
  if (taskId) {
417
433
  this.endBackground(taskId, events);
418
434
  this.workflows.delete(taskId);
@@ -0,0 +1 @@
1
+ export declare function claudeSystemPromptArgs(instructions: string, platform?: NodeJS.Platform, folder?: string): string[];
@@ -0,0 +1,18 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ /*
6
+ * The flags that hand Claude its system prompt. npm installs the CLI as a batch file on Windows, and
7
+ * a line through cmd.exe holds no line break and stops near 8 KB, so there the prompt goes in a file.
8
+ * The name follows the content, so chats with the same prompt share one file and no chat removes it from under another.
9
+ */
10
+ export function claudeSystemPromptArgs(instructions, platform = process.platform, folder = tmpdir()) {
11
+ if (platform !== 'win32') {
12
+ return ['--append-system-prompt', instructions];
13
+ }
14
+ const digest = createHash('sha256').update(instructions).digest('hex').slice(0, 32);
15
+ const path = join(folder, `adecore-claude-prompt-${digest}.md`);
16
+ writeFileSync(path, instructions, { mode: 0o600 });
17
+ return ['--append-system-prompt-file', path];
18
+ }
@@ -146,7 +146,7 @@ export class CodexBackend {
146
146
  result = await transport.request('thread/start', { ...params, ...instructions });
147
147
  }
148
148
  this.imageInputSupported = await this.readImageSupport(transport);
149
- for (const event of this.protocol.threadReady(result)) {
149
+ for (const event of this.protocol.threadReady(result, params.sandbox)) {
150
150
  if (event.type === 'session' && event.agentSessionId) {
151
151
  this.threadId = event.agentSessionId;
152
152
  }
@@ -213,6 +213,15 @@ export class CodexBackend {
213
213
  return [...textInput(parts.join('')), ...images.map((attachment) => ({ type: 'localImage', path: attachment.path }))];
214
214
  }
215
215
  sendTurn(input) {
216
+ // TUI menus are not model instructions; explain the missing host flow instead of spending a turn on it.
217
+ if (this.transport &&
218
+ input.attachments.length === 0 &&
219
+ input.mentions.length === 0 &&
220
+ input.skills.length === 0 &&
221
+ /^\/usage(?:[ \t]+[^\r\n]*)?$/.test(input.text.trim())) {
222
+ this.reply(input.promptId, 'This chat does not support the /usage menu. Open Codex in a terminal and run /usage to view account activity or use an available earned rate-limit reset.');
223
+ return;
224
+ }
216
225
  const command = input.attachments.length === 0 ? parseGoalCommand(input.text) : null;
217
226
  if (command !== null) {
218
227
  void this.runGoalCommand(command, input);
@@ -24,7 +24,7 @@ export declare class CodexProtocol {
24
24
  private readonly agentRows;
25
25
  constructor(generation: number);
26
26
  get turnId(): string | null;
27
- threadReady(result: unknown): BackendEvent[];
27
+ threadReady(result: unknown, askedSandbox?: string): BackendEvent[];
28
28
  handle(frame: CodexFrame): BackendEvent[];
29
29
  approvalDecision(requestId: string, decision: ApprovalDecision): {
30
30
  rpcId: CodexRpcId;
@@ -57,6 +57,7 @@ function textOf(content) {
57
57
  .filter((text) => text !== null)
58
58
  .join('\n');
59
59
  }
60
+ const SANDBOX_NOT_SET_UP = "Codex's sandbox is not set up here, so it can only read and asks before each command. Run `codex` once in a terminal to set it up.";
60
61
  // What `agentsStates` says of an agent that stopped working; the call that reports it may have completed long before.
61
62
  const ENDED_AGENT_STATES = new Set(['completed', 'errored', 'interrupted', 'shutdown', 'notFound']);
62
63
  /* What became of every agent a collab call touched, one line each; empty while the call still runs. */
@@ -189,7 +190,7 @@ export class CodexProtocol {
189
190
  return this.codexTurnId;
190
191
  }
191
192
  /* The result of `thread/start` or `thread/resume`. The thread id is what a terminal resumes. */
192
- threadReady(result) {
193
+ threadReady(result, askedSandbox) {
193
194
  const thread = isRecord(result) && isRecord(result.thread) ? result.thread : {};
194
195
  this.ownThreadId = str(thread.id) ?? this.ownThreadId;
195
196
  const policy = isRecord(result) ? str(result.approvalPolicy) : null;
@@ -203,7 +204,11 @@ export class CodexProtocol {
203
204
  title: cleanTitle(thread.name),
204
205
  ...(policy && sandbox ? { permissionMode: `${policy} / ${sandbox}` } : {}),
205
206
  ...(effectiveRuntimeMode === undefined ? {} : { effectiveRuntimeMode })
206
- }
207
+ },
208
+ // On Windows a sandbox that was never set up leaves a thread read-only without an error, and the question that sets it up lives in Codex's TUI.
209
+ ...(askedSandbox === 'workspace-write' && sandbox === 'readOnly'
210
+ ? [{ type: 'note', level: 'warning', text: SANDBOX_NOT_SET_UP }]
211
+ : [])
207
212
  ];
208
213
  }
209
214
  handle(frame) {
@@ -3,7 +3,7 @@
3
3
  * compaction, edits, subagents, interrupts and crashes through the real frame shapes.
4
4
  */
5
5
  import { spawn } from 'node:child_process';
6
- import { writeFileSync } from 'node:fs';
6
+ import { readFileSync, writeFileSync } from 'node:fs';
7
7
  import { resolve } from 'node:path';
8
8
  export function fakeClaude(io) {
9
9
  const args = io.argv;
@@ -938,7 +938,8 @@ export function fakeClaude(io) {
938
938
  }
939
939
  if (text === 'system?') {
940
940
  const at = args.indexOf('--append-system-prompt');
941
- assistantText(at >= 0 ? args[at + 1] : 'none');
941
+ const fileAt = args.indexOf('--append-system-prompt-file');
942
+ assistantText(at >= 0 ? args[at + 1] : fileAt >= 0 ? readFileSync(args[fileAt + 1], 'utf8') : 'none');
942
943
  result();
943
944
  return;
944
945
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/agents",
3
- "version": "0.48.3",
3
+ "version": "0.49.0",
4
4
  "description": "Node-compatible agent chat hosts, providers, accounts, usage and durable task coordination.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -93,8 +93,8 @@
93
93
  "test": "bun --conditions=source test"
94
94
  },
95
95
  "dependencies": {
96
- "@adecore/agent-contracts": "0.48.3",
97
- "@adecore/intelligent-ui": "0.48.3",
96
+ "@adecore/agent-contracts": "0.49.0",
97
+ "@adecore/intelligent-ui": "0.49.0",
98
98
  "zod": "^4.6.7"
99
99
  },
100
100
  "devDependencies": {
@@ -4,6 +4,7 @@ import { claudeArgs, claudeEnv, promptPrefix } from '../providers/claude.ts';
4
4
  import type { ApprovalDecision, BackendHost, BackendLaunch, ChatBackend, TurnInput } from './backend.ts';
5
5
  import { ChatChild, MAX_LINE_LENGTH, readStreamLines } from './chat-process.ts';
6
6
  import { ClaudeProtocol } from './claude-protocol.ts';
7
+ import { claudeSystemPromptArgs } from './claude-system-prompt.ts';
7
8
  import { buildUserMessage } from './input.ts';
8
9
 
9
10
  // Claude may echo a steer only once a slow tool returns, so a running turn sets no deadline; after the turn ended, a message
@@ -72,7 +73,11 @@ export class ClaudeBackend implements ChatBackend {
72
73
  ];
73
74
 
74
75
  if (this.launch.instructions !== null) {
75
- args.push('--append-system-prompt', this.launch.instructions);
76
+ try {
77
+ args.push(...claudeSystemPromptArgs(this.launch.instructions));
78
+ } catch (error) {
79
+ return Promise.reject(error instanceof Error ? error : new Error(errorText(error)));
80
+ }
76
81
  }
77
82
 
78
83
  this.stdinClosed = false;
@@ -41,6 +41,13 @@ function contextTokens(usage: unknown): number {
41
41
  return isRecord(usage) ? num(usage.input_tokens) + num(usage.cache_creation_input_tokens) + num(usage.cache_read_input_tokens) : 0;
42
42
  }
43
43
 
44
+ /*
45
+ * Claude Code 2.1.312 says `completed` for a subagent that stopped to wait on its own background work, and
46
+ * notifies the same task again when it reports. No field tells them apart: `handback` is also absent from a
47
+ * subagent that does not report through `SubagentHandback`, and `background_tasks_changed` drops the agent too.
48
+ */
49
+ const INTERIM_TASK_SUMMARY = 'This agent has not reported yet';
50
+
44
51
  // How long a prompt cache entry lives, by the field of `usage.cache_creation` that counts what a request wrote with it.
45
52
  const CACHE_TTL_MS: Record<string, number> = { ephemeral_5m_input_tokens: 5 * 60_000, ephemeral_1h_input_tokens: 60 * 60_000 };
46
53
 
@@ -511,13 +518,25 @@ export class ClaudeProtocol {
511
518
  }
512
519
  } else if (frame.subtype === 'task_notification') {
513
520
  const ref = str(frame.tool_use_id);
521
+ const taskId = str(frame.task_id);
522
+
523
+ if (str(frame.status) === 'completed' && str(frame.summary)?.startsWith(INTERIM_TASK_SUMMARY)) {
524
+ // `task_updated` already took it off the list of running agents.
525
+ if (taskId) {
526
+ this.backgroundAgents.add(taskId);
527
+ }
528
+
529
+ if (ref) {
530
+ events.push({ type: 'task.progress', ref, taskId, summary: null, lastTool: null, usage: taskUsage(frame.usage) });
531
+ }
532
+
533
+ return;
534
+ }
514
535
 
515
536
  if (ref) {
516
537
  this.subagentModels.delete(ref);
517
538
  }
518
539
 
519
- const taskId = str(frame.task_id);
520
-
521
540
  if (taskId) {
522
541
  this.endBackground(taskId, events);
523
542
  this.workflows.delete(taskId);
@@ -0,0 +1,22 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ /*
7
+ * The flags that hand Claude its system prompt. npm installs the CLI as a batch file on Windows, and
8
+ * a line through cmd.exe holds no line break and stops near 8 KB, so there the prompt goes in a file.
9
+ * The name follows the content, so chats with the same prompt share one file and no chat removes it from under another.
10
+ */
11
+ export function claudeSystemPromptArgs(instructions: string, platform: NodeJS.Platform = process.platform, folder: string = tmpdir()): string[] {
12
+ if (platform !== 'win32') {
13
+ return ['--append-system-prompt', instructions];
14
+ }
15
+
16
+ const digest = createHash('sha256').update(instructions).digest('hex').slice(0, 32);
17
+ const path = join(folder, `adecore-claude-prompt-${digest}.md`);
18
+
19
+ writeFileSync(path, instructions, { mode: 0o600 });
20
+
21
+ return ['--append-system-prompt-file', path];
22
+ }
@@ -180,7 +180,7 @@ export class CodexBackend implements ChatBackend {
180
180
 
181
181
  this.imageInputSupported = await this.readImageSupport(transport);
182
182
 
183
- for (const event of this.protocol.threadReady(result)) {
183
+ for (const event of this.protocol.threadReady(result, params.sandbox)) {
184
184
  if (event.type === 'session' && event.agentSessionId) {
185
185
  this.threadId = event.agentSessionId;
186
186
  }
@@ -264,6 +264,22 @@ export class CodexBackend implements ChatBackend {
264
264
  }
265
265
 
266
266
  sendTurn(input: TurnInput): void {
267
+ // TUI menus are not model instructions; explain the missing host flow instead of spending a turn on it.
268
+ if (
269
+ this.transport &&
270
+ input.attachments.length === 0 &&
271
+ input.mentions.length === 0 &&
272
+ input.skills.length === 0 &&
273
+ /^\/usage(?:[ \t]+[^\r\n]*)?$/.test(input.text.trim())
274
+ ) {
275
+ this.reply(
276
+ input.promptId,
277
+ 'This chat does not support the /usage menu. Open Codex in a terminal and run /usage to view account activity or use an available earned rate-limit reset.'
278
+ );
279
+
280
+ return;
281
+ }
282
+
267
283
  const command = input.attachments.length === 0 ? parseGoalCommand(input.text) : null;
268
284
 
269
285
  if (command !== null) {
@@ -83,6 +83,8 @@ function textOf(content: unknown): string {
83
83
  .join('\n');
84
84
  }
85
85
 
86
+ const SANDBOX_NOT_SET_UP = "Codex's sandbox is not set up here, so it can only read and asks before each command. Run `codex` once in a terminal to set it up.";
87
+
86
88
  // What `agentsStates` says of an agent that stopped working; the call that reports it may have completed long before.
87
89
  const ENDED_AGENT_STATES = new Set(['completed', 'errored', 'interrupted', 'shutdown', 'notFound']);
88
90
 
@@ -259,7 +261,7 @@ export class CodexProtocol {
259
261
  }
260
262
 
261
263
  /* The result of `thread/start` or `thread/resume`. The thread id is what a terminal resumes. */
262
- threadReady(result: unknown): BackendEvent[] {
264
+ threadReady(result: unknown, askedSandbox?: string): BackendEvent[] {
263
265
  const thread = isRecord(result) && isRecord(result.thread) ? result.thread : {};
264
266
  this.ownThreadId = str(thread.id) ?? this.ownThreadId;
265
267
  const policy = isRecord(result) ? str(result.approvalPolicy) : null;
@@ -274,7 +276,11 @@ export class CodexProtocol {
274
276
  title: cleanTitle(thread.name),
275
277
  ...(policy && sandbox ? { permissionMode: `${policy} / ${sandbox}` } : {}),
276
278
  ...(effectiveRuntimeMode === undefined ? {} : { effectiveRuntimeMode })
277
- }
279
+ },
280
+ // On Windows a sandbox that was never set up leaves a thread read-only without an error, and the question that sets it up lives in Codex's TUI.
281
+ ...(askedSandbox === 'workspace-write' && sandbox === 'readOnly'
282
+ ? [{ type: 'note' as const, level: 'warning' as const, text: SANDBOX_NOT_SET_UP }]
283
+ : [])
278
284
  ];
279
285
  }
280
286
 
@@ -3,7 +3,7 @@
3
3
  * compaction, edits, subagents, interrupts and crashes through the real frame shapes.
4
4
  */
5
5
  import { spawn } from 'node:child_process';
6
- import { writeFileSync } from 'node:fs';
6
+ import { readFileSync, writeFileSync } from 'node:fs';
7
7
  import { resolve } from 'node:path';
8
8
  import type { FakeCli, FakeIo, FakeProgram } from './fake-cli.ts';
9
9
 
@@ -1113,7 +1113,8 @@ export function fakeClaude(io: FakeIo): FakeProgram {
1113
1113
 
1114
1114
  if (text === 'system?') {
1115
1115
  const at = args.indexOf('--append-system-prompt');
1116
- assistantText(at >= 0 ? args[at + 1]! : 'none');
1116
+ const fileAt = args.indexOf('--append-system-prompt-file');
1117
+ assistantText(at >= 0 ? args[at + 1]! : fileAt >= 0 ? readFileSync(args[fileAt + 1]!, 'utf8') : 'none');
1117
1118
  result();
1118
1119
 
1119
1120
  return;