@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.
- package/dist/chat/claude-backend.js +7 -1
- package/dist/chat/claude-protocol.js +17 -1
- package/dist/chat/claude-system-prompt.d.ts +1 -0
- package/dist/chat/claude-system-prompt.js +18 -0
- package/dist/chat/codex-backend.js +10 -1
- package/dist/chat/codex-protocol.d.ts +1 -1
- package/dist/chat/codex-protocol.js +7 -2
- package/dist/testing/fake-claude.js +3 -2
- package/package.json +3 -3
- package/src/chat/claude-backend.ts +6 -1
- package/src/chat/claude-protocol.ts +21 -2
- package/src/chat/claude-system-prompt.ts +22 -0
- package/src/chat/codex-backend.ts +17 -1
- package/src/chat/codex-protocol.ts +8 -2
- package/src/testing/fake-claude.ts +3 -2
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
97
|
-
"@adecore/intelligent-ui": "0.
|
|
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
|
-
|
|
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
|
-
|
|
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;
|