@adecore/agents 0.27.2 → 0.27.3
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/backend.d.ts +1 -0
- package/dist/chat/chat-core.d.ts +1 -0
- package/dist/chat/chat-core.js +1 -0
- package/dist/chat/chat-session.d.ts +1 -0
- package/dist/chat/chat-session.js +1 -0
- package/dist/chat/codex-backend.d.ts +1 -0
- package/dist/chat/codex-backend.js +22 -2
- package/dist/testing/fake-codex.d.ts +1 -0
- package/dist/testing/fake-codex.js +16 -0
- package/package.json +3 -3
- package/src/chat/backend.ts +3 -0
- package/src/chat/chat-core.ts +3 -0
- package/src/chat/chat-session.ts +2 -0
- package/src/chat/codex-backend.ts +22 -2
- package/src/testing/fake-codex.ts +18 -0
package/dist/chat/backend.d.ts
CHANGED
package/dist/chat/chat-core.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export interface ChatCoreHooks {
|
|
|
29
29
|
runtimeModeFor?(chatId: string, mode: RuntimeMode): RuntimeMode;
|
|
30
30
|
instructionsFor?(chatId: string): string | null;
|
|
31
31
|
resumeNoteFor?(chatId: string): string | null;
|
|
32
|
+
resumeInstructionsFor?(chatId: string): string | null;
|
|
32
33
|
foldersFor?(chatId: string): readonly string[];
|
|
33
34
|
projectConfigFor?(chatId: string): boolean;
|
|
34
35
|
hiddenFor?(chatId: string): boolean;
|
package/dist/chat/chat-core.js
CHANGED
|
@@ -306,6 +306,7 @@ export class ChatCore {
|
|
|
306
306
|
saveGeneratedImage: (ref, upload) => this.attachments.saveGenerated(payload.chatId, ref, upload),
|
|
307
307
|
instructions: () => (this.hooks.instructionsFor ? this.hooks.instructionsFor(payload.chatId) : this.instructions),
|
|
308
308
|
resumeNote: () => this.hooks.resumeNoteFor?.(payload.chatId) ?? null,
|
|
309
|
+
resumeInstructions: () => this.hooks.resumeInstructionsFor?.(payload.chatId) ?? null,
|
|
309
310
|
folders: () => this.hooks.foldersFor?.(payload.chatId) ?? [],
|
|
310
311
|
projectConfig: () => this.hooks.projectConfigFor?.(payload.chatId) ?? true,
|
|
311
312
|
...(promptNotes ? { promptNotes } : {}),
|
|
@@ -41,6 +41,7 @@ export interface ChatSessionOptions {
|
|
|
41
41
|
saveGeneratedImage?: BackendLaunch['saveGeneratedImage'];
|
|
42
42
|
instructions?(): string | null;
|
|
43
43
|
resumeNote?(): string | null;
|
|
44
|
+
resumeInstructions?(): string | null;
|
|
44
45
|
folders?(): readonly string[];
|
|
45
46
|
projectConfig?(): boolean;
|
|
46
47
|
promptNotes?: PromptNotes;
|
|
@@ -1629,6 +1629,7 @@ export class ChatSession {
|
|
|
1629
1629
|
saveGeneratedImage: this.options.saveGeneratedImage,
|
|
1630
1630
|
instructions: this.options.instructions?.() ?? null,
|
|
1631
1631
|
resumeNote: this.options.resumeNote?.() ?? null,
|
|
1632
|
+
resumeInstructions: this.options.resumeInstructions?.() ?? null,
|
|
1632
1633
|
...(info.goal === undefined ? {} : { goal: info.goal }),
|
|
1633
1634
|
...(folders.length > 0 ? { folders } : {}),
|
|
1634
1635
|
...(projectConfig ? {} : { projectConfig }),
|
|
@@ -68,7 +68,7 @@ export class CodexBackend {
|
|
|
68
68
|
imageWrites = null;
|
|
69
69
|
transport = null;
|
|
70
70
|
threadId = '';
|
|
71
|
-
// Codex
|
|
71
|
+
// Codex ignores developer instructions on `thread/resume` (measured on 0.154 and 0.163), so the first prompt of a resumed process carries them.
|
|
72
72
|
contextPending = null;
|
|
73
73
|
imageInputSupported = null;
|
|
74
74
|
/* A turn asked for that Codex has not named yet; `turn/interrupt` needs that name. */
|
|
@@ -131,7 +131,7 @@ export class CodexBackend {
|
|
|
131
131
|
if (this.launch.resume) {
|
|
132
132
|
try {
|
|
133
133
|
result = await transport.request('thread/resume', { threadId: this.launch.resume, excludeTurns: true, ...params });
|
|
134
|
-
this.contextPending = this.launch.
|
|
134
|
+
this.contextPending = await this.resumeContext(transport, this.launch.resume);
|
|
135
135
|
}
|
|
136
136
|
catch (error) {
|
|
137
137
|
// A refusal means the thread is gone from Codex's store and a fresh one keeps the chat usable; a silence or an exit says nothing about the thread.
|
|
@@ -156,6 +156,26 @@ export class CodexBackend {
|
|
|
156
156
|
// An app-server without goals, or with the feature turned off, has none to report.
|
|
157
157
|
});
|
|
158
158
|
}
|
|
159
|
+
/*
|
|
160
|
+
* A developer message outranks the thread's first instructions, where a note in front of the prompt loses to
|
|
161
|
+
* them; an app-server without `thread/inject_items` gets the instructions in front of the prompt after all.
|
|
162
|
+
*/
|
|
163
|
+
async resumeContext(transport, threadId) {
|
|
164
|
+
const instructions = this.launch.resumeInstructions ?? null;
|
|
165
|
+
if (instructions !== null) {
|
|
166
|
+
try {
|
|
167
|
+
const items = [{ type: 'message', role: 'developer', content: [{ type: 'input_text', text: instructions }] }];
|
|
168
|
+
await transport.request('thread/inject_items', { threadId, items });
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (!(error instanceof Error) || error.name !== 'CodexRpcError') {
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
return [instructions, this.launch.resumeNote].filter((part) => part !== null).join('\n\n');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return this.launch.resumeNote;
|
|
178
|
+
}
|
|
159
179
|
async readGoal(transport) {
|
|
160
180
|
const result = await transport.request('thread/goal/get', { threadId: this.threadId });
|
|
161
181
|
const goal = codexGoal(isRecord(result) ? result.goal : null);
|
|
@@ -4,6 +4,7 @@ export declare const FAKE_CHILD_STEPS = 250;
|
|
|
4
4
|
export declare const fakeCodexForks: Frame[];
|
|
5
5
|
export interface FakeCodexOptions {
|
|
6
6
|
resumeLead?: string;
|
|
7
|
+
injectItems?: boolean;
|
|
7
8
|
}
|
|
8
9
|
export declare function fakeCodexWith(options: FakeCodexOptions): FakeCli;
|
|
9
10
|
export declare const fakeCodex: FakeCli;
|
|
@@ -451,6 +451,22 @@ export function fakeCodexWith(options) {
|
|
|
451
451
|
notify('thread/started', { thread });
|
|
452
452
|
return;
|
|
453
453
|
}
|
|
454
|
+
case 'thread/inject_items': {
|
|
455
|
+
if (options.injectItems === false) {
|
|
456
|
+
out({ id, error: { code: -32601, message: 'unknown method thread/inject_items' } });
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
// The newest developer message is the one the model follows, so `note?` answers with it.
|
|
460
|
+
const items = Array.isArray(params.items) ? params.items : [];
|
|
461
|
+
for (const injected of items) {
|
|
462
|
+
const content = Array.isArray(injected.content) ? injected.content[0] : undefined;
|
|
463
|
+
if (injected.role === 'developer' && typeof content?.text === 'string') {
|
|
464
|
+
threadInstructions.set(String(params.threadId), content.text);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
out({ id, result: {} });
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
454
470
|
case 'thread/fork': {
|
|
455
471
|
const source = String(params.threadId);
|
|
456
472
|
const turns = threadTurns.get(source) ?? [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/agents",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.3",
|
|
4
4
|
"description": "Node-compatible agent chat hosts, providers, accounts, usage and durable task coordination.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -81,8 +81,8 @@
|
|
|
81
81
|
"test": "bun --conditions=source test"
|
|
82
82
|
},
|
|
83
83
|
"dependencies": {
|
|
84
|
-
"@adecore/agent-contracts": "0.27.
|
|
85
|
-
"@adecore/intelligent-ui": "0.27.
|
|
84
|
+
"@adecore/agent-contracts": "0.27.3",
|
|
85
|
+
"@adecore/intelligent-ui": "0.27.3",
|
|
86
86
|
"zod": "^4.6.5"
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
package/src/chat/backend.ts
CHANGED
|
@@ -40,6 +40,9 @@ export interface BackendLaunch {
|
|
|
40
40
|
// The part of it a resumed thread has to hear again in front of its next prompt, for a CLI that ignores new
|
|
41
41
|
// instructions on a resume; null when there is nothing to repeat.
|
|
42
42
|
resumeNote: string | null;
|
|
43
|
+
// Instructions a resumed thread takes in as a developer message, for a CLI that keeps the ones its thread started
|
|
44
|
+
// with; absent or null when the thread holds them already.
|
|
45
|
+
resumeInstructions?: string | null;
|
|
43
46
|
// The goal the chat last knew of, which a CLI that takes it up again from its own transcript on a resume still works toward.
|
|
44
47
|
goal?: ChatGoal;
|
|
45
48
|
// Folders outside `cwd` the agent may reach without asking. Claude Code takes them as `--add-dir`; Codex's
|
package/src/chat/chat-core.ts
CHANGED
|
@@ -119,6 +119,8 @@ export interface ChatCoreHooks {
|
|
|
119
119
|
instructionsFor?(chatId: string): string | null;
|
|
120
120
|
/* What of the instructions a resumed thread hears again in front of its next prompt, for a CLI that ignores them on a resume. */
|
|
121
121
|
resumeNoteFor?(chatId: string): string | null;
|
|
122
|
+
/* Instructions a resumed thread takes in again as a developer message, for a CLI that keeps those its thread started with; null when it holds them. */
|
|
123
|
+
resumeInstructionsFor?(chatId: string): string | null;
|
|
122
124
|
/*
|
|
123
125
|
* Folders outside its working directory a chat's agent may reach without asking, absolute. Asked
|
|
124
126
|
* before every turn, so a host can hand one over for as long as it is needed: a change starts the
|
|
@@ -495,6 +497,7 @@ export class ChatCore {
|
|
|
495
497
|
saveGeneratedImage: (ref, upload) => this.attachments.saveGenerated(payload.chatId, ref, upload),
|
|
496
498
|
instructions: () => (this.hooks.instructionsFor ? this.hooks.instructionsFor(payload.chatId) : this.instructions),
|
|
497
499
|
resumeNote: () => this.hooks.resumeNoteFor?.(payload.chatId) ?? null,
|
|
500
|
+
resumeInstructions: () => this.hooks.resumeInstructionsFor?.(payload.chatId) ?? null,
|
|
498
501
|
folders: () => this.hooks.foldersFor?.(payload.chatId) ?? [],
|
|
499
502
|
projectConfig: () => this.hooks.projectConfigFor?.(payload.chatId) ?? true,
|
|
500
503
|
...(promptNotes ? { promptNotes } : {}),
|
package/src/chat/chat-session.ts
CHANGED
|
@@ -76,6 +76,7 @@ export interface ChatSessionOptions {
|
|
|
76
76
|
// What the agent is told at the start of every process, and what of it a resumed thread hears again; see `BackendLaunch`.
|
|
77
77
|
instructions?(): string | null;
|
|
78
78
|
resumeNote?(): string | null;
|
|
79
|
+
resumeInstructions?(): string | null;
|
|
79
80
|
// The folders beside the working directory, asked before every turn; a change starts the next turn in a new process.
|
|
80
81
|
folders?(): readonly string[];
|
|
81
82
|
// False runs the CLI without the configuration of the working folder; asked before every turn like the folders.
|
|
@@ -1876,6 +1877,7 @@ export class ChatSession {
|
|
|
1876
1877
|
saveGeneratedImage: this.options.saveGeneratedImage,
|
|
1877
1878
|
instructions: this.options.instructions?.() ?? null,
|
|
1878
1879
|
resumeNote: this.options.resumeNote?.() ?? null,
|
|
1880
|
+
resumeInstructions: this.options.resumeInstructions?.() ?? null,
|
|
1879
1881
|
...(info.goal === undefined ? {} : { goal: info.goal }),
|
|
1880
1882
|
...(folders.length > 0 ? { folders } : {}),
|
|
1881
1883
|
...(projectConfig ? {} : { projectConfig }),
|
|
@@ -81,7 +81,7 @@ export class CodexBackend implements ChatBackend {
|
|
|
81
81
|
private imageWrites: Promise<void> | null = null;
|
|
82
82
|
private transport: CodexTransport | null = null;
|
|
83
83
|
private threadId = '';
|
|
84
|
-
// Codex
|
|
84
|
+
// Codex ignores developer instructions on `thread/resume` (measured on 0.154 and 0.163), so the first prompt of a resumed process carries them.
|
|
85
85
|
private contextPending: string | null = null;
|
|
86
86
|
private imageInputSupported: boolean | null = null;
|
|
87
87
|
/* A turn asked for that Codex has not named yet; `turn/interrupt` needs that name. */
|
|
@@ -148,7 +148,7 @@ export class CodexBackend implements ChatBackend {
|
|
|
148
148
|
if (this.launch.resume) {
|
|
149
149
|
try {
|
|
150
150
|
result = await transport.request('thread/resume', { threadId: this.launch.resume, excludeTurns: true, ...params });
|
|
151
|
-
this.contextPending = this.launch.
|
|
151
|
+
this.contextPending = await this.resumeContext(transport, this.launch.resume);
|
|
152
152
|
} catch (error) {
|
|
153
153
|
// A refusal means the thread is gone from Codex's store and a fresh one keeps the chat usable; a silence or an exit says nothing about the thread.
|
|
154
154
|
if (!(error instanceof Error) || error.name !== 'CodexRpcError') {
|
|
@@ -172,6 +172,26 @@ export class CodexBackend implements ChatBackend {
|
|
|
172
172
|
});
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
/*
|
|
176
|
+
* A developer message outranks the thread's first instructions, where a note in front of the prompt loses to
|
|
177
|
+
* them; an app-server without `thread/inject_items` gets the instructions in front of the prompt after all.
|
|
178
|
+
*/
|
|
179
|
+
private async resumeContext(transport: CodexTransport, threadId: string): Promise<string | null> {
|
|
180
|
+
const instructions = this.launch.resumeInstructions ?? null;
|
|
181
|
+
if (instructions !== null) {
|
|
182
|
+
try {
|
|
183
|
+
const items = [{ type: 'message', role: 'developer', content: [{ type: 'input_text', text: instructions }] }];
|
|
184
|
+
await transport.request('thread/inject_items', { threadId, items });
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (!(error instanceof Error) || error.name !== 'CodexRpcError') {
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
return [instructions, this.launch.resumeNote].filter((part) => part !== null).join('\n\n');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return this.launch.resumeNote;
|
|
193
|
+
}
|
|
194
|
+
|
|
175
195
|
private async readGoal(transport: CodexTransport): Promise<ChatGoal | null> {
|
|
176
196
|
const result = await transport.request('thread/goal/get', { threadId: this.threadId });
|
|
177
197
|
const goal = codexGoal(isRecord(result) ? result.goal : null);
|
|
@@ -26,6 +26,8 @@ export const fakeCodexForks: Frame[] = [];
|
|
|
26
26
|
export interface FakeCodexOptions {
|
|
27
27
|
// How the note a host repeats in front of a resumed thread's first prompt starts; `pasted?` answers it back.
|
|
28
28
|
resumeLead?: string;
|
|
29
|
+
// False answers `thread/inject_items` the way an app-server without it does.
|
|
30
|
+
injectItems?: boolean;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
|
|
@@ -490,6 +492,22 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
|
|
|
490
492
|
notify('thread/started', { thread });
|
|
491
493
|
return;
|
|
492
494
|
}
|
|
495
|
+
case 'thread/inject_items': {
|
|
496
|
+
if (options.injectItems === false) {
|
|
497
|
+
out({ id, error: { code: -32601, message: 'unknown method thread/inject_items' } });
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
// The newest developer message is the one the model follows, so `note?` answers with it.
|
|
501
|
+
const items = Array.isArray(params.items) ? (params.items as Frame[]) : [];
|
|
502
|
+
for (const injected of items) {
|
|
503
|
+
const content = Array.isArray(injected.content) ? (injected.content[0] as { text?: unknown } | undefined) : undefined;
|
|
504
|
+
if (injected.role === 'developer' && typeof content?.text === 'string') {
|
|
505
|
+
threadInstructions.set(String(params.threadId), content.text);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
out({ id, result: {} });
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
493
511
|
case 'thread/fork': {
|
|
494
512
|
const source = String(params.threadId);
|
|
495
513
|
const turns = threadTurns.get(source) ?? [];
|