@adecore/agents 0.48.4 → 0.49.1

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.
@@ -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);
@@ -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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/agents",
3
- "version": "0.48.4",
3
+ "version": "0.49.1",
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.4",
97
- "@adecore/intelligent-ui": "0.48.4",
96
+ "@adecore/agent-contracts": "0.49.1",
97
+ "@adecore/intelligent-ui": "0.49.1",
98
98
  "zod": "^4.6.7"
99
99
  },
100
100
  "devDependencies": {
@@ -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);
@@ -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