@adecore/agents 0.31.6 → 0.31.7
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/acp-backend.js +10 -3
- package/dist/chat/acp-protocol.d.ts +3 -0
- package/dist/chat/acp-protocol.js +26 -2
- package/dist/chat/chat-session.js +5 -2
- package/dist/chat/fixtures/copilot-1.0.101-turn.json +677 -0
- package/dist/testing/fake-acp-capture.d.ts +11 -0
- package/dist/testing/fake-acp-capture.js +50 -0
- package/package.json +3 -3
- package/src/chat/acp-backend.ts +9 -3
- package/src/chat/acp-protocol.ts +30 -2
- package/src/chat/chat-session.ts +5 -2
- package/src/chat/fixtures/copilot-1.0.101-turn.json +677 -0
- package/src/testing/fake-acp-capture.ts +70 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FakeCli } from './fake-cli.ts';
|
|
2
|
+
export interface AcpCapture {
|
|
3
|
+
sessionId: string;
|
|
4
|
+
initialize: unknown;
|
|
5
|
+
sessionNew: unknown;
|
|
6
|
+
commands: unknown[];
|
|
7
|
+
setConfigOption: unknown;
|
|
8
|
+
promptUpdates: unknown[];
|
|
9
|
+
promptResponse: unknown;
|
|
10
|
+
}
|
|
11
|
+
export declare function fakeAcpCapture(capture: AcpCapture): FakeCli;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export function fakeAcpCapture(capture) {
|
|
2
|
+
return (io) => {
|
|
3
|
+
const send = (frame) => {
|
|
4
|
+
io.out({ jsonrpc: '2.0', ...frame });
|
|
5
|
+
};
|
|
6
|
+
const update = (fields) => {
|
|
7
|
+
send({ method: 'session/update', params: { sessionId: capture.sessionId, update: fields } });
|
|
8
|
+
};
|
|
9
|
+
return {
|
|
10
|
+
onLine: (line) => {
|
|
11
|
+
const frame = JSON.parse(line);
|
|
12
|
+
const id = frame.id;
|
|
13
|
+
switch (frame.method) {
|
|
14
|
+
case undefined:
|
|
15
|
+
return;
|
|
16
|
+
case 'initialize':
|
|
17
|
+
send({ id, result: capture.initialize });
|
|
18
|
+
return;
|
|
19
|
+
case 'session/new':
|
|
20
|
+
update({ sessionUpdate: 'available_commands_update', availableCommands: capture.commands });
|
|
21
|
+
send({ id, result: capture.sessionNew });
|
|
22
|
+
return;
|
|
23
|
+
case 'session/set_config_option':
|
|
24
|
+
send({ id, result: capture.setConfigOption });
|
|
25
|
+
return;
|
|
26
|
+
case 'session/set_mode':
|
|
27
|
+
case 'session/close':
|
|
28
|
+
send({ id, result: {} });
|
|
29
|
+
return;
|
|
30
|
+
case 'session/prompt': {
|
|
31
|
+
const blocks = Array.isArray(frame.params?.prompt) ? frame.params.prompt : [];
|
|
32
|
+
const text = blocks.find((block) => block.type === 'text')?.text;
|
|
33
|
+
for (const fields of capture.promptUpdates) {
|
|
34
|
+
const named = fields.sessionUpdate === 'session_info_update';
|
|
35
|
+
update(named ? { ...fields, title: text } : fields);
|
|
36
|
+
}
|
|
37
|
+
send({ id, result: capture.promptResponse });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
case 'session/cancel':
|
|
41
|
+
return;
|
|
42
|
+
default:
|
|
43
|
+
if (id !== undefined) {
|
|
44
|
+
send({ id, error: { code: -32601, message: `Method not found: ${String(frame.method)}` } });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/agents",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.7",
|
|
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.31.
|
|
85
|
-
"@adecore/intelligent-ui": "0.31.
|
|
84
|
+
"@adecore/agent-contracts": "0.31.7",
|
|
85
|
+
"@adecore/intelligent-ui": "0.31.7",
|
|
86
86
|
"zod": "^4.6.5"
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
package/src/chat/acp-backend.ts
CHANGED
|
@@ -357,7 +357,9 @@ export class AcpBackend implements ChatBackend {
|
|
|
357
357
|
const named: ChatAttachment[] = input.attachments.filter((attachment) => !images.includes(attachment));
|
|
358
358
|
const parts = [this.contextPending, input.preamble, input.text, attachmentNote(named)].filter((part): part is string => part !== null && part !== '');
|
|
359
359
|
this.contextPending = null;
|
|
360
|
-
|
|
360
|
+
const text = parts.join('\n\n');
|
|
361
|
+
this.protocol.prompted(text);
|
|
362
|
+
return [{ type: 'text', text }, ...blocks, ...this.mentionBlocks(input.mentions)];
|
|
361
363
|
}
|
|
362
364
|
|
|
363
365
|
/*
|
|
@@ -577,13 +579,17 @@ export class AcpBackend implements ChatBackend {
|
|
|
577
579
|
this.emit({ type: 'exit', exitCode, ...(stderr === null ? {} : { stderr }) });
|
|
578
580
|
}
|
|
579
581
|
|
|
580
|
-
/* A cancelled turn still spent what it reports. */
|
|
582
|
+
/* A cancelled turn still spent what it reports. A host that fails to keep it costs the record, never the end of the turn. */
|
|
581
583
|
private report(result: unknown): void {
|
|
582
584
|
const usage = this.spec.onUsage === undefined ? null : promptUsageOf(result);
|
|
583
585
|
if (usage === null) {
|
|
584
586
|
return;
|
|
585
587
|
}
|
|
586
|
-
|
|
588
|
+
try {
|
|
589
|
+
this.spec.onUsage?.({ ...usage, sessionId: this.sessionId, cwd: this.launch.cwd, model: this.model ?? this.launch.selection.model });
|
|
590
|
+
} catch (error) {
|
|
591
|
+
console.warn(`Could not keep the usage of a ${this.spec.name} turn:`, errorText(error));
|
|
592
|
+
}
|
|
587
593
|
}
|
|
588
594
|
|
|
589
595
|
private emitAll(events: BackendEvent[]): void {
|
package/src/chat/acp-protocol.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isRecord, type ChatFileChange, type RuntimeMode } from '@adecore/agent-contracts';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { MODE_ORDER } from '../modes.ts';
|
|
4
|
+
import { cleanTitle } from '../title-file.ts';
|
|
4
5
|
import type { ApprovalDecision, BackendEvent } from './backend.ts';
|
|
5
6
|
|
|
6
7
|
/*
|
|
@@ -463,6 +464,13 @@ const KIND_NAMES: Record<string, string> = {
|
|
|
463
464
|
other: 'Tool'
|
|
464
465
|
};
|
|
465
466
|
|
|
467
|
+
// A session's title comes from its opening prompts; later ones are not kept.
|
|
468
|
+
const KEPT_PROMPTS = 3;
|
|
469
|
+
|
|
470
|
+
function foldSpace(text: string): string {
|
|
471
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
472
|
+
}
|
|
473
|
+
|
|
466
474
|
function str(value: unknown): string | null {
|
|
467
475
|
return typeof value === 'string' && value !== '' ? value : null;
|
|
468
476
|
}
|
|
@@ -598,6 +606,8 @@ export class AcpProtocol {
|
|
|
598
606
|
private thought: { ref: string; text: string } | null = null;
|
|
599
607
|
private counter = 0;
|
|
600
608
|
private costUsd = 0;
|
|
609
|
+
// The opening prompts of the process, whitespace folded, which an agent may hand back as the session's title.
|
|
610
|
+
private readonly prompts: string[] = [];
|
|
601
611
|
|
|
602
612
|
constructor(generation: number, modes: AcpModeMap, words: AcpStopWords) {
|
|
603
613
|
this.generation = generation;
|
|
@@ -647,13 +657,31 @@ export class AcpProtocol {
|
|
|
647
657
|
}
|
|
648
658
|
return [{ type: 'usage', contextTokens: usage.data.used, contextWindow: usage.data.size, ...(cost === undefined ? {} : { costUsd: cost }) }];
|
|
649
659
|
}
|
|
650
|
-
case 'session_info_update':
|
|
651
|
-
|
|
660
|
+
case 'session_info_update': {
|
|
661
|
+
const title = cleanTitle(update.title);
|
|
662
|
+
return title === null || this.restatesPrompt(title) ? [] : [{ type: 'title', title }];
|
|
663
|
+
}
|
|
652
664
|
default:
|
|
653
665
|
return [];
|
|
654
666
|
}
|
|
655
667
|
}
|
|
656
668
|
|
|
669
|
+
/* Notes a prompt sent to the agent, so a title that only repeats its start is not taken for a name. */
|
|
670
|
+
prompted(text: string): void {
|
|
671
|
+
if (this.prompts.length < KEPT_PROMPTS) {
|
|
672
|
+
this.prompts.push(foldSpace(text));
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/*
|
|
677
|
+
* Whether a title is only the start of a prompt, as Copilot CLI 1.0.101 names a session: with a host's instructions in
|
|
678
|
+
* front of the message that names every chat after them, and it keeps the host from naming the chat itself.
|
|
679
|
+
*/
|
|
680
|
+
private restatesPrompt(title: string): boolean {
|
|
681
|
+
const start = foldSpace(title.endsWith('…') ? title.slice(0, -1) : title);
|
|
682
|
+
return start !== '' && this.prompts.some((prompt) => prompt.startsWith(start));
|
|
683
|
+
}
|
|
684
|
+
|
|
657
685
|
modeEvent(modeId: string): BackendEvent {
|
|
658
686
|
const runtimeMode = runtimeModeOf(modeId, this.modes);
|
|
659
687
|
return { type: 'permissions', permissionMode: modeId, ...(runtimeMode === null ? {} : { effectiveRuntimeMode: runtimeMode }) };
|
package/src/chat/chat-session.ts
CHANGED
|
@@ -36,6 +36,7 @@ import { ChatThread } from './thread.ts';
|
|
|
36
36
|
import { UiReplies, type UiReplyHost } from './ui-replies.ts';
|
|
37
37
|
import { errorText } from '../error-text.ts';
|
|
38
38
|
import { systemClock, type OutboxClock } from '../outbox/outbox-worker.ts';
|
|
39
|
+
import { cleanTitle } from '../title-file.ts';
|
|
39
40
|
|
|
40
41
|
/* What a host keeps of a chat's turns in git (or anything like it), so a turn can show what it changed. */
|
|
41
42
|
export interface TurnCheckpoints {
|
|
@@ -2171,8 +2172,10 @@ export class ChatSession {
|
|
|
2171
2172
|
.catch(() => undefined);
|
|
2172
2173
|
}
|
|
2173
2174
|
|
|
2174
|
-
private applyTitle(
|
|
2175
|
-
|
|
2175
|
+
private applyTitle(raw: string): void {
|
|
2176
|
+
// Every info a client reads carries the title, so one it would refuse would leave the chat stuck there.
|
|
2177
|
+
const title = cleanTitle(raw);
|
|
2178
|
+
if (title === null || title === this.thread.info.suggestedTitle) {
|
|
2176
2179
|
return;
|
|
2177
2180
|
}
|
|
2178
2181
|
this.emit([this.thread.patchInfo({ suggestedTitle: title })]);
|