@adecore/agents 0.21.1 → 0.22.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/async.js +3 -8
- package/dist/chat/backend.d.ts +4 -0
- package/dist/chat/chat-core.d.ts +5 -2
- package/dist/chat/chat-core.js +8 -7
- package/dist/chat/chat-log.js +2 -2
- package/dist/chat/chat-session.d.ts +11 -2
- package/dist/chat/chat-session.js +141 -13
- package/dist/chat/chat-store.js +4 -7
- package/dist/chat/chat-title.js +4 -8
- package/dist/chat/claude-backend.d.ts +1 -0
- package/dist/chat/claude-backend.js +8 -6
- package/dist/chat/claude-protocol.d.ts +2 -0
- package/dist/chat/claude-protocol.js +21 -1
- package/dist/chat/claude-title.js +1 -1
- package/dist/chat/claude-transcript.d.ts +1 -0
- package/dist/chat/claude-transcript.js +1 -1
- package/dist/chat/codex-backend.d.ts +2 -2
- package/dist/chat/codex-backend.js +5 -8
- package/dist/chat/codex-protocol.js +1 -1
- package/dist/chat/codex-thread.js +4 -5
- package/dist/chat/composer-preferences.js +2 -3
- 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/limit-resume.js +2 -7
- package/dist/chat/thread-search.d.ts +14 -0
- package/dist/chat/thread-search.js +64 -0
- package/dist/chat/visual-store.js +8 -6
- package/dist/context/refusal.js +3 -7
- package/dist/context/verb.js +1 -5
- package/dist/host/agent-host.js +2 -2
- package/dist/host/handlers.js +1 -2
- package/dist/host/wiring.js +1 -1
- package/dist/lineage.js +2 -3
- package/dist/messages/deliver-message.js +6 -11
- package/dist/messages/deliver-notice.js +8 -12
- package/dist/messages/notice-store.js +3 -5
- package/dist/outbox/outbox-worker.js +5 -2
- package/dist/providers/accounts/service.d.ts +2 -1
- package/dist/providers/accounts/service.js +11 -13
- package/dist/providers/catalog.js +2 -1
- package/dist/providers/claude.js +0 -1
- package/dist/providers/detect.js +2 -10
- package/dist/record-directory.js +0 -1
- package/dist/tasks/task-coordinator.d.ts +1 -0
- package/dist/tasks/task-coordinator.js +34 -34
- package/dist/tasks/task-store.js +1 -1
- package/dist/tasks/task-work.js +2 -2
- package/dist/tasks/waiting-child.d.ts +1 -0
- package/dist/tasks/waiting-child.js +8 -6
- package/dist/tasks/wake-parent.js +15 -10
- package/dist/tasks/wiring.js +32 -37
- package/dist/usage/aggregate.js +62 -46
- package/dist/usage/exchange.js +1 -1
- package/dist/usage/limits/monitor.d.ts +2 -2
- package/dist/usage/limits/monitor.js +2 -2
- package/dist/usage/limits/probe.js +1 -1
- package/dist/usage/pricing.js +1 -1
- package/dist/usage/readers/codex.d.ts +1 -0
- package/dist/usage/readers/codex.js +1 -1
- package/dist/usage/roots.js +1 -2
- package/dist/usage/scanner.js +7 -9
- package/dist/usage/usage-service.d.ts +1 -2
- package/dist/usage/usage-service.js +3 -6
- package/package.json +3 -3
- package/src/async.ts +3 -9
- package/src/chat/backend.ts +2 -0
- package/src/chat/chat-core.ts +8 -7
- package/src/chat/chat-log.ts +2 -2
- package/src/chat/chat-session.ts +148 -13
- package/src/chat/chat-store.ts +4 -8
- package/src/chat/chat-title.ts +4 -8
- package/src/chat/claude-backend.ts +9 -6
- package/src/chat/claude-protocol.ts +23 -1
- package/src/chat/claude-title.ts +1 -1
- package/src/chat/claude-transcript.ts +2 -2
- package/src/chat/codex-backend.ts +5 -8
- package/src/chat/codex-protocol.ts +1 -1
- package/src/chat/codex-thread.ts +6 -5
- package/src/chat/composer-preferences.ts +2 -3
- 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/limit-resume.ts +2 -7
- package/src/chat/thread-search.ts +87 -0
- package/src/chat/thread.ts +1 -1
- package/src/chat/visual-store.ts +9 -6
- package/src/chat/wake-chat.ts +4 -1
- package/src/context/refusal.ts +3 -7
- package/src/context/verb.ts +1 -5
- package/src/host/agent-host.ts +2 -2
- package/src/host/handlers.ts +1 -2
- package/src/host/wiring.ts +1 -1
- package/src/lineage.ts +2 -3
- package/src/messages/deliver-message.ts +6 -11
- package/src/messages/deliver-notice.ts +8 -12
- package/src/messages/notice-store.ts +3 -5
- package/src/outbox/outbox-worker.ts +5 -2
- package/src/providers/accounts/service.ts +12 -13
- package/src/providers/accounts/variables.ts +1 -0
- package/src/providers/catalog.ts +2 -1
- package/src/providers/claude.ts +0 -1
- package/src/providers/detect.ts +2 -11
- package/src/record-directory.ts +0 -1
- package/src/skills.ts +0 -1
- package/src/tasks/task-coordinator.ts +35 -33
- package/src/tasks/task-store.ts +1 -1
- package/src/tasks/task-work.ts +2 -2
- package/src/tasks/waiting-child.ts +10 -7
- package/src/tasks/wake-parent.ts +21 -16
- package/src/tasks/wiring.ts +33 -41
- package/src/usage/aggregate.ts +80 -50
- package/src/usage/exchange.ts +1 -1
- package/src/usage/limits/monitor.ts +4 -4
- package/src/usage/limits/probe.ts +2 -2
- package/src/usage/pricing.ts +1 -1
- package/src/usage/readers/codex.ts +1 -1
- package/src/usage/roots.ts +1 -2
- package/src/usage/scanner.ts +8 -10
- package/src/usage/usage-service.ts +5 -9
package/dist/async.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
/* The two waits the host does: a pause it can be woken from, and a deadline on somebody else's promise. */
|
|
2
1
|
/*
|
|
3
|
-
* Resolves after `ms`, or at once when the signal is raised. An abort
|
|
4
|
-
*
|
|
5
|
-
* rejection would only be caught and thrown away.
|
|
2
|
+
* Resolves after `ms`, or at once when the signal is raised. An abort resolves instead of rejecting:
|
|
3
|
+
* every caller is a loop that checks the signal itself on its next turn.
|
|
6
4
|
*/
|
|
7
5
|
export function wait(ms, signal) {
|
|
8
6
|
return new Promise((resolve) => {
|
|
@@ -13,10 +11,7 @@ export function wait(ms, signal) {
|
|
|
13
11
|
}, { once: true });
|
|
14
12
|
});
|
|
15
13
|
}
|
|
16
|
-
/*
|
|
17
|
-
* The promise, or `message` when it takes longer than `ms`. The timer is cleared either way, so a
|
|
18
|
-
* call that answers at once does not hold the process for the rest of its deadline.
|
|
19
|
-
*/
|
|
14
|
+
/* The timer is cleared either way, so a call that answers at once does not hold the process until its deadline. */
|
|
20
15
|
export function withTimeout(promise, ms, message) {
|
|
21
16
|
return new Promise((resolve, reject) => {
|
|
22
17
|
const timer = setTimeout(() => reject(new Error(message)), ms);
|
package/dist/chat/backend.d.ts
CHANGED
package/dist/chat/chat-core.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type AgentKind, type ChatAttachment, type ChatAttachResult, type ChatAttachmentUpload, type ChatBookmark, type ChatCheckpointDiff, type ChatConfigurePayload, type ChatCreatePayload, type ChatEvent, type ChatHistoryResult, type ChatInfo, type ChatItem, type ChatQueuedMessage, type ChatUiChoicePayload, type ChatUiQueryPayload, type ChatUiLinkPayload, type ChatSkill, type ChatSubagentPayload, type ChatSubagentResult, type ChatTurnItem, type ChatVisual, type ModelSelection, type RuntimeMode } from '@adecore/agent-contracts';
|
|
2
|
-
import { type ChatUiHost } from './ui-queries.ts';
|
|
3
2
|
import { ClientSinks } from '../client-sinks.ts';
|
|
4
3
|
import type { AgentEvent, AgentSink } from '../events.ts';
|
|
5
4
|
import type { OutboxOutcome } from '../outbox/outbox-worker.ts';
|
|
@@ -8,6 +7,7 @@ import type { ProviderRegistry } from '../providers/registry.ts';
|
|
|
8
7
|
import { SkillIndex } from '../skills.ts';
|
|
9
8
|
import type { LimitsUpdate } from '../usage/limits/normalize.ts';
|
|
10
9
|
import type { AttachmentStore } from './attachment-store.ts';
|
|
10
|
+
import { type BackgroundWork } from './background-work.ts';
|
|
11
11
|
import type { BookmarkStore } from './bookmark-store.ts';
|
|
12
12
|
import type { SpawnChatProcess } from './chat-process.ts';
|
|
13
13
|
import { ChatSession, type ChatReferences, type ChatSendExtras, type PromptNotes, type ResumeWords, type TurnCheckpoints } from './chat-session.ts';
|
|
@@ -17,8 +17,8 @@ import type { CodexProcessSpec } from './codex-thread.ts';
|
|
|
17
17
|
import type { CodexClientInfo } from './codex-transport.ts';
|
|
18
18
|
import { ComposerPreferences } from './composer-preferences.ts';
|
|
19
19
|
import { DeltaCoalescer } from './delta-coalescer.ts';
|
|
20
|
-
import { type BackgroundWork } from './background-work.ts';
|
|
21
20
|
import { SubagentReader, type SubagentReaderOptions } from './subagent-reader.ts';
|
|
21
|
+
import { type ChatUiHost } from './ui-queries.ts';
|
|
22
22
|
import type { VisualInput, VisualStore } from './visual-store.ts';
|
|
23
23
|
export interface InterruptedRun {
|
|
24
24
|
chatId: string;
|
|
@@ -113,6 +113,9 @@ export declare class ChatCore {
|
|
|
113
113
|
values?: Record<string, unknown> | undefined;
|
|
114
114
|
at: number;
|
|
115
115
|
} | undefined;
|
|
116
|
+
quoteSource?: {
|
|
117
|
+
itemId: string;
|
|
118
|
+
} | undefined;
|
|
116
119
|
}[] | undefined;
|
|
117
120
|
queuePaused?: boolean | undefined;
|
|
118
121
|
background?: {
|
package/dist/chat/chat-core.js
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { ChatAttachmentUploadsSchema, generatedImageAttachment } from '@adecore/agent-contracts';
|
|
4
|
-
import { ChatUiQueries } from './ui-queries.js';
|
|
5
4
|
import { ClientSinks } from '../client-sinks.js';
|
|
6
5
|
import { errorText } from '../error-text.js';
|
|
7
6
|
import { AccountError, definedEnv, isDefaultAccountOf, launchEnv, storedAccount } from '../providers/accounts/launch.js';
|
|
8
7
|
import { SkillIndex } from '../skills.js';
|
|
9
8
|
import { usageRoots } from '../usage/roots.js';
|
|
9
|
+
import { commandLabel, isBackgroundWork, runsInBackground } from './background-work.js';
|
|
10
10
|
import { ChatLog, COMPACT_ABOVE_BYTES } from './chat-log.js';
|
|
11
11
|
import { ChatSession } from './chat-session.js';
|
|
12
12
|
import { ComposerPreferences } from './composer-preferences.js';
|
|
13
13
|
import { DeltaCoalescer } from './delta-coalescer.js';
|
|
14
|
-
import { readableAssistantText } from './readable-text.js';
|
|
15
14
|
import { ChatError } from './errors.js';
|
|
16
|
-
import {
|
|
15
|
+
import { readableAssistantText } from './readable-text.js';
|
|
17
16
|
import { SubagentReader } from './subagent-reader.js';
|
|
18
|
-
|
|
17
|
+
import { ChatUiQueries } from './ui-queries.js';
|
|
19
18
|
function withoutHidden(info) {
|
|
20
19
|
const { hidden: _hidden, ...rest } = info;
|
|
21
20
|
return rest;
|
|
22
21
|
}
|
|
22
|
+
/* A message a person or the agent wrote in the chat's own thread; a subagent's words belong to its row. */
|
|
23
23
|
function isBookmarkable(item) {
|
|
24
24
|
return item.kind === 'user' || (item.kind === 'assistant' && (item.parentToolUseId ?? null) === null);
|
|
25
25
|
}
|
|
@@ -486,8 +486,9 @@ export class ChatCore {
|
|
|
486
486
|
return result;
|
|
487
487
|
}
|
|
488
488
|
/*
|
|
489
|
-
* Stops one running sub-agent of the CLI's own.
|
|
490
|
-
*
|
|
489
|
+
* Stops one running sub-agent of the CLI's own. The process that started it ends it with `stop_task`
|
|
490
|
+
* and the parent keeps working. Without such a process the row is only marked, and only while no
|
|
491
|
+
* turn runs: stopping that turn is the composer's Stop.
|
|
491
492
|
*/
|
|
492
493
|
async stopSubagent(chatId, toolUseId) {
|
|
493
494
|
await this.loaded(chatId);
|
|
@@ -502,7 +503,7 @@ export class ChatCore {
|
|
|
502
503
|
if (row.origin === 'ruimte') {
|
|
503
504
|
throw new ChatError('chat-unsupported', 'This row stands for work the host runs; stop it there');
|
|
504
505
|
}
|
|
505
|
-
session.
|
|
506
|
+
session.stopSubagent(toolUseId);
|
|
506
507
|
}
|
|
507
508
|
/* Ends a command or a monitor a chat's CLI runs in the background, for a person who pressed Stop on it. */
|
|
508
509
|
async stopTask(chatId, taskId) {
|
package/dist/chat/chat-log.js
CHANGED
|
@@ -31,8 +31,8 @@ export function parseLog(raw) {
|
|
|
31
31
|
return lines;
|
|
32
32
|
}
|
|
33
33
|
/*
|
|
34
|
-
*
|
|
35
|
-
* one order. Recent lines stay in memory
|
|
34
|
+
* Numbers every broadcast event and appends it synchronously, so disk, memory and snapshot boundaries
|
|
35
|
+
* keep one order. Recent lines stay in memory to answer an attach with `since` at once.
|
|
36
36
|
*/
|
|
37
37
|
export class ChatLog {
|
|
38
38
|
path;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChatAttachment, ChatAssistantItem, ChatCheckpointDiff, ChatEvent, ChatInfo, ChatItem, ChatQueuedMessage, ChatSkill, ChatUiChoiceOrigin, ChatUiChoicePayload, ChatUiQueryState, ModelSelection, RuntimeMode } from '@adecore/agent-contracts';
|
|
1
|
+
import type { ChatAttachment, ChatAssistantItem, ChatCheckpointDiff, ChatEvent, ChatInfo, ChatItem, ChatQueuedMessage, ChatQuoteSource, ChatSkill, ChatUiChoiceOrigin, ChatUiChoicePayload, ChatUiQueryState, ModelSelection, RuntimeMode } from '@adecore/agent-contracts';
|
|
2
2
|
import type { UiCompileOptions } from '@adecore/intelligent-ui/compiler';
|
|
3
3
|
import type { ChatProvider } from '../providers/provider.ts';
|
|
4
4
|
import type { LimitsUpdate } from '../usage/limits/normalize.ts';
|
|
@@ -15,6 +15,7 @@ export interface TurnCheckpoints {
|
|
|
15
15
|
diff: ChatCheckpointDiff;
|
|
16
16
|
after: string;
|
|
17
17
|
} | null>;
|
|
18
|
+
head?(cwd: string): Promise<string | null>;
|
|
18
19
|
}
|
|
19
20
|
export interface PromptNotes {
|
|
20
21
|
next(): {
|
|
@@ -72,6 +73,7 @@ export interface ChatSendExtras {
|
|
|
72
73
|
mentions?: string[];
|
|
73
74
|
skills?: string[];
|
|
74
75
|
chats?: string[];
|
|
76
|
+
quoteSource?: ChatQuoteSource;
|
|
75
77
|
attachments?: ChatAttachment[];
|
|
76
78
|
}
|
|
77
79
|
export interface ResumeWords {
|
|
@@ -101,6 +103,8 @@ export declare class ChatSession {
|
|
|
101
103
|
private titleTimer;
|
|
102
104
|
private naming;
|
|
103
105
|
private frozen;
|
|
106
|
+
private readonly subagentOwners;
|
|
107
|
+
private readonly stopping;
|
|
104
108
|
private readonly orphans;
|
|
105
109
|
private pendingPreambles;
|
|
106
110
|
private submission;
|
|
@@ -127,7 +131,7 @@ export declare class ChatSession {
|
|
|
127
131
|
resumeAtReset?: boolean;
|
|
128
132
|
}): ChatInfo;
|
|
129
133
|
setAccount(account: string | undefined): ChatInfo;
|
|
130
|
-
send(text: string,
|
|
134
|
+
send(text: string, sent?: ChatSendExtras): {
|
|
131
135
|
queued: boolean;
|
|
132
136
|
turnId: string;
|
|
133
137
|
};
|
|
@@ -140,6 +144,7 @@ export declare class ChatSession {
|
|
|
140
144
|
turnId: string;
|
|
141
145
|
};
|
|
142
146
|
private recordUiAnswer;
|
|
147
|
+
private vetted;
|
|
143
148
|
sendInput(text: string, extras?: ChatSendExtras): Promise<{
|
|
144
149
|
queued: boolean;
|
|
145
150
|
turnId: string;
|
|
@@ -199,6 +204,10 @@ export declare class ChatSession {
|
|
|
199
204
|
}): Promise<unknown> | null;
|
|
200
205
|
settleOrphanedSubagents(): Promise<void>;
|
|
201
206
|
private recordLostBackground;
|
|
207
|
+
stopSubagent(toolUseId: string): 'stopping' | 'marked';
|
|
208
|
+
private trackSubagentOwner;
|
|
209
|
+
private refuseStop;
|
|
210
|
+
private noteStopped;
|
|
202
211
|
markSubagentStopped(toolUseId: string): void;
|
|
203
212
|
markSubagentsStopped(): void;
|
|
204
213
|
private markStopped;
|
|
@@ -70,6 +70,10 @@ export class ChatSession {
|
|
|
70
70
|
naming = false;
|
|
71
71
|
// Set while the host goes down: a CLI dying with it must not end the turn a restart takes up again.
|
|
72
72
|
frozen = false;
|
|
73
|
+
// Which CLI process (generation) announced each subagent of its own, by call id; only that one can stop it.
|
|
74
|
+
subagentOwners = new Map();
|
|
75
|
+
// Subagents a stop_task went out for, as task id to call id, until their notification settles them.
|
|
76
|
+
stopping = new Map();
|
|
73
77
|
// Background subagents a load found running whose transcript did not show an end yet; no CLI here will report them.
|
|
74
78
|
orphans = new Set();
|
|
75
79
|
pendingPreambles;
|
|
@@ -167,7 +171,8 @@ export class ChatSession {
|
|
|
167
171
|
/*
|
|
168
172
|
* Internal follow-ups wait for a new turn. User input goes through `sendInput`, which can steer native work.
|
|
169
173
|
*/
|
|
170
|
-
send(text,
|
|
174
|
+
send(text, sent = {}) {
|
|
175
|
+
const extras = this.vetted(sent);
|
|
171
176
|
const turnId = newId('turn');
|
|
172
177
|
// Behind whatever still waits (a CLI that crashed leaves its queue), so everything goes out in the order it was sent.
|
|
173
178
|
if (this.busy || this.queue.length > 0 || this.replacementWaits()) {
|
|
@@ -183,6 +188,7 @@ export class ChatSession {
|
|
|
183
188
|
...(extras.mentions?.length ? { mentions: extras.mentions } : {}),
|
|
184
189
|
...(extras.skills?.length ? { skills: extras.skills } : {}),
|
|
185
190
|
...(extras.chats?.length ? { chats: extras.chats } : {}),
|
|
191
|
+
...(extras.quoteSource ? { quoteSource: extras.quoteSource } : {}),
|
|
186
192
|
...(extras.attachments?.length ? { attachments: extras.attachments } : {})
|
|
187
193
|
};
|
|
188
194
|
this.setQueue([...this.queue, message]);
|
|
@@ -278,10 +284,20 @@ export class ChatSession {
|
|
|
278
284
|
this.options.emit(this.thread.upsert({ ...item, uiAnswers: { ...item.uiAnswers, [answer.blockId]: answer } }));
|
|
279
285
|
this.options.persist();
|
|
280
286
|
}
|
|
287
|
+
/* A quote whose source is no reply of this thread is sent as the plain text it is; the message itself is never refused for it. */
|
|
288
|
+
vetted(extras) {
|
|
289
|
+
if (extras.quoteSource === undefined || this.thread.get(extras.quoteSource.itemId)?.kind === 'assistant') {
|
|
290
|
+
return extras;
|
|
291
|
+
}
|
|
292
|
+
const plain = { ...extras };
|
|
293
|
+
delete plain.quoteSource;
|
|
294
|
+
return plain;
|
|
295
|
+
}
|
|
281
296
|
sendInput(text, extras = {}) {
|
|
282
297
|
return this.submitInput(text, extras);
|
|
283
298
|
}
|
|
284
|
-
async submitInput(text,
|
|
299
|
+
async submitInput(text, sent) {
|
|
300
|
+
const extras = this.vetted(sent);
|
|
285
301
|
const backend = this.backend;
|
|
286
302
|
const turnId = this.info.activeTurnId;
|
|
287
303
|
if (extras.delivery !== 'steer' ||
|
|
@@ -323,7 +339,8 @@ export class ChatSession {
|
|
|
323
339
|
mentions: extras.mentions,
|
|
324
340
|
skills: extras.skills,
|
|
325
341
|
chats: references.ids,
|
|
326
|
-
attachments: extras.attachments
|
|
342
|
+
attachments: extras.attachments,
|
|
343
|
+
...(extras.quoteSource ? { quoteSource: extras.quoteSource } : {})
|
|
327
344
|
}),
|
|
328
345
|
this.thread.patchInfo({ queue: this.queue.filter((entry) => entry.id !== message.id) })
|
|
329
346
|
]);
|
|
@@ -406,7 +423,14 @@ export class ChatSession {
|
|
|
406
423
|
this.sendAfterReplacement = false;
|
|
407
424
|
this.pauseQueue(false);
|
|
408
425
|
this.setQueue(rest);
|
|
409
|
-
this.dispatch(next.text, {
|
|
426
|
+
this.dispatch(next.text, {
|
|
427
|
+
mentions: next.mentions,
|
|
428
|
+
skills: next.skills,
|
|
429
|
+
chats: next.chats,
|
|
430
|
+
attachments: next.attachments,
|
|
431
|
+
uiChoice: next.uiChoice,
|
|
432
|
+
quoteSource: next.quoteSource
|
|
433
|
+
}, next.turnId);
|
|
410
434
|
}
|
|
411
435
|
dispatch(text, extras, requestedTurnId) {
|
|
412
436
|
this.pauseQueue(false);
|
|
@@ -1016,6 +1040,96 @@ export class ChatSession {
|
|
|
1016
1040
|
const text = `The machine restarted before background work "${label}" completed. It will not send a result. Do not wait for it.`;
|
|
1017
1041
|
this.deliverNote({ noteId: `lost-background-${item.id}`, note: text, preamble: text });
|
|
1018
1042
|
}
|
|
1043
|
+
/*
|
|
1044
|
+
* Stops one subagent of the CLI's own. The process that announced it ends it with `stop_task` and the
|
|
1045
|
+
* parent goes on; a process that did not start it has nothing to address, so while a turn runs that
|
|
1046
|
+
* is refused, and otherwise the row is only marked.
|
|
1047
|
+
*/
|
|
1048
|
+
stopSubagent(toolUseId) {
|
|
1049
|
+
const row = this.thread.find('subagent', (item) => item.toolUseId === toolUseId);
|
|
1050
|
+
const agentId = row?.native?.agentId;
|
|
1051
|
+
const owned = this.subagentOwners.get(toolUseId) === this.generation;
|
|
1052
|
+
if (agentId !== undefined && owned && this.backend?.stopTask) {
|
|
1053
|
+
this.stopping.set(agentId, toolUseId);
|
|
1054
|
+
this.backend.stopTask(agentId);
|
|
1055
|
+
return 'stopping';
|
|
1056
|
+
}
|
|
1057
|
+
if (agentId !== undefined && !owned && this.thread.info.activeTurnId !== null) {
|
|
1058
|
+
throw new ChatError('subagent-stale', 'This sub-agent belongs to a process that has ended');
|
|
1059
|
+
}
|
|
1060
|
+
this.markSubagentStopped(toolUseId);
|
|
1061
|
+
return 'marked';
|
|
1062
|
+
}
|
|
1063
|
+
trackSubagentOwner(generation, event) {
|
|
1064
|
+
if (event.type === 'task.started' && event.taskId) {
|
|
1065
|
+
this.subagentOwners.set(event.ref, generation);
|
|
1066
|
+
}
|
|
1067
|
+
else if (event.type === 'task.done') {
|
|
1068
|
+
this.noteStopped(event);
|
|
1069
|
+
if (event.ref !== null) {
|
|
1070
|
+
this.subagentOwners.delete(event.ref);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
else if (event.type === 'exit') {
|
|
1074
|
+
this.stopping.clear();
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
/* The CLI would not stop it: with no turn running the row is only marked, and while one runs a note says it still goes on. */
|
|
1078
|
+
refuseStop(agentId, message) {
|
|
1079
|
+
const toolUseId = this.stopping.get(agentId);
|
|
1080
|
+
if (toolUseId === undefined) {
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
this.stopping.delete(agentId);
|
|
1084
|
+
const row = this.thread.find('subagent', (item) => item.toolUseId === toolUseId);
|
|
1085
|
+
if (row?.status !== 'running') {
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
if (this.thread.info.activeTurnId === null) {
|
|
1089
|
+
this.markSubagentStopped(toolUseId);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
this.emit([
|
|
1093
|
+
this.thread.upsert({
|
|
1094
|
+
id: `stop-refused-${toolUseId}-${Date.now()}`,
|
|
1095
|
+
kind: 'note',
|
|
1096
|
+
createdAt: Date.now(),
|
|
1097
|
+
turnId: null,
|
|
1098
|
+
level: 'warning',
|
|
1099
|
+
text: `"${row.description || row.subagentType || 'Sub-agent'}" could not be stopped: ${message}`
|
|
1100
|
+
})
|
|
1101
|
+
]);
|
|
1102
|
+
this.options.persist();
|
|
1103
|
+
}
|
|
1104
|
+
/* The line under a subagent that `stop_task` ended, once the CLI reported it settled. */
|
|
1105
|
+
noteStopped(event) {
|
|
1106
|
+
const stopped = [...this.stopping].find(([agentId, toolUseId]) => agentId === event.taskId || toolUseId === event.ref);
|
|
1107
|
+
if (stopped === undefined) {
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
this.stopping.delete(stopped[0]);
|
|
1111
|
+
// It finished on its own just before the stop arrived; the row already says so.
|
|
1112
|
+
if (event.ok) {
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
const row = this.thread.find('subagent', (item) => item.toolUseId === stopped[1]);
|
|
1116
|
+
if (!row) {
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
const now = Date.now();
|
|
1120
|
+
this.emit([
|
|
1121
|
+
...(row.status === 'running' ? [this.thread.upsert({ ...row, status: 'failed', finishedAt: now })] : []),
|
|
1122
|
+
this.thread.upsert({
|
|
1123
|
+
id: `stopped-${row.toolUseId}`,
|
|
1124
|
+
kind: 'note',
|
|
1125
|
+
createdAt: now,
|
|
1126
|
+
turnId: null,
|
|
1127
|
+
level: 'info',
|
|
1128
|
+
text: `"${row.description || row.subagentType || 'Sub-agent'}" was stopped.`
|
|
1129
|
+
})
|
|
1130
|
+
]);
|
|
1131
|
+
this.options.persist();
|
|
1132
|
+
}
|
|
1019
1133
|
/*
|
|
1020
1134
|
* Marks a subagent of the CLI's own as stopped: `failed` with a note that says what it means, since
|
|
1021
1135
|
* no CLI stops one subagent on its own. Refused while a turn runs, which may still be waiting on it.
|
|
@@ -1121,7 +1235,8 @@ export class ChatSession {
|
|
|
1121
1235
|
skills,
|
|
1122
1236
|
chats,
|
|
1123
1237
|
attachments,
|
|
1124
|
-
...(extras.uiChoice ? { uiChoice: extras.uiChoice } : {})
|
|
1238
|
+
...(extras.uiChoice ? { uiChoice: extras.uiChoice } : {}),
|
|
1239
|
+
...(extras.quoteSource ? { quoteSource: extras.quoteSource } : {})
|
|
1125
1240
|
}));
|
|
1126
1241
|
}
|
|
1127
1242
|
events.push(this.thread.patchInfo({ status: this.thread.statusFor(turnId), activeTurnId: turnId }));
|
|
@@ -1155,14 +1270,14 @@ export class ChatSession {
|
|
|
1155
1270
|
if (!checkpoints) {
|
|
1156
1271
|
return Promise.resolve();
|
|
1157
1272
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
.then((tree) => {
|
|
1273
|
+
const cwd = this.thread.info.cwd;
|
|
1274
|
+
return Promise.all([checkpoints.take(cwd), checkpoints.head?.(cwd) ?? null])
|
|
1275
|
+
.then(([tree, head]) => {
|
|
1161
1276
|
const turn = this.thread.get(turnId);
|
|
1162
1277
|
if (tree === null || turn?.kind !== 'turn' || turn.state !== 'running' || this.info.activeTurnId !== turnId) {
|
|
1163
1278
|
return;
|
|
1164
1279
|
}
|
|
1165
|
-
this.emit([this.thread.upsert({ ...turn, checkpoint: tree })]);
|
|
1280
|
+
this.emit([this.thread.upsert({ ...turn, checkpoint: tree, ...(head === null ? {} : { checkpointHead: head }) })]);
|
|
1166
1281
|
})
|
|
1167
1282
|
.catch(() => undefined);
|
|
1168
1283
|
}
|
|
@@ -1172,14 +1287,22 @@ export class ChatSession {
|
|
|
1172
1287
|
if (turn?.kind !== 'turn' || turn.checkpoint === undefined || turn.checkpointDiff || !this.options.checkpoints) {
|
|
1173
1288
|
return;
|
|
1174
1289
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1290
|
+
const { checkpoints } = this.options;
|
|
1291
|
+
const cwd = this.thread.info.cwd;
|
|
1292
|
+
void Promise.all([checkpoints.settle(cwd, turn.checkpoint), checkpoints.head?.(cwd) ?? null])
|
|
1293
|
+
.then(([answer, head]) => {
|
|
1178
1294
|
const settled = this.thread.get(turnId);
|
|
1179
1295
|
if (answer === null || settled?.kind !== 'turn') {
|
|
1180
1296
|
return;
|
|
1181
1297
|
}
|
|
1182
|
-
this.emit([
|
|
1298
|
+
this.emit([
|
|
1299
|
+
this.thread.upsert({
|
|
1300
|
+
...settled,
|
|
1301
|
+
checkpointDiff: answer.diff,
|
|
1302
|
+
checkpointAfter: answer.after,
|
|
1303
|
+
...(head === null ? {} : { checkpointHeadAfter: head })
|
|
1304
|
+
})
|
|
1305
|
+
]);
|
|
1183
1306
|
this.options.persist();
|
|
1184
1307
|
})
|
|
1185
1308
|
.catch(() => undefined);
|
|
@@ -1409,6 +1532,10 @@ export class ChatSession {
|
|
|
1409
1532
|
this.applyTitle(event.title);
|
|
1410
1533
|
return;
|
|
1411
1534
|
}
|
|
1535
|
+
if (event.type === 'stop.refused') {
|
|
1536
|
+
this.refuseStop(event.taskId, event.message);
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1412
1539
|
/*
|
|
1413
1540
|
* A CLI reports the window it was started on. A pick made since waits for the restart the next
|
|
1414
1541
|
* send does, so its report may not put the old size back under a meter that already reads the new one.
|
|
@@ -1466,6 +1593,7 @@ export class ChatSession {
|
|
|
1466
1593
|
this.awaitingBackgroundReport = true;
|
|
1467
1594
|
}
|
|
1468
1595
|
this.emit(projected);
|
|
1596
|
+
this.trackSubagentOwner(generation, event);
|
|
1469
1597
|
this.watchWorkflows(event);
|
|
1470
1598
|
if (isMainAgentOutput(event)) {
|
|
1471
1599
|
this.spokenTurnId = this.thread.info.activeTurnId;
|
package/dist/chat/chat-store.js
CHANGED
|
@@ -4,12 +4,12 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { ChatInfoSchema, ChatItemSchema } from '@adecore/agent-contracts';
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { isNotFound, writeAtomic, writeAtomicSync } from '../fs.js';
|
|
7
|
-
import {
|
|
7
|
+
import { recordFileName } from '../record-directory.js';
|
|
8
8
|
import { migrateInlineAttachments } from './attachment-store.js';
|
|
9
|
+
import { isBookmarkFileName } from './bookmark-store.js';
|
|
9
10
|
import { parseLog } from './chat-log.js';
|
|
10
11
|
import { ChatThread } from './thread.js';
|
|
11
12
|
import { isVisualFileName } from './visual-store.js';
|
|
12
|
-
import { recordFileName } from '../record-directory.js';
|
|
13
13
|
// zod strips what it does not know inside the info and the items, so a file written by an older build
|
|
14
14
|
// (with `interactionMode`, say) still parses and loses only the dropped field. A record without `seq`
|
|
15
15
|
// predates the log. Keys beside these are the host's own, and come back to it as they were written.
|
|
@@ -24,7 +24,7 @@ const RecordSchema = z.looseObject({
|
|
|
24
24
|
uiAccess: z.record(z.string(), z.unknown()).optional()
|
|
25
25
|
});
|
|
26
26
|
const OWN_KEYS = new Set(['info', 'items', 'seq', 'resetSeq', 'preambles', 'uiAccess']);
|
|
27
|
-
function
|
|
27
|
+
function withoutOwnKeys(record) {
|
|
28
28
|
return Object.fromEntries(Object.entries(record).filter(([key]) => !OWN_KEYS.has(key)));
|
|
29
29
|
}
|
|
30
30
|
function logName(chatId) {
|
|
@@ -54,9 +54,6 @@ function liftUiAccess(record) {
|
|
|
54
54
|
}
|
|
55
55
|
return Object.keys(uiAccess).length === 0 ? record : { ...record, uiAccess };
|
|
56
56
|
}
|
|
57
|
-
function withoutOwnKeys(extras) {
|
|
58
|
-
return Object.fromEntries(Object.entries(extras).filter(([key]) => !OWN_KEYS.has(key)));
|
|
59
|
-
}
|
|
60
57
|
/*
|
|
61
58
|
* One JSON file per chat under `<home>/chats`, written while a turn runs and on shutdown, with
|
|
62
59
|
* the log of what happened since beside it (`ChatLog` appends to it; reading plays it back).
|
|
@@ -146,7 +143,7 @@ export class ChatStore {
|
|
|
146
143
|
resetSeq,
|
|
147
144
|
preambles: snapshot.preambles ?? [],
|
|
148
145
|
uiAccess: snapshot.uiAccess ?? {},
|
|
149
|
-
extras:
|
|
146
|
+
extras: withoutOwnKeys(snapshot),
|
|
150
147
|
lines
|
|
151
148
|
};
|
|
152
149
|
}
|
package/dist/chat/chat-title.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { cleanTitle } from '../title-file.js';
|
|
2
1
|
import { runProcess } from '../run-process.js';
|
|
2
|
+
import { cleanTitle } from '../title-file.js';
|
|
3
3
|
// Enough of a conversation to name it; the rest of a long prompt or answer says nothing a title needs.
|
|
4
4
|
const MAX_PROMPT_CHARS = 2000;
|
|
5
5
|
const MAX_ANSWER_CHARS = 1500;
|
|
@@ -8,10 +8,7 @@ const TIMEOUT_MS = 45_000;
|
|
|
8
8
|
function cap(text, limit) {
|
|
9
9
|
return text.length <= limit ? text : `${text.slice(0, limit)}...`;
|
|
10
10
|
}
|
|
11
|
-
/*
|
|
12
|
-
* What the CLI is asked. JSON again, as for a commit message: a model left to write freely opens
|
|
13
|
-
* with a sentence, and that sentence would become the name of the node.
|
|
14
|
-
*/
|
|
11
|
+
/* JSON, since a model left to write freely opens with a sentence, and that sentence would become the name. */
|
|
15
12
|
export function buildTitlePrompt(prompt, answer) {
|
|
16
13
|
return [
|
|
17
14
|
'Write a short title for the conversation below, as a person would name it in a list of chats.',
|
|
@@ -30,9 +27,8 @@ export function buildTitlePrompt(prompt, answer) {
|
|
|
30
27
|
].join('\n');
|
|
31
28
|
}
|
|
32
29
|
/*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* is better than a stray line of it.
|
|
30
|
+
* Strict on purpose: a run that holds no `{"title": "..."}` object printed something other than a
|
|
31
|
+
* title, and a derived name is better than a stray line of it.
|
|
36
32
|
*/
|
|
37
33
|
export function parseTitle(output) {
|
|
38
34
|
const start = [...output.matchAll(/\{\s*"title"/g)].at(-1)?.index;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { errorText } from '../error-text.js';
|
|
2
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { errorText } from '../error-text.js';
|
|
3
3
|
import { claudeArgs, claudeEnv, promptPrefix } from '../providers/claude.js';
|
|
4
4
|
import { ChatChild } from './chat-process.js';
|
|
5
5
|
import { ClaudeProtocol } from './claude-protocol.js';
|
|
@@ -59,12 +59,10 @@ export class ClaudeBackend {
|
|
|
59
59
|
return Promise.resolve();
|
|
60
60
|
}
|
|
61
61
|
sendTurn(input) {
|
|
62
|
-
// The prefix is what the CLI must see first (ultrathink), then the note about the links, then what was typed.
|
|
63
|
-
const prefix = `${promptPrefix(this.launch.selection)}${input.preamble === null ? '' : `${input.preamble}\n\n`}`;
|
|
64
62
|
const promptId = input.promptId ?? randomUUID();
|
|
65
63
|
this.active = true;
|
|
66
64
|
this.protocol.beginPrompt(promptId);
|
|
67
|
-
this.write(
|
|
65
|
+
this.write(this.userMessage(input, promptId));
|
|
68
66
|
}
|
|
69
67
|
steerTurn(input) {
|
|
70
68
|
if (!this.child || !this.active || this.stdinClosed) {
|
|
@@ -75,8 +73,7 @@ export class ClaudeBackend {
|
|
|
75
73
|
this.steers.set(promptId, { resolve, reject });
|
|
76
74
|
this.protocol.beginSteer(promptId);
|
|
77
75
|
try {
|
|
78
|
-
|
|
79
|
-
this.write(buildUserMessage({ text: input.text, attachments: input.attachments, prefix, skills: input.skills, promptId }));
|
|
76
|
+
this.write(this.userMessage(input, promptId));
|
|
80
77
|
}
|
|
81
78
|
catch (error) {
|
|
82
79
|
this.steers.delete(promptId);
|
|
@@ -126,6 +123,11 @@ export class ClaudeBackend {
|
|
|
126
123
|
this.child = null;
|
|
127
124
|
return child?.end() ?? Promise.resolve();
|
|
128
125
|
}
|
|
126
|
+
userMessage(input, promptId) {
|
|
127
|
+
// What the CLI must see first (ultrathink), then the preamble, then what was typed.
|
|
128
|
+
const prefix = `${promptPrefix(this.launch.selection)}${input.preamble === null ? '' : `${input.preamble}\n\n`}`;
|
|
129
|
+
return buildUserMessage({ text: input.text, attachments: input.attachments, prefix, skills: input.skills, promptId });
|
|
130
|
+
}
|
|
129
131
|
async readLines(child) {
|
|
130
132
|
const reader = child.process.stdout.getReader();
|
|
131
133
|
const decoder = new TextDecoder();
|
|
@@ -15,6 +15,7 @@ export declare class ClaudeProtocol {
|
|
|
15
15
|
private readonly subagentModels;
|
|
16
16
|
private apiError;
|
|
17
17
|
private readonly refusedWindows;
|
|
18
|
+
private readonly stopRequests;
|
|
18
19
|
private readonly steerIds;
|
|
19
20
|
beginSteer(promptId: string): void;
|
|
20
21
|
beginPrompt(promptId: string): void;
|
|
@@ -30,6 +31,7 @@ export declare class ClaudeProtocol {
|
|
|
30
31
|
private endBackground;
|
|
31
32
|
private endWorkflow;
|
|
32
33
|
stopTaskRequest(taskId: string): unknown;
|
|
34
|
+
private handleControlResponse;
|
|
33
35
|
private handleStreamEvent;
|
|
34
36
|
private handleAssistant;
|
|
35
37
|
private handleToolProgress;
|
|
@@ -117,6 +117,8 @@ export class ClaudeProtocol {
|
|
|
117
117
|
// Only main-chain errors classify the turn; each plan window can refuse it independently.
|
|
118
118
|
apiError = null;
|
|
119
119
|
refusedWindows = new Map();
|
|
120
|
+
// The task each `stop_task` request named, so an error answer finds it again.
|
|
121
|
+
stopRequests = new Map();
|
|
120
122
|
steerIds = new Set();
|
|
121
123
|
beginSteer(promptId) {
|
|
122
124
|
this.steerIds.add(promptId);
|
|
@@ -152,6 +154,9 @@ export class ClaudeProtocol {
|
|
|
152
154
|
case 'control_request':
|
|
153
155
|
this.handleControlRequest(frame, events);
|
|
154
156
|
break;
|
|
157
|
+
case 'control_response':
|
|
158
|
+
this.handleControlResponse(frame, events);
|
|
159
|
+
break;
|
|
155
160
|
case 'control_cancel_request': {
|
|
156
161
|
const requestId = str(frame.request_id);
|
|
157
162
|
if (requestId) {
|
|
@@ -408,7 +413,22 @@ export class ClaudeProtocol {
|
|
|
408
413
|
}
|
|
409
414
|
/* The control request that ends one task the CLI runs beside its turns. */
|
|
410
415
|
stopTaskRequest(taskId) {
|
|
411
|
-
|
|
416
|
+
const requestId = `stop-task-${taskId}-${Date.now()}`;
|
|
417
|
+
this.stopRequests.set(requestId, taskId);
|
|
418
|
+
return { type: 'control_request', request_id: requestId, request: { subtype: 'stop_task', task_id: taskId } };
|
|
419
|
+
}
|
|
420
|
+
/* Only a refused `stop_task` matters; every other answer is the CLI saying it took the request. */
|
|
421
|
+
handleControlResponse(frame, events) {
|
|
422
|
+
const response = isRecord(frame.response) ? frame.response : null;
|
|
423
|
+
const requestId = str(response?.request_id);
|
|
424
|
+
const taskId = requestId === null ? undefined : this.stopRequests.get(requestId);
|
|
425
|
+
if (response === null || requestId === null || taskId === undefined) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
this.stopRequests.delete(requestId);
|
|
429
|
+
if (response.subtype === 'error') {
|
|
430
|
+
events.push({ type: 'stop.refused', taskId, message: str(response.error) ?? 'The CLI refused to stop it' });
|
|
431
|
+
}
|
|
412
432
|
}
|
|
413
433
|
handleStreamEvent(frame, events) {
|
|
414
434
|
const event = frame.event;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { readdir, stat } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { usageRoots } from '../usage/roots.js';
|
|
5
4
|
import { cleanTitle, readLines } from '../title-file.js';
|
|
5
|
+
import { usageRoots } from '../usage/roots.js';
|
|
6
6
|
// The two records carry these words; a line without them is never parsed.
|
|
7
7
|
const MARKERS = ['"ai-title"', '"custom-title"'];
|
|
8
8
|
/*
|
|
@@ -12,7 +12,7 @@ function str(value) {
|
|
|
12
12
|
return typeof value === 'string' && value !== '' ? value : null;
|
|
13
13
|
}
|
|
14
14
|
/* The words of a user line that somebody said, or null for a line that carries tool results. */
|
|
15
|
-
function spokenText(content) {
|
|
15
|
+
export function spokenText(content) {
|
|
16
16
|
if (typeof content === 'string') {
|
|
17
17
|
return content;
|
|
18
18
|
}
|