@adecore/agents 0.21.2 → 0.22.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.
- package/dist/chat/backend.d.ts +4 -0
- package/dist/chat/chat-core.d.ts +3 -0
- package/dist/chat/chat-core.js +4 -3
- package/dist/chat/chat-session.d.ts +11 -2
- package/dist/chat/chat-session.js +141 -13
- package/dist/chat/claude-protocol.d.ts +2 -0
- package/dist/chat/claude-protocol.js +21 -1
- package/dist/chat/claude-transcript.d.ts +1 -0
- package/dist/chat/claude-transcript.js +1 -1
- package/dist/chat/errors.d.ts +1 -1
- package/dist/chat/external-sessions.d.ts +26 -0
- package/dist/chat/external-sessions.js +136 -0
- package/dist/chat/fake-claude.js +73 -0
- package/dist/chat/thread-search.d.ts +14 -0
- package/dist/chat/thread-search.js +64 -0
- package/dist/host/handlers.js +1 -1
- package/dist/providers/catalog.js +2 -1
- package/dist/tasks/waiting-child.d.ts +1 -0
- package/dist/tasks/waiting-child.js +3 -3
- package/dist/usage/readers/codex.d.ts +1 -0
- package/dist/usage/readers/codex.js +1 -1
- package/package.json +3 -3
- package/src/chat/backend.ts +2 -0
- package/src/chat/chat-core.ts +4 -3
- package/src/chat/chat-session.ts +148 -13
- package/src/chat/claude-protocol.ts +23 -1
- package/src/chat/claude-transcript.ts +1 -1
- package/src/chat/errors.ts +2 -0
- package/src/chat/external-sessions.ts +177 -0
- package/src/chat/fake-claude.ts +75 -1
- package/src/chat/thread-search.ts +87 -0
- package/src/host/handlers.ts +1 -1
- package/src/providers/catalog.ts +2 -1
- package/src/tasks/waiting-child.ts +4 -4
- package/src/usage/readers/codex.ts +1 -1
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { clipText } from '@adecore/agent-contracts';
|
|
3
|
+
import { isForkOrSubagent } from '../usage/readers/codex.ts';
|
|
4
|
+
import { claudeProjectSlug, spokenText } from './claude-transcript.ts';
|
|
5
|
+
|
|
6
|
+
/* What the listing may do to the file system; a fake in tests, `node:fs` in a host. */
|
|
7
|
+
export interface SessionFs {
|
|
8
|
+
/* The names in a folder; empty when it is not there. */
|
|
9
|
+
readdir(path: string): Promise<string[]>;
|
|
10
|
+
stat(path: string): Promise<{ mtimeMs: number } | null>;
|
|
11
|
+
/* The first `maxBytes` of a file as text; the last line may be cut short. */
|
|
12
|
+
readHead(path: string, maxBytes: number): Promise<string>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* A conversation a CLI keeps on disk, started outside the app that lists it. */
|
|
16
|
+
export interface ExternalSession {
|
|
17
|
+
provider: 'claude' | 'codex';
|
|
18
|
+
sessionId: string;
|
|
19
|
+
/* The first thing the person said, on one line. */
|
|
20
|
+
title: string;
|
|
21
|
+
updatedAt: number;
|
|
22
|
+
cwd: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const TITLE_LENGTH = 120;
|
|
26
|
+
// Claude writes hook output and attachments ahead of the first prompt, and Codex its instructions.
|
|
27
|
+
const HEAD_BYTES = 256 * 1024;
|
|
28
|
+
const CLAUDE_MAX_FILES = 200;
|
|
29
|
+
|
|
30
|
+
function titleOf(text: string): string {
|
|
31
|
+
return clipText(text.replace(/\s+/g, ' ').trim(), TITLE_LENGTH);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function records(head: string): Array<Record<string, unknown>> {
|
|
35
|
+
const lines = head.split('\n');
|
|
36
|
+
// The head ends wherever the byte limit fell, so its last line may not parse.
|
|
37
|
+
return lines.flatMap((line) => {
|
|
38
|
+
if (line === '') {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const parsed: unknown = JSON.parse(line);
|
|
43
|
+
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) ? [parsed as Record<string, unknown>] : [];
|
|
44
|
+
} catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const CLAUDE_BOOKKEEPING = new Set(['summary', 'file-history-snapshot', 'queue-operation']);
|
|
51
|
+
|
|
52
|
+
/* A reminder or the wrapper of a slash command is written as a user line too, but nobody said it. */
|
|
53
|
+
function claudePrompt(entry: Record<string, unknown>): string | null {
|
|
54
|
+
if (CLAUDE_BOOKKEEPING.has(String(entry.type)) || entry.type !== 'user' || entry.isSidechain === true || entry.isMeta === true) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const message = typeof entry.message === 'object' && entry.message !== null ? (entry.message as Record<string, unknown>) : {};
|
|
58
|
+
const said = spokenText(message.content);
|
|
59
|
+
if (said === null || said.trim() === '' || said.startsWith('<command-') || said.startsWith('<local-command-')) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return said;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/*
|
|
66
|
+
* The conversations Claude Code kept for a folder, newest first. Only the folder's own directory is
|
|
67
|
+
* read, and only the `.jsonl` files in it, so the transcripts of subagents in `<session>/subagents` stay out.
|
|
68
|
+
* A file without a prompt somebody said is no conversation.
|
|
69
|
+
*/
|
|
70
|
+
export async function listClaudeSessions({ projectsDir, folder, fs }: { projectsDir: string; folder: string; fs: SessionFs }): Promise<ExternalSession[]> {
|
|
71
|
+
const dir = join(projectsDir, claudeProjectSlug(folder));
|
|
72
|
+
const names = (await fs.readdir(dir)).filter((name) => name.endsWith('.jsonl'));
|
|
73
|
+
const stats = await Promise.all(names.map(async (name) => ({ name, stat: await fs.stat(join(dir, name)) })));
|
|
74
|
+
const newest = stats
|
|
75
|
+
.flatMap(({ name, stat }) => (stat === null ? [] : [{ name, mtimeMs: stat.mtimeMs }]))
|
|
76
|
+
.sort((left, right) => right.mtimeMs - left.mtimeMs)
|
|
77
|
+
.slice(0, CLAUDE_MAX_FILES);
|
|
78
|
+
const sessions: ExternalSession[] = [];
|
|
79
|
+
for (const { name, mtimeMs } of newest) {
|
|
80
|
+
const prompt = records(await fs.readHead(join(dir, name), HEAD_BYTES))
|
|
81
|
+
.map(claudePrompt)
|
|
82
|
+
.find((said) => said !== null);
|
|
83
|
+
if (prompt !== undefined && prompt !== null) {
|
|
84
|
+
sessions.push({ provider: 'claude', sessionId: name.slice(0, -'.jsonl'.length), title: titleOf(prompt), updatedAt: mtimeMs, cwd: folder });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return sessions;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function codexPrompt(entry: Record<string, unknown>): string | null {
|
|
91
|
+
const payload = typeof entry.payload === 'object' && entry.payload !== null ? (entry.payload as Record<string, unknown>) : null;
|
|
92
|
+
if (payload === null) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
if (entry.type === 'event_msg' && payload.type === 'user_message' && typeof payload.message === 'string') {
|
|
96
|
+
return payload.message.trim() === '' ? null : payload.message;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* The newest names first; the folders are named by year, month and day, and the files by when they began. */
|
|
102
|
+
function newestFirst(names: string[]): string[] {
|
|
103
|
+
return [...names].sort().reverse();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function rolloutFiles(sessionsDir: string, fs: SessionFs, max: number): Promise<string[]> {
|
|
107
|
+
const files: string[] = [];
|
|
108
|
+
for (const year of newestFirst(await fs.readdir(sessionsDir))) {
|
|
109
|
+
for (const month of newestFirst(await fs.readdir(join(sessionsDir, year)))) {
|
|
110
|
+
for (const day of newestFirst(await fs.readdir(join(sessionsDir, year, month)))) {
|
|
111
|
+
const names = newestFirst(
|
|
112
|
+
(await fs.readdir(join(sessionsDir, year, month, day))).filter((name) => name.startsWith('rollout-') && name.endsWith('.jsonl'))
|
|
113
|
+
);
|
|
114
|
+
for (const name of names) {
|
|
115
|
+
files.push(join(sessionsDir, year, month, day, name));
|
|
116
|
+
if (files.length >= max) {
|
|
117
|
+
return files;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return files;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/*
|
|
127
|
+
* The conversations Codex kept for a folder, newest first, out of the newest `maxFiles` rollouts. A
|
|
128
|
+
* rollout counts when its first line, the session's metadata, names this folder and is no fork or subagent.
|
|
129
|
+
*/
|
|
130
|
+
export async function listCodexSessions({
|
|
131
|
+
sessionsDir,
|
|
132
|
+
folder,
|
|
133
|
+
fs,
|
|
134
|
+
maxFiles = 400
|
|
135
|
+
}: {
|
|
136
|
+
sessionsDir: string;
|
|
137
|
+
folder: string;
|
|
138
|
+
fs: SessionFs;
|
|
139
|
+
maxFiles?: number;
|
|
140
|
+
}): Promise<ExternalSession[]> {
|
|
141
|
+
const sessions: ExternalSession[] = [];
|
|
142
|
+
for (const path of await rolloutFiles(sessionsDir, fs, maxFiles)) {
|
|
143
|
+
const entries = records(await fs.readHead(path, HEAD_BYTES));
|
|
144
|
+
const meta = entries[0];
|
|
145
|
+
const payload =
|
|
146
|
+
meta?.type === 'session_meta' && typeof meta.payload === 'object' && meta.payload !== null ? (meta.payload as Record<string, unknown>) : null;
|
|
147
|
+
if (payload === null || payload.cwd !== folder || typeof payload.id !== 'string' || isForkOrSubagent(payload)) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const prompt = entries.map(codexPrompt).find((said) => said !== null);
|
|
151
|
+
if (prompt === undefined || prompt === null) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const stat = await fs.stat(path);
|
|
155
|
+
sessions.push({ provider: 'codex', sessionId: payload.id, title: titleOf(prompt), updatedAt: stat?.mtimeMs ?? 0, cwd: folder });
|
|
156
|
+
}
|
|
157
|
+
return sessions;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* A `node:fs` behind the seam, for a host that lists the real thing. */
|
|
161
|
+
export async function nodeSessionFs(): Promise<SessionFs> {
|
|
162
|
+
const { readdir, stat, open } = await import('node:fs/promises');
|
|
163
|
+
return {
|
|
164
|
+
readdir: async (path) => readdir(path).catch(() => []),
|
|
165
|
+
stat: async (path) => stat(path).catch(() => null),
|
|
166
|
+
readHead: async (path, maxBytes) => {
|
|
167
|
+
const handle = await open(path, 'r');
|
|
168
|
+
try {
|
|
169
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
170
|
+
const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
|
|
171
|
+
return buffer.subarray(0, bytesRead).toString('utf8');
|
|
172
|
+
} finally {
|
|
173
|
+
await handle.close();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
package/src/chat/fake-claude.ts
CHANGED
|
@@ -146,6 +146,40 @@ export function fakeClaude(io: FakeIo): FakeProgram {
|
|
|
146
146
|
// What a background agent asked; its answer comes in after the turn that launched it, whenever a person gives one.
|
|
147
147
|
let backgroundAsk: { requestId: string; command: string } | null = null;
|
|
148
148
|
let slow = false;
|
|
149
|
+
// The two foreground subagents of `two subagents` that have not settled; the turn stays open until both have.
|
|
150
|
+
const openTwo = new Set<string>();
|
|
151
|
+
|
|
152
|
+
const settleTwo = (name: 'a-one' | 'a-two', stopped: boolean): void => {
|
|
153
|
+
const toolUseId = name === 'a-one' ? 'toolu_two_a' : 'toolu_two_b';
|
|
154
|
+
if (!openTwo.delete(name)) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (stopped) {
|
|
158
|
+
out({ type: 'system', subtype: 'task_updated', task_id: name, patch: { status: 'killed', end_time: 0 }, session_id: sessionId });
|
|
159
|
+
out({
|
|
160
|
+
type: 'system',
|
|
161
|
+
subtype: 'task_notification',
|
|
162
|
+
task_id: name,
|
|
163
|
+
tool_use_id: toolUseId,
|
|
164
|
+
status: 'stopped',
|
|
165
|
+
output_file: '',
|
|
166
|
+
summary: 'Stopped',
|
|
167
|
+
session_id: sessionId
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
out({
|
|
171
|
+
type: 'user',
|
|
172
|
+
message: {
|
|
173
|
+
role: 'user',
|
|
174
|
+
content: [{ type: 'tool_result', tool_use_id: toolUseId, content: stopped ? 'Stopped' : `${name} done`, is_error: stopped }]
|
|
175
|
+
},
|
|
176
|
+
session_id: sessionId
|
|
177
|
+
});
|
|
178
|
+
if (openTwo.size === 0) {
|
|
179
|
+
assistantText('summarized B');
|
|
180
|
+
result();
|
|
181
|
+
}
|
|
182
|
+
};
|
|
149
183
|
|
|
150
184
|
// The rest of a background agent once its request was answered: its call's result, its report, and the turn the CLI opens about it.
|
|
151
185
|
const finishBackgroundAgent = (command: string, allowed: boolean, message: string | undefined): void => {
|
|
@@ -193,6 +227,43 @@ export function fakeClaude(io: FakeIo): FakeProgram {
|
|
|
193
227
|
io.exit(1);
|
|
194
228
|
return;
|
|
195
229
|
}
|
|
230
|
+
// Two foreground subagents beside each other; the turn ends once both settled: a-two when the test runs what is put off, a-one by a `stop_task`.
|
|
231
|
+
if (text === 'two subagents') {
|
|
232
|
+
openTwo.add('a-one').add('a-two');
|
|
233
|
+
for (const [name, toolUseId] of [
|
|
234
|
+
['a-one', 'toolu_two_a'],
|
|
235
|
+
['a-two', 'toolu_two_b']
|
|
236
|
+
] as const) {
|
|
237
|
+
out({
|
|
238
|
+
type: 'assistant',
|
|
239
|
+
message: {
|
|
240
|
+
id: `msg_${nonce}_${++messageCounter}`,
|
|
241
|
+
model,
|
|
242
|
+
role: 'assistant',
|
|
243
|
+
content: [
|
|
244
|
+
{ type: 'tool_use', id: toolUseId, name: 'Agent', input: { description: name, subagent_type: 'general-purpose', prompt: name } }
|
|
245
|
+
],
|
|
246
|
+
usage
|
|
247
|
+
},
|
|
248
|
+
session_id: sessionId
|
|
249
|
+
});
|
|
250
|
+
out({
|
|
251
|
+
type: 'system',
|
|
252
|
+
subtype: 'task_started',
|
|
253
|
+
task_id: name,
|
|
254
|
+
tool_use_id: toolUseId,
|
|
255
|
+
description: name,
|
|
256
|
+
subagent_type: 'general-purpose',
|
|
257
|
+
is_backgrounded: false,
|
|
258
|
+
spawn_depth: 1,
|
|
259
|
+
task_type: 'local_agent',
|
|
260
|
+
prompt: name,
|
|
261
|
+
session_id: sessionId
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
io.later(() => settleTwo('a-two', false));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
196
267
|
if (text === 'crash loudly') {
|
|
197
268
|
io.err(`${'warming up\n'.repeat(2000)}Error: the fake lost its key\n at handleUser (fake-claude.ts)\n`);
|
|
198
269
|
io.exit(1);
|
|
@@ -952,8 +1023,11 @@ export function fakeClaude(io: FakeIo): FakeProgram {
|
|
|
952
1023
|
} else if (frame.type === 'control_response') {
|
|
953
1024
|
handleControlResponse(frame.response as Record<string, unknown>);
|
|
954
1025
|
} else if (frame.type === 'control_request') {
|
|
955
|
-
const request = frame.request as { subtype: string };
|
|
1026
|
+
const request = frame.request as { subtype: string; task_id?: string };
|
|
956
1027
|
out({ type: 'control_response', response: { subtype: 'success', request_id: frame.request_id, response: {} } });
|
|
1028
|
+
if (request.subtype === 'stop_task' && (request.task_id === 'a-one' || request.task_id === 'a-two')) {
|
|
1029
|
+
settleTwo(request.task_id, true);
|
|
1030
|
+
}
|
|
957
1031
|
if (request.subtype === 'interrupt' && slow) {
|
|
958
1032
|
slow = false;
|
|
959
1033
|
assistantText('[interrupted]');
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { ChatItem } from '@adecore/agent-contracts';
|
|
2
|
+
|
|
3
|
+
/* One message of a thread that holds the query, cut to a line around the match. */
|
|
4
|
+
export interface ThreadHit {
|
|
5
|
+
itemId: string;
|
|
6
|
+
author: 'person' | 'agent';
|
|
7
|
+
snippet: string;
|
|
8
|
+
/* Where the match sits in `snippet`, in UTF-16 units. */
|
|
9
|
+
matchStart: number;
|
|
10
|
+
matchLength: number;
|
|
11
|
+
createdAt: number;
|
|
12
|
+
turnId: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ThreadSearchOptions {
|
|
16
|
+
limit: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Characters kept on each side of the match.
|
|
20
|
+
const CONTEXT = 60;
|
|
21
|
+
|
|
22
|
+
// Emphasis and code markers carry no meaning in a one-line snippet, where they only read as noise.
|
|
23
|
+
function plain(text: string): string {
|
|
24
|
+
return text.replace(/\*{1,3}|_{2,}|`+/g, '').replace(/\s+/g, ' ');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Moves a cut forward to the start of the next word, never past `limit`, so a snippet does not open on half a word.
|
|
28
|
+
function wordStart(text: string, from: number, limit: number): number {
|
|
29
|
+
if (from === 0 || /\s/.test(text[from - 1]!) || /\s/.test(text[from]!)) {
|
|
30
|
+
return from;
|
|
31
|
+
}
|
|
32
|
+
const space = text.slice(from, limit).search(/\s/);
|
|
33
|
+
return space === -1 ? from : from + space + 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Moves a cut back to the end of the previous word, never before `limit`.
|
|
37
|
+
function wordEnd(text: string, to: number, limit: number): number {
|
|
38
|
+
if (to === text.length || /\s/.test(text[to - 1]!) || /\s/.test(text[to]!)) {
|
|
39
|
+
return to;
|
|
40
|
+
}
|
|
41
|
+
const space = text.slice(limit, to).search(/\s\S*$/);
|
|
42
|
+
return space === -1 ? to : limit + space;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function snippetOf(text: string, index: number, length: number): { snippet: string; matchStart: number; matchLength: number } {
|
|
46
|
+
const from = wordStart(text, Math.max(0, index - CONTEXT), index);
|
|
47
|
+
const to = wordEnd(text, Math.min(text.length, index + length + CONTEXT), index + length);
|
|
48
|
+
const before = `${from > 0 ? '…' : ''}${plain(text.slice(from, index))}`;
|
|
49
|
+
const match = plain(text.slice(index, index + length));
|
|
50
|
+
const after = `${plain(text.slice(index + length, to))}${to < text.length ? '…' : ''}`;
|
|
51
|
+
return { snippet: `${before}${match}${after}`, matchStart: before.length, matchLength: match.length };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/*
|
|
55
|
+
* The messages of a thread that hold `query`, oldest first, one hit per message. Only what a person
|
|
56
|
+
* and the agent said counts: what a subagent said (`parentToolUseId`) is not part of the conversation.
|
|
57
|
+
*/
|
|
58
|
+
export function searchThreadItems(items: readonly ChatItem[], query: string, { limit }: ThreadSearchOptions): ThreadHit[] {
|
|
59
|
+
if (query === '' || limit <= 0) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
const pattern = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
|
|
63
|
+
const hits: ThreadHit[] = [];
|
|
64
|
+
for (const item of items) {
|
|
65
|
+
if (item.kind !== 'user' && item.kind !== 'assistant') {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (item.kind === 'assistant' && item.parentToolUseId) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const found = pattern.exec(item.text);
|
|
72
|
+
if (found === null) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
hits.push({
|
|
76
|
+
itemId: item.id,
|
|
77
|
+
author: item.kind === 'user' ? 'person' : 'agent',
|
|
78
|
+
...snippetOf(item.text, found.index, found[0].length),
|
|
79
|
+
createdAt: item.createdAt,
|
|
80
|
+
turnId: item.turnId
|
|
81
|
+
});
|
|
82
|
+
if (hits.length >= limit) {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return hits;
|
|
87
|
+
}
|
package/src/host/handlers.ts
CHANGED
|
@@ -63,7 +63,7 @@ export function chatHandlers(chats: ChatCore, providers: ProviderRegistry): { [T
|
|
|
63
63
|
chats.send(
|
|
64
64
|
payload.chatId,
|
|
65
65
|
payload.text,
|
|
66
|
-
{ delivery: payload.delivery, mentions: payload.mentions, skills: payload.skills, chats: payload.chats },
|
|
66
|
+
{ delivery: payload.delivery, mentions: payload.mentions, skills: payload.skills, chats: payload.chats, quoteSource: payload.quoteSource },
|
|
67
67
|
payload.attachments
|
|
68
68
|
),
|
|
69
69
|
'chat.uiChoice': (payload, clientId) => chats.choose(payload, clientId),
|
package/src/providers/catalog.ts
CHANGED
|
@@ -43,7 +43,8 @@ export class ModelCatalog {
|
|
|
43
43
|
badge: model.badge,
|
|
44
44
|
legacy: model.legacy === true,
|
|
45
45
|
isDefault: model.slug === this.data.defaultModel,
|
|
46
|
-
options: this.profileOf(model.slug)?.options ?? []
|
|
46
|
+
options: this.profileOf(model.slug)?.options ?? [],
|
|
47
|
+
...(model.minCliVersion === undefined ? {} : { minCliVersion: model.minCliVersion })
|
|
47
48
|
}));
|
|
48
49
|
}
|
|
49
50
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { clipText, type ChatApprovalItem, type ChatItem, type ChatQuestionItem, type Task } from '@adecore/agent-contracts';
|
|
1
|
+
import { clipText, WAITING_NOTE_PREFIX, type ChatApprovalItem, type ChatItem, type ChatQuestionItem, type Task } from '@adecore/agent-contracts';
|
|
2
2
|
import type { ChatCore } from '../chat/chat-core.ts';
|
|
3
3
|
import { errorText } from '../error-text.ts';
|
|
4
4
|
import type { AgentEvent } from '../events.ts';
|
|
@@ -45,7 +45,7 @@ export function chatRequests(chats: Pick<ChatCore, 'get' | 'answer'>): ChatReque
|
|
|
45
45
|
|
|
46
46
|
/* The id of the note one request of one child leaves in its parent, so delivering it twice leaves one. */
|
|
47
47
|
export function waitingNoteId(childId: string, requestId: string): string {
|
|
48
|
-
return
|
|
48
|
+
return `${WAITING_NOTE_PREFIX}${childId}-${requestId}`;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/*
|
|
@@ -185,7 +185,7 @@ export function waitingTexts(child: { id: string; title: string }, item: Waiting
|
|
|
185
185
|
export interface DeliverWaitingDeps {
|
|
186
186
|
request: ChatRequests['request'];
|
|
187
187
|
titleFor(nodeId: string): string | null;
|
|
188
|
-
deliver(chatId: string, delivery: { noteId: string; note: string; preamble: string }): Promise<boolean>;
|
|
188
|
+
deliver(chatId: string, delivery: { noteId: string; note: string; from?: string; preamble: string }): Promise<boolean>;
|
|
189
189
|
words: WaitingWords;
|
|
190
190
|
}
|
|
191
191
|
|
|
@@ -201,6 +201,6 @@ export function deliverWaitingHandler(deps: DeliverWaitingDeps) {
|
|
|
201
201
|
return;
|
|
202
202
|
}
|
|
203
203
|
const texts = waitingTexts({ id: childId, title: deps.titleFor(childId) ?? childId }, item, deps.words);
|
|
204
|
-
await deps.deliver(entry.target, { noteId: waitingNoteId(childId, requestId), ...texts });
|
|
204
|
+
await deps.deliver(entry.target, { noteId: waitingNoteId(childId, requestId), from: childId, ...texts });
|
|
205
205
|
};
|
|
206
206
|
}
|
|
@@ -79,7 +79,7 @@ function deltaOf(total: CodexCounts, previous: CodexCounts | null): CodexCounts
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/* A thread that was forked or spawned repeats its parent's events before its own work starts. */
|
|
82
|
-
function isForkOrSubagent(payload: Record<string, unknown>): boolean {
|
|
82
|
+
export function isForkOrSubagent(payload: Record<string, unknown>): boolean {
|
|
83
83
|
if (typeof payload.forked_from_id === 'string' || typeof payload.parent_thread_id === 'string') {
|
|
84
84
|
return true;
|
|
85
85
|
}
|