@adecore/agents 0.23.1 → 0.24.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/README.md +3 -3
- package/dist/chat/attachment-store.js +4 -5
- package/dist/chat/backend.d.ts +6 -1
- package/dist/chat/background-work.d.ts +1 -1
- package/dist/chat/background-work.js +2 -1
- package/dist/chat/chat-core.d.ts +53 -161
- package/dist/chat/chat-core.js +146 -112
- package/dist/chat/chat-log.js +3 -3
- package/dist/chat/chat-process.d.ts +21 -0
- package/dist/chat/chat-process.js +109 -9
- package/dist/chat/chat-session.d.ts +10 -17
- package/dist/chat/chat-session.js +141 -182
- package/dist/chat/chat-store.d.ts +1 -0
- package/dist/chat/chat-store.js +105 -1
- package/dist/chat/claude-backend.d.ts +4 -0
- package/dist/chat/claude-backend.js +51 -27
- package/dist/chat/claude-protocol.d.ts +12 -0
- package/dist/chat/claude-protocol.js +137 -7
- package/dist/chat/claude-title.d.ts +1 -3
- package/dist/chat/claude-title.js +11 -39
- package/dist/chat/claude-transcript.d.ts +1 -0
- package/dist/chat/claude-transcript.js +36 -17
- package/dist/chat/codex-backend.d.ts +21 -1
- package/dist/chat/codex-backend.js +168 -10
- package/dist/chat/codex-protocol.d.ts +2 -0
- package/dist/chat/codex-protocol.js +37 -3
- package/dist/chat/codex-thread.js +1 -3
- package/dist/chat/codex-transport.d.ts +3 -0
- package/dist/chat/codex-transport.js +38 -35
- package/dist/chat/context-breakdown.js +2 -2
- package/dist/chat/errors.d.ts +1 -1
- package/dist/chat/external-sessions.js +2 -2
- package/dist/chat/fixtures/claude-2.1.296-goal.json +136 -0
- package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10.jsonl +2 -0
- package/dist/chat/index.d.ts +29 -0
- package/dist/chat/index.js +23 -0
- package/dist/chat/projector.d.ts +2 -1
- package/dist/chat/projector.js +12 -3
- package/dist/chat/request-summary.js +1 -4
- package/dist/chat/subagent-settlement.js +1 -3
- package/dist/chat/thread.d.ts +3 -0
- package/dist/chat/thread.js +20 -3
- package/dist/chat/ui-queries.d.ts +2 -0
- package/dist/chat/ui-queries.js +40 -5
- package/dist/chat/ui-replies.d.ts +33 -0
- package/dist/chat/ui-replies.js +152 -0
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +3 -0
- package/dist/coordination/index.d.ts +15 -0
- package/dist/coordination/index.js +16 -0
- package/dist/host/handlers.js +8 -0
- package/dist/host/index.d.ts +7 -0
- package/dist/host/index.js +7 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +10 -0
- package/dist/providers/accounts/status.js +1 -3
- package/dist/providers/claude.js +3 -1
- package/dist/providers/codex.js +3 -1
- package/dist/providers/index.d.ts +14 -0
- package/dist/providers/index.js +12 -0
- package/dist/run-process.js +54 -14
- package/dist/skills.d.ts +1 -0
- package/dist/skills.js +4 -0
- package/dist/tasks/task-coordinator.d.ts +4 -0
- package/dist/tasks/task-coordinator.js +16 -2
- package/dist/tasks/wiring.js +7 -1
- package/dist/{providers/accounts/test-accounts.d.ts → testing/accounts.d.ts} +2 -2
- package/dist/{providers/accounts/test-accounts.js → testing/accounts.js} +3 -3
- package/dist/testing/bin/fake-claude.d.ts +1 -0
- package/dist/testing/bin/fake-claude.js +6 -0
- package/dist/testing/bin/fake-codex.d.ts +1 -0
- package/dist/testing/bin/fake-codex.js +6 -0
- package/dist/{chat → testing}/fake-claude.d.ts +1 -1
- package/dist/{chat → testing}/fake-claude.js +0 -4
- package/dist/{chat → testing}/fake-cli.d.ts +1 -1
- package/dist/{chat → testing}/fake-codex.d.ts +1 -1
- package/dist/{chat → testing}/fake-codex.js +70 -8
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.js +7 -0
- package/dist/{outbox → testing}/manual-clock.d.ts +1 -1
- package/dist/{watch-test-helpers.d.ts → testing/watch.d.ts} +1 -1
- package/dist/usage/exchange.d.ts +5 -3
- package/dist/usage/exchange.js +25 -16
- package/dist/usage/index-file.d.ts +1 -0
- package/dist/usage/index-file.js +20 -2
- package/dist/usage/index.d.ts +9 -0
- package/dist/usage/index.js +6 -0
- package/dist/usage/limits/normalize.js +1 -3
- package/dist/usage/pricing.d.ts +5 -3
- package/dist/usage/pricing.js +28 -19
- package/dist/usage/scanner.js +39 -11
- package/dist/usage/usage-service.js +8 -3
- package/package.json +72 -11
- package/src/chat/attachment-store.ts +4 -6
- package/src/chat/backend.ts +8 -0
- package/src/chat/background-work.ts +2 -2
- package/src/chat/chat-core.ts +221 -143
- package/src/chat/chat-log.ts +3 -3
- package/src/chat/chat-process.ts +128 -8
- package/src/chat/chat-session.ts +153 -204
- package/src/chat/chat-store.ts +104 -1
- package/src/chat/claude-backend.ts +58 -26
- package/src/chat/claude-protocol.ts +147 -8
- package/src/chat/claude-title.ts +11 -39
- package/src/chat/claude-transcript.ts +38 -18
- package/src/chat/codex-backend.ts +182 -20
- package/src/chat/codex-protocol.ts +40 -5
- package/src/chat/codex-thread.ts +1 -4
- package/src/chat/codex-transport.ts +45 -35
- package/src/chat/context-breakdown.ts +2 -2
- package/src/chat/errors.ts +1 -0
- package/src/chat/external-sessions.ts +2 -2
- package/src/chat/fixtures/claude-2.1.296-goal.json +136 -0
- package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10.jsonl +2 -0
- package/src/chat/index.ts +49 -0
- package/src/chat/projector.ts +13 -4
- package/src/chat/request-summary.ts +8 -5
- package/src/chat/subagent-settlement.ts +1 -4
- package/src/chat/thread.ts +22 -3
- package/src/chat/ui-queries.ts +46 -5
- package/src/chat/ui-replies.ts +187 -0
- package/src/context/index.ts +26 -0
- package/src/coordination/index.ts +67 -0
- package/src/host/handlers.ts +8 -0
- package/src/host/index.ts +8 -0
- package/src/index.ts +20 -0
- package/src/providers/accounts/status.ts +1 -4
- package/src/providers/claude.ts +3 -1
- package/src/providers/codex.ts +3 -1
- package/src/providers/index.ts +15 -0
- package/src/run-process.ts +56 -16
- package/src/skills.ts +5 -0
- package/src/tasks/task-coordinator.ts +22 -2
- package/src/tasks/wiring.ts +7 -1
- package/src/{providers/accounts/test-accounts.ts → testing/accounts.ts} +4 -4
- package/src/testing/bin/fake-claude.ts +7 -0
- package/src/testing/bin/fake-codex.ts +7 -0
- package/src/{chat → testing}/fake-claude.ts +1 -5
- package/src/{chat → testing}/fake-cli.ts +1 -1
- package/src/{chat → testing}/fake-codex.ts +75 -6
- package/src/testing/index.ts +7 -0
- package/src/{outbox → testing}/manual-clock.ts +1 -1
- package/src/{watch-test-helpers.ts → testing/watch.ts} +1 -1
- package/src/usage/exchange.ts +31 -20
- package/src/usage/index-file.ts +23 -2
- package/src/usage/index.ts +10 -0
- package/src/usage/limits/normalize.ts +1 -4
- package/src/usage/pricing.ts +34 -23
- package/src/usage/scanner.ts +48 -11
- package/src/usage/usage-service.ts +8 -3
- /package/dist/{chat → testing}/fake-cli.js +0 -0
- /package/dist/{outbox → testing}/manual-clock.js +0 -0
- /package/dist/{watch-test-helpers.js → testing/watch.js} +0 -0
package/src/chat/chat-session.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import type {
|
|
3
3
|
ChatAttachment,
|
|
4
|
-
ChatAssistantItem,
|
|
5
4
|
ChatCheckpointDiff,
|
|
6
5
|
ChatEvent,
|
|
6
|
+
ChatGoalAction,
|
|
7
7
|
ChatInfo,
|
|
8
8
|
ChatItem,
|
|
9
9
|
ChatQueuedMessage,
|
|
10
10
|
ChatQuoteSource,
|
|
11
11
|
ChatSkill,
|
|
12
12
|
ChatSubagentItem,
|
|
13
|
+
ChatToolItem,
|
|
13
14
|
ChatTurnItem,
|
|
14
15
|
ChatUiAnswer,
|
|
15
16
|
ChatUiChoiceOrigin,
|
|
@@ -18,10 +19,8 @@ import type {
|
|
|
18
19
|
ModelSelection,
|
|
19
20
|
RuntimeMode
|
|
20
21
|
} from '@adecore/agent-contracts';
|
|
21
|
-
import {
|
|
22
|
-
import { resolveUiChoice, UI_FENCE_LANGUAGE, UiFailure
|
|
23
|
-
import type { UiCompileOptions } from '@adecore/intelligent-ui/compiler';
|
|
24
|
-
import { UiStream, type UiStreamPreview } from '@adecore/intelligent-ui/stream';
|
|
22
|
+
import { HOST_TASK_ORIGIN, notResumedNote } from '@adecore/agent-contracts';
|
|
23
|
+
import { resolveUiChoice, UI_FENCE_LANGUAGE, UiFailure } from '@adecore/intelligent-ui';
|
|
25
24
|
import type { ChatProvider } from '../providers/provider.ts';
|
|
26
25
|
import type { LimitsUpdate } from '../usage/limits/normalize.ts';
|
|
27
26
|
import type { BackendEvent, BackendLaunch, ChatBackend, TurnInput } from './backend.ts';
|
|
@@ -34,6 +33,7 @@ import { isMainAgentOutput, ThreadProjector } from './projector.ts';
|
|
|
34
33
|
import { readableAssistantText } from './readable-text.ts';
|
|
35
34
|
import type { SubagentSettlement } from './subagent-settlement.ts';
|
|
36
35
|
import { ChatThread } from './thread.ts';
|
|
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
39
|
|
|
@@ -84,10 +84,7 @@ export interface ChatSessionOptions {
|
|
|
84
84
|
checkpoints?: TurnCheckpoints;
|
|
85
85
|
// The info string of the host's UI fences; `UI_FENCE_LANGUAGE` without one.
|
|
86
86
|
uiFenceLanguage?: string;
|
|
87
|
-
ui?:
|
|
88
|
-
schemas: UiCompileOptions['querySchemas'];
|
|
89
|
-
observe(item: ChatAssistantItem, final: boolean): void;
|
|
90
|
-
};
|
|
87
|
+
ui?: UiReplyHost;
|
|
91
88
|
emit(event: ChatEvent): void;
|
|
92
89
|
/* What a turn said about the plan it runs on. It belongs to the machine, so it leaves the chat. */
|
|
93
90
|
onLimits?(update: LimitsUpdate): void;
|
|
@@ -117,8 +114,6 @@ export interface LimitResumeHooks {
|
|
|
117
114
|
|
|
118
115
|
// Claude Code names a session about six seconds after its first prompt, and a first turn can run for minutes.
|
|
119
116
|
const TITLE_RECHECK_MS = 10_000;
|
|
120
|
-
// Longer than any fence line or the name of a tag a scan looks for, so a match never falls between two deltas.
|
|
121
|
-
const UI_SCAN_OVERLAP = 64;
|
|
122
117
|
|
|
123
118
|
export interface ChatSendExtras {
|
|
124
119
|
uiChoice?: ChatUiChoiceOrigin;
|
|
@@ -206,40 +201,35 @@ export class ChatSession {
|
|
|
206
201
|
private pendingPreambles: readonly string[];
|
|
207
202
|
private submission: { turnId: string; input: TurnInput } | null = null;
|
|
208
203
|
private readonly steering = new Set<string>();
|
|
209
|
-
private readonly uiStreams = new Map<string, UiStream>();
|
|
210
|
-
private readonly uiCompiled = new Map<string, UiStreamPreview>();
|
|
211
204
|
private readonly capturedUiAccess: Map<string, unknown>;
|
|
212
|
-
|
|
213
|
-
private readonly uiScanned = new Map<string, number>();
|
|
214
|
-
private readonly uiReferencing = new Set<string>();
|
|
215
|
-
private readonly uiAttachments = new Map<string | null, string | undefined>();
|
|
216
|
-
private readonly uiFenceLanguage: string;
|
|
205
|
+
private readonly uiReplies: UiReplies;
|
|
217
206
|
|
|
218
207
|
constructor(options: ChatSessionOptions) {
|
|
219
208
|
this.options = options;
|
|
220
|
-
this.uiFenceLanguage = options.uiFenceLanguage ?? UI_FENCE_LANGUAGE;
|
|
221
209
|
this.pendingPreambles = options.preambles ?? [];
|
|
222
210
|
this.capturedUiAccess = new Map(Object.entries(options.uiAccess ?? {}));
|
|
223
211
|
this.thread = new ChatThread(options.info, options.items);
|
|
224
212
|
this.projector = new ThreadProjector(this.thread, { providerName: options.provider.name });
|
|
213
|
+
this.uiReplies = new UiReplies({
|
|
214
|
+
thread: this.thread,
|
|
215
|
+
fenceLanguage: options.uiFenceLanguage ?? UI_FENCE_LANGUAGE,
|
|
216
|
+
clock: options.clock ?? systemClock,
|
|
217
|
+
...(options.ui ? { host: options.ui } : {}),
|
|
218
|
+
emit: (event) => options.emit(event),
|
|
219
|
+
repair: (note) => {
|
|
220
|
+
if (!this.pendingPreambles.includes(note)) {
|
|
221
|
+
this.pendingPreambles = [...this.pendingPreambles, note];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
get uiPreviews(): ChatEvent[] {
|
|
228
|
-
|
|
229
|
-
for (const [itemId, preview] of this.uiCompiled) {
|
|
230
|
-
const item = this.thread.get(itemId);
|
|
231
|
-
if (item?.kind === 'assistant' && item.streaming && item.text.length === preview.textLength) {
|
|
232
|
-
events.push({ type: 'delta', itemId, text: '', ui: preview.blocks, textLength: preview.textLength });
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
return events;
|
|
228
|
+
return this.uiReplies.previews();
|
|
236
229
|
}
|
|
237
230
|
|
|
238
231
|
previewItems(items: ChatItem[]): ChatItem[] {
|
|
239
|
-
return
|
|
240
|
-
const preview = this.uiCompiled.get(item.id);
|
|
241
|
-
return item.kind === 'assistant' && preview?.textLength === item.text.length ? { ...item, ui: preview.blocks } : item;
|
|
242
|
-
});
|
|
232
|
+
return this.uiReplies.previewItems(items);
|
|
243
233
|
}
|
|
244
234
|
|
|
245
235
|
get id(): string {
|
|
@@ -267,7 +257,37 @@ export class ChatSession {
|
|
|
267
257
|
get busy(): boolean {
|
|
268
258
|
const turnId = this.thread.info.activeTurnId;
|
|
269
259
|
const turn = turnId ? this.thread.get(turnId) : undefined;
|
|
270
|
-
|
|
260
|
+
// A turn the CLI opened toward a goal is the goal's own: a message waits for it like for any turn of the person's.
|
|
261
|
+
return turnId !== null && (turn?.kind !== 'turn' || turn.origin !== 'agent' || this.goalWorks());
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* Whether the CLI works toward a goal, which goes on in turns of its own until the goal is met, paused or cleared. */
|
|
265
|
+
private goalWorks(): boolean {
|
|
266
|
+
return this.thread.info.goal?.status === 'active';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/*
|
|
270
|
+
* Pauses, resumes or clears the chat's goal. A CLI with calls for it takes them at once, even while a goal
|
|
271
|
+
* turn runs; resuming an idle goal and clearing a goal of a CLI without them go out as `/goal` in a turn of
|
|
272
|
+
* their own, which a goal turn still running is in the way of.
|
|
273
|
+
*/
|
|
274
|
+
async goal(action: ChatGoalAction): Promise<void> {
|
|
275
|
+
const supported = this.options.provider.capabilities.goals;
|
|
276
|
+
if (supported === undefined || (supported === 'command' && action !== 'clear')) {
|
|
277
|
+
throw new ChatError('chat-unsupported', `${this.options.provider.name} cannot ${action} a goal`);
|
|
278
|
+
}
|
|
279
|
+
if (this.thread.info.goal === undefined) {
|
|
280
|
+
throw new ChatError('goal-not-found', 'This chat has no goal');
|
|
281
|
+
}
|
|
282
|
+
const backend = this.backend;
|
|
283
|
+
if (supported === 'native' && backend?.goal && backend.running && (action !== 'resume' || this.busy)) {
|
|
284
|
+
await backend.goal(action);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (this.busy) {
|
|
288
|
+
throw new ChatError('chat-busy', 'Stop the turn that works toward the goal first');
|
|
289
|
+
}
|
|
290
|
+
this.send(`/goal ${action}`);
|
|
271
291
|
}
|
|
272
292
|
|
|
273
293
|
/* The next process uses these settings; live work keeps the process it started in. */
|
|
@@ -456,6 +476,8 @@ export class ChatSession {
|
|
|
456
476
|
extras.delivery !== 'steer' ||
|
|
457
477
|
this.info.queuePaused ||
|
|
458
478
|
text.startsWith('/') ||
|
|
479
|
+
// A goal runs as turns of its own; a message waits for the one running instead of joining it.
|
|
480
|
+
this.goalWorks() ||
|
|
459
481
|
!this.busy ||
|
|
460
482
|
!turnId ||
|
|
461
483
|
!backend?.steerTurn ||
|
|
@@ -599,6 +621,10 @@ export class ChatSession {
|
|
|
599
621
|
private dispatch(text: string, extras: ChatSendExtras, requestedTurnId?: string): void {
|
|
600
622
|
this.pauseQueue(false);
|
|
601
623
|
this.settleAgentTurn();
|
|
624
|
+
// A goal that was met has said what it had to; the next message of the person leaves it behind.
|
|
625
|
+
if (this.thread.info.goal?.status === 'complete') {
|
|
626
|
+
this.emit([this.thread.patchInfo({ goal: undefined })]);
|
|
627
|
+
}
|
|
602
628
|
const { preamble, note } = this.contextNote(text);
|
|
603
629
|
// A slash command must stay the first thing the CLI reads, as `contextNote` keeps it.
|
|
604
630
|
const references = text.startsWith('/') || !this.options.references ? { ids: [], note: null } : this.options.references(extras.chats);
|
|
@@ -723,6 +749,22 @@ export class ChatSession {
|
|
|
723
749
|
return true;
|
|
724
750
|
}
|
|
725
751
|
|
|
752
|
+
/*
|
|
753
|
+
* Marks the chat as waiting on a person for something the host asks beside the CLI, such as a
|
|
754
|
+
* card a verb holds its call for. The chat reads `needs-you` until every key is released.
|
|
755
|
+
*/
|
|
756
|
+
holdForPerson(key: string): void {
|
|
757
|
+
if (this.thread.hold(key)) {
|
|
758
|
+
this.emit([this.thread.setStatus(this.thread.statusFor(this.thread.info.activeTurnId))]);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
releaseHold(key: string): void {
|
|
763
|
+
if (this.thread.release(key) && this.thread.info.status === 'needs-you') {
|
|
764
|
+
this.emit([this.thread.setStatus(this.thread.statusFor(this.thread.info.activeTurnId))]);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
726
768
|
/*
|
|
727
769
|
* Starts the chat over: the CLI goes, and the next send starts one without a session to resume.
|
|
728
770
|
* The thread resets at once; the returned promise lets the host wait for pending attachment
|
|
@@ -759,6 +801,7 @@ export class ChatSession {
|
|
|
759
801
|
queuePaused: false,
|
|
760
802
|
background: [],
|
|
761
803
|
delegating: undefined,
|
|
804
|
+
goal: undefined,
|
|
762
805
|
effectiveRuntimeMode: undefined,
|
|
763
806
|
permissionMode: undefined,
|
|
764
807
|
slashCommands: [],
|
|
@@ -1149,7 +1192,7 @@ export class ChatSession {
|
|
|
1149
1192
|
const settled =
|
|
1150
1193
|
item.kind === 'turn' && item.state === 'running'
|
|
1151
1194
|
? { ...item, state: 'aborted' as const, endedAt: now }
|
|
1152
|
-
: item.kind === 'subagent' && item.origin !==
|
|
1195
|
+
: item.kind === 'subagent' && item.origin !== HOST_TASK_ORIGIN && item.status === 'running'
|
|
1153
1196
|
? { ...item, status: 'failed' as const, finishedAt: now }
|
|
1154
1197
|
: settleStoredItem(item, null);
|
|
1155
1198
|
if (settled !== item) {
|
|
@@ -1174,9 +1217,30 @@ export class ChatSession {
|
|
|
1174
1217
|
this.options.persist();
|
|
1175
1218
|
}
|
|
1176
1219
|
|
|
1220
|
+
/*
|
|
1221
|
+
* Ends the CLI and keeps the conversation: the next message starts a fresh process that resumes it,
|
|
1222
|
+
* and with it the skills, plugins and servers the CLI reads at its start. Refused while a turn runs or
|
|
1223
|
+
* the process still holds work, a request or a background command, since those would end with it.
|
|
1224
|
+
*/
|
|
1225
|
+
restart(): Promise<void> {
|
|
1226
|
+
if (this.thread.info.activeTurnId !== null || this.processInUse()) {
|
|
1227
|
+
throw new ChatError('chat-busy', `Chat ${this.id} is still working; stop it before restarting its agent session`);
|
|
1228
|
+
}
|
|
1229
|
+
this.clearStartWarning();
|
|
1230
|
+
this.clearWorkflowWarnings();
|
|
1231
|
+
const backend = this.backend;
|
|
1232
|
+
this.backend = null;
|
|
1233
|
+
this.starting = null;
|
|
1234
|
+
this.launchedSelection = null;
|
|
1235
|
+
// What the init frame of the process that goes announced; a skill added since would be filtered out by it.
|
|
1236
|
+
this.emit([this.thread.patchInfo({ running: false, effectiveRuntimeMode: undefined, permissionMode: undefined, skills: undefined })]);
|
|
1237
|
+
this.options.persist();
|
|
1238
|
+
return backend?.dispose() ?? Promise.resolve();
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1177
1241
|
/* Ends the process and stops listening to it, as the host goes down; the thread stays as it is. */
|
|
1178
1242
|
freeze(): void {
|
|
1179
|
-
this.
|
|
1243
|
+
this.uiReplies.clear();
|
|
1180
1244
|
this.clearStartWarning();
|
|
1181
1245
|
this.clearWorkflowWarnings();
|
|
1182
1246
|
this.frozen = true;
|
|
@@ -1185,7 +1249,7 @@ export class ChatSession {
|
|
|
1185
1249
|
|
|
1186
1250
|
/* Ends the CLI and everything it started; settles once it exited or was sent the SIGKILL. */
|
|
1187
1251
|
dispose(): Promise<void> {
|
|
1188
|
-
this.
|
|
1252
|
+
this.uiReplies.clear();
|
|
1189
1253
|
this.clearStartWarning();
|
|
1190
1254
|
this.clearWorkflowWarnings();
|
|
1191
1255
|
if (this.titleTimer !== null) {
|
|
@@ -1351,7 +1415,9 @@ export class ChatSession {
|
|
|
1351
1415
|
/* Marks every running subagent of the CLI's own as stopped, for a person stopping the turn together with them. */
|
|
1352
1416
|
markSubagentsStopped(): void {
|
|
1353
1417
|
this.markStopped(
|
|
1354
|
-
this.thread
|
|
1418
|
+
this.thread
|
|
1419
|
+
.list()
|
|
1420
|
+
.filter((item): item is ChatSubagentItem => item.kind === 'subagent' && item.status === 'running' && item.origin !== HOST_TASK_ORIGIN)
|
|
1355
1421
|
);
|
|
1356
1422
|
}
|
|
1357
1423
|
|
|
@@ -1650,7 +1716,7 @@ export class ChatSession {
|
|
|
1650
1716
|
}
|
|
1651
1717
|
|
|
1652
1718
|
private processInUse(): boolean {
|
|
1653
|
-
return this.hasNativeWork() || this.thread.pending().length > 0 || this.awaitingBackgroundReport;
|
|
1719
|
+
return this.hasNativeWork() || this.thread.pending().length > 0 || this.awaitingBackgroundReport || this.goalWorks();
|
|
1654
1720
|
}
|
|
1655
1721
|
|
|
1656
1722
|
private replacementWaits(): boolean {
|
|
@@ -1696,6 +1762,7 @@ export class ChatSession {
|
|
|
1696
1762
|
saveGeneratedImage: this.options.saveGeneratedImage,
|
|
1697
1763
|
instructions: this.options.instructions?.() ?? null,
|
|
1698
1764
|
resumeNote: this.options.resumeNote?.() ?? null,
|
|
1765
|
+
...(info.goal === undefined ? {} : { goal: info.goal }),
|
|
1699
1766
|
...(folders.length > 0 ? { folders } : {}),
|
|
1700
1767
|
...(this.options.spawn ? { spawn: this.options.spawn } : {})
|
|
1701
1768
|
};
|
|
@@ -1822,7 +1889,7 @@ export class ChatSession {
|
|
|
1822
1889
|
}
|
|
1823
1890
|
this.emit(projected);
|
|
1824
1891
|
this.trackSubagentOwner(generation, event);
|
|
1825
|
-
this.watchWorkflows(event);
|
|
1892
|
+
this.watchWorkflows(generation, event);
|
|
1826
1893
|
if (isMainAgentOutput(event)) {
|
|
1827
1894
|
this.spokenTurnId = this.thread.info.activeTurnId;
|
|
1828
1895
|
}
|
|
@@ -1892,56 +1959,58 @@ export class ChatSession {
|
|
|
1892
1959
|
this.workflowWarnings.clear();
|
|
1893
1960
|
}
|
|
1894
1961
|
|
|
1895
|
-
private watchWorkflows(event: BackendEvent): void {
|
|
1896
|
-
if (
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
)
|
|
1900
|
-
|
|
1962
|
+
private watchWorkflows(generation: number, event: BackendEvent): void {
|
|
1963
|
+
if (event.type === 'tool.output' || event.type === 'tool.progress') {
|
|
1964
|
+
// These come by the thousand, and only the call they name can be a workflow they move.
|
|
1965
|
+
const item = this.projector.itemOf(generation, event.ref);
|
|
1966
|
+
if (item?.kind === 'tool' && item.name === 'Workflow') {
|
|
1967
|
+
this.watchWorkflow(item, true);
|
|
1968
|
+
}
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
if (!['tool.started', 'tool.done', 'workflow.progress', 'task.started', 'task.done', 'exit', 'failed'].includes(event.type)) {
|
|
1901
1972
|
return;
|
|
1902
1973
|
}
|
|
1903
|
-
const clock = this.options.clock ?? systemClock;
|
|
1904
1974
|
const ref = 'ref' in event ? event.ref : null;
|
|
1905
1975
|
for (const item of this.thread.list()) {
|
|
1906
|
-
if (item.kind
|
|
1907
|
-
|
|
1908
|
-
}
|
|
1909
|
-
const existing = this.workflowWarnings.get(item.id);
|
|
1910
|
-
const progress =
|
|
1911
|
-
ref === item.toolUseId &&
|
|
1912
|
-
['workflow.progress', 'tool.started', 'tool.progress', 'tool.output', 'tool.done', 'task.started'].includes(event.type);
|
|
1913
|
-
if (item.state !== 'running' || progress) {
|
|
1914
|
-
if (existing !== undefined) {
|
|
1915
|
-
clock.clearTimeout(existing);
|
|
1916
|
-
this.workflowWarnings.delete(item.id);
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1919
|
-
if (item.state !== 'running' || (!progress && existing !== undefined) || (!progress && item.workflow?.stalledAt !== undefined)) {
|
|
1920
|
-
continue;
|
|
1976
|
+
if (item.kind === 'tool' && item.name === 'Workflow') {
|
|
1977
|
+
this.watchWorkflow(item, ref === item.toolUseId && ['workflow.progress', 'tool.started', 'tool.done', 'task.started'].includes(event.type));
|
|
1921
1978
|
}
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
private watchWorkflow(item: ChatToolItem, progress: boolean): void {
|
|
1983
|
+
const clock = this.options.clock ?? systemClock;
|
|
1984
|
+
const existing = this.workflowWarnings.get(item.id);
|
|
1985
|
+
if (item.state !== 'running' || progress) {
|
|
1986
|
+
if (existing !== undefined) {
|
|
1987
|
+
clock.clearTimeout(existing);
|
|
1988
|
+
this.workflowWarnings.delete(item.id);
|
|
1929
1989
|
}
|
|
1930
|
-
this.workflowWarnings.set(
|
|
1931
|
-
item.id,
|
|
1932
|
-
clock.setTimeout(() => {
|
|
1933
|
-
this.workflowWarnings.delete(item.id);
|
|
1934
|
-
const current = this.thread.get(item.id);
|
|
1935
|
-
if (this.frozen || current?.kind !== 'tool' || current.state !== 'running') {
|
|
1936
|
-
return;
|
|
1937
|
-
}
|
|
1938
|
-
this.emit([
|
|
1939
|
-
this.thread.upsert({ ...current, workflow: { name: null, phases: [], agents: [], ...current.workflow, stalledAt: clock.now() } })
|
|
1940
|
-
]);
|
|
1941
|
-
this.options.persist();
|
|
1942
|
-
}, 5 * 60_000)
|
|
1943
|
-
);
|
|
1944
1990
|
}
|
|
1991
|
+
if (item.state !== 'running' || (!progress && existing !== undefined) || (!progress && item.workflow?.stalledAt !== undefined)) {
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
if (progress) {
|
|
1995
|
+
this.emit([
|
|
1996
|
+
this.thread.upsert({
|
|
1997
|
+
...item,
|
|
1998
|
+
workflow: { name: null, phases: [], agents: [], ...item.workflow, lastProgressAt: clock.now(), stalledAt: undefined }
|
|
1999
|
+
})
|
|
2000
|
+
]);
|
|
2001
|
+
}
|
|
2002
|
+
this.workflowWarnings.set(
|
|
2003
|
+
item.id,
|
|
2004
|
+
clock.setTimeout(() => {
|
|
2005
|
+
this.workflowWarnings.delete(item.id);
|
|
2006
|
+
const current = this.thread.get(item.id);
|
|
2007
|
+
if (this.frozen || current?.kind !== 'tool' || current.state !== 'running') {
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
this.emit([this.thread.upsert({ ...current, workflow: { name: null, phases: [], agents: [], ...current.workflow, stalledAt: clock.now() } })]);
|
|
2011
|
+
this.options.persist();
|
|
2012
|
+
}, 5 * 60_000)
|
|
2013
|
+
);
|
|
1945
2014
|
}
|
|
1946
2015
|
|
|
1947
2016
|
/* Whether the CLI that reports was started on a pick standing for the same window as the chat's own. */
|
|
@@ -2050,129 +2119,9 @@ export class ChatSession {
|
|
|
2050
2119
|
|
|
2051
2120
|
private emit(events: ChatEvent[]): void {
|
|
2052
2121
|
const delegating = this.delegatingChange(events);
|
|
2053
|
-
for (
|
|
2054
|
-
|
|
2055
|
-
this.clearUi();
|
|
2056
|
-
}
|
|
2057
|
-
if (event.type === 'item' && event.item.kind === 'tool') {
|
|
2058
|
-
this.uiAttachments.delete(event.item.turnId);
|
|
2059
|
-
}
|
|
2060
|
-
const item = event.type === 'item' ? event.item : event.type === 'delta' ? this.thread.get(event.itemId) : null;
|
|
2061
|
-
if (item?.kind !== 'assistant' || item.parentToolUseId) {
|
|
2062
|
-
this.options.emit(event);
|
|
2063
|
-
continue;
|
|
2064
|
-
}
|
|
2065
|
-
const tail = this.unscannedUiText(item, event.type === 'item');
|
|
2066
|
-
if (uiMayReferenceHost(tail)) {
|
|
2067
|
-
this.uiReferencing.add(item.id);
|
|
2068
|
-
}
|
|
2069
|
-
let stream = this.uiStreams.get(item.id);
|
|
2070
|
-
if (!stream && !uiHasFence(tail, this.uiFenceLanguage)) {
|
|
2071
|
-
this.forgetUiScan(item);
|
|
2072
|
-
this.options.emit(event);
|
|
2073
|
-
continue;
|
|
2074
|
-
}
|
|
2075
|
-
if (!stream) {
|
|
2076
|
-
stream = new UiStream({
|
|
2077
|
-
querySchemas: this.options.ui?.schemas,
|
|
2078
|
-
fenceLanguage: this.uiFenceLanguage,
|
|
2079
|
-
id: item.id,
|
|
2080
|
-
clock: this.options.clock ?? systemClock,
|
|
2081
|
-
emit: (preview) => {
|
|
2082
|
-
this.uiCompiled.set(item.id, preview);
|
|
2083
|
-
this.options.emit({ type: 'delta', itemId: item.id, text: '', ui: preview.blocks, textLength: preview.textLength });
|
|
2084
|
-
}
|
|
2085
|
-
});
|
|
2086
|
-
this.uiStreams.set(item.id, stream);
|
|
2087
|
-
}
|
|
2088
|
-
if (this.uiReferencing.has(item.id)) {
|
|
2089
|
-
this.options.ui?.observe(item, false);
|
|
2090
|
-
}
|
|
2091
|
-
this.forgetUiScan(item);
|
|
2092
|
-
const latestAttachment = this.latestUiAttachment(item.turnId);
|
|
2093
|
-
if (event.type === 'item' && !item.streaming) {
|
|
2094
|
-
const ui = stream
|
|
2095
|
-
.finish(item.text, latestAttachment)
|
|
2096
|
-
.map((block) => ({ ...block, revision: createHash('sha256').update(JSON.stringify(block)).digest('hex') }));
|
|
2097
|
-
this.uiStreams.delete(item.id);
|
|
2098
|
-
this.uiCompiled.delete(item.id);
|
|
2099
|
-
const diagnostics = [...new Set(ui.flatMap((block) => block.diagnostics.map((diagnostic) => diagnostic.message)))].slice(0, 6);
|
|
2100
|
-
if (diagnostics.length > 0) {
|
|
2101
|
-
const repair =
|
|
2102
|
-
`Your last ${this.uiFenceLanguage} block had these diagnostics: ${diagnostics.join(' ')} Use the catalog's supported props and components.`.slice(
|
|
2103
|
-
0,
|
|
2104
|
-
1200
|
|
2105
|
-
);
|
|
2106
|
-
if (!this.pendingPreambles.includes(repair)) {
|
|
2107
|
-
this.pendingPreambles = [...this.pendingPreambles, repair];
|
|
2108
|
-
}
|
|
2109
|
-
}
|
|
2110
|
-
const current = this.thread.get(item.id);
|
|
2111
|
-
const completed = { ...item, ui, ...(current?.kind === 'assistant' && current.uiQueries ? { uiQueries: current.uiQueries } : {}) };
|
|
2112
|
-
event = this.thread.upsert(completed);
|
|
2113
|
-
this.options.emit(event);
|
|
2114
|
-
this.options.ui?.observe(completed, true);
|
|
2115
|
-
} else {
|
|
2116
|
-
this.options.emit(event);
|
|
2117
|
-
stream.update(item.text, latestAttachment);
|
|
2118
|
-
}
|
|
2119
|
-
}
|
|
2120
|
-
}
|
|
2121
|
-
|
|
2122
|
-
/*
|
|
2123
|
-
* The text a scan has not covered yet, from the start of the line the last scan ended in, since a fence
|
|
2124
|
-
* is one line; a long line is cut short, behind a character no fence or tag can follow.
|
|
2125
|
-
*/
|
|
2126
|
-
private unscannedUiText(item: ChatAssistantItem, whole: boolean): string {
|
|
2127
|
-
const scanned = whole ? 0 : (this.uiScanned.get(item.id) ?? 0);
|
|
2128
|
-
this.uiScanned.set(item.id, item.text.length);
|
|
2129
|
-
if (scanned === 0 || scanned > item.text.length) {
|
|
2130
|
-
return item.text;
|
|
2131
|
-
}
|
|
2132
|
-
const line = item.text.lastIndexOf('\n', scanned - 1) + 1;
|
|
2133
|
-
const from = Math.max(line, scanned - UI_SCAN_OVERLAP);
|
|
2134
|
-
return from === line ? item.text.slice(from) : `\0${item.text.slice(from)}`;
|
|
2135
|
-
}
|
|
2136
|
-
|
|
2137
|
-
private forgetUiScan(item: ChatAssistantItem): void {
|
|
2138
|
-
if (!item.streaming) {
|
|
2139
|
-
this.uiScanned.delete(item.id);
|
|
2140
|
-
this.uiReferencing.delete(item.id);
|
|
2141
|
-
}
|
|
2142
|
-
}
|
|
2143
|
-
|
|
2144
|
-
/* Looked up once per turn, and again only after one of its tool calls changed. */
|
|
2145
|
-
private latestUiAttachment(turnId: string | null): string | undefined {
|
|
2146
|
-
if (this.uiAttachments.has(turnId)) {
|
|
2147
|
-
return this.uiAttachments.get(turnId);
|
|
2148
|
-
}
|
|
2149
|
-
const attachment = this.findUiAttachment(turnId);
|
|
2150
|
-
this.uiAttachments.set(turnId, attachment);
|
|
2151
|
-
return attachment;
|
|
2152
|
-
}
|
|
2153
|
-
|
|
2154
|
-
private findUiAttachment(turnId: string | null): string | undefined {
|
|
2155
|
-
for (const item of this.thread.list().toReversed()) {
|
|
2156
|
-
if (item.kind !== 'tool' || item.turnId !== turnId || item.name !== 'ImageGeneration' || item.state !== 'done') {
|
|
2157
|
-
continue;
|
|
2158
|
-
}
|
|
2159
|
-
const input = GeneratedImageInputSchema.safeParse(item.input);
|
|
2160
|
-
if (input.success && input.data.attachment?.mime.startsWith('image/')) {
|
|
2161
|
-
return input.data.attachment.id;
|
|
2162
|
-
}
|
|
2163
|
-
}
|
|
2164
|
-
return undefined;
|
|
2165
|
-
}
|
|
2166
|
-
|
|
2167
|
-
private clearUi(): void {
|
|
2168
|
-
for (const stream of this.uiStreams.values()) {
|
|
2169
|
-
stream.dispose();
|
|
2122
|
+
for (const event of delegating === null ? events : [...events, delegating]) {
|
|
2123
|
+
this.uiReplies.pass(event);
|
|
2170
2124
|
}
|
|
2171
|
-
this.uiStreams.clear();
|
|
2172
|
-
this.uiCompiled.clear();
|
|
2173
|
-
this.uiScanned.clear();
|
|
2174
|
-
this.uiReferencing.clear();
|
|
2175
|
-
this.uiAttachments.clear();
|
|
2176
2125
|
}
|
|
2177
2126
|
|
|
2178
2127
|
/* Said on the info as well as by the rows, since a client with the thread closed sees only the info. */
|
package/src/chat/chat-store.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mkdirSync } from 'node:fs';
|
|
2
|
-
import { mkdir, readdir, readFile, rm, stat } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, open, readdir, readFile, rm, stat } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { ChatInfoSchema, ChatItemSchema, type ChatInfo, type ChatItem } from '@adecore/agent-contracts';
|
|
5
5
|
import { z } from 'zod';
|
|
@@ -57,6 +57,7 @@ function logName(chatId: string): string {
|
|
|
57
57
|
return `${encodeURIComponent(chatId)}.log`;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// `settled` reads the info from the head of the record, so it stays the first key.
|
|
60
61
|
function recordBody(
|
|
61
62
|
info: ChatInfo,
|
|
62
63
|
items: ChatItem[],
|
|
@@ -223,6 +224,19 @@ export class ChatStore {
|
|
|
223
224
|
return (await this.readSnapshot(chatId)) === UNREADABLE;
|
|
224
225
|
}
|
|
225
226
|
|
|
227
|
+
/*
|
|
228
|
+
* Whether the chat has neither a turn in flight nor a resume after a limit, by the info its record
|
|
229
|
+
* starts with and the last one its log wrote, without parsing the thread. False whenever that
|
|
230
|
+
* cannot be told so cheaply, which leaves the decision to `read`.
|
|
231
|
+
*/
|
|
232
|
+
async settled(chatId: string): Promise<boolean> {
|
|
233
|
+
if (!settledInfo(await leadingInfo(join(this.dir, recordFileName(chatId))))) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
const last = lastLoggedInfo((await readOrNull(this.logPath(chatId))) ?? '');
|
|
237
|
+
return last === undefined || settledInfo(last);
|
|
238
|
+
}
|
|
239
|
+
|
|
226
240
|
private async readSnapshot(chatId: string): Promise<z.infer<typeof RecordSchema> | typeof UNREADABLE | null> {
|
|
227
241
|
const raw = await readOrNull(join(this.dir, recordFileName(chatId)));
|
|
228
242
|
if (raw === null) {
|
|
@@ -253,6 +267,95 @@ export class ChatStore {
|
|
|
253
267
|
|
|
254
268
|
const UNREADABLE = Symbol('unreadable');
|
|
255
269
|
|
|
270
|
+
const RECORD_HEAD = Buffer.from('{"info":{');
|
|
271
|
+
const HEAD_CHUNK_BYTES = 16384;
|
|
272
|
+
const QUOTE = 0x22;
|
|
273
|
+
const BACKSLASH = 0x5c;
|
|
274
|
+
const OPEN_BRACE = 0x7b;
|
|
275
|
+
const CLOSE_BRACE = 0x7d;
|
|
276
|
+
|
|
277
|
+
/*
|
|
278
|
+
* The info a record starts with, read only as far as it goes; undefined for a record that is missing,
|
|
279
|
+
* starts otherwise (an older or hand-edited one) or ends before its info does. Scanning bytes is safe
|
|
280
|
+
* in UTF-8, where no byte of a multibyte character equals a quote, a backslash or a brace.
|
|
281
|
+
*/
|
|
282
|
+
async function leadingInfo(path: string): Promise<unknown> {
|
|
283
|
+
let handle: Awaited<ReturnType<typeof open>>;
|
|
284
|
+
try {
|
|
285
|
+
handle = await open(path, 'r');
|
|
286
|
+
} catch (e) {
|
|
287
|
+
if (isNotFound(e)) {
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
throw e;
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
let head = Buffer.alloc(0);
|
|
294
|
+
let at = RECORD_HEAD.length;
|
|
295
|
+
let depth = 1;
|
|
296
|
+
let inString = false;
|
|
297
|
+
let escaped = false;
|
|
298
|
+
const chunk = Buffer.alloc(HEAD_CHUNK_BYTES);
|
|
299
|
+
let { bytesRead } = await handle.read(chunk, 0, chunk.length, 0);
|
|
300
|
+
while (bytesRead > 0) {
|
|
301
|
+
head = Buffer.concat([head, chunk.subarray(0, bytesRead)]);
|
|
302
|
+
if (!head.subarray(0, RECORD_HEAD.length).equals(RECORD_HEAD.subarray(0, Math.min(head.length, RECORD_HEAD.length)))) {
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
for (; at < head.length; at++) {
|
|
306
|
+
const byte = head[at]!;
|
|
307
|
+
if (escaped) {
|
|
308
|
+
escaped = false;
|
|
309
|
+
} else if (inString) {
|
|
310
|
+
escaped = byte === BACKSLASH;
|
|
311
|
+
inString = byte !== QUOTE;
|
|
312
|
+
} else if (byte === QUOTE) {
|
|
313
|
+
inString = true;
|
|
314
|
+
} else if (byte === OPEN_BRACE) {
|
|
315
|
+
depth += 1;
|
|
316
|
+
} else if (byte === CLOSE_BRACE) {
|
|
317
|
+
depth -= 1;
|
|
318
|
+
if (depth === 0) {
|
|
319
|
+
return parseOrUndefined(head.subarray(RECORD_HEAD.length - 1, at + 1).toString('utf8'));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
({ bytesRead } = await handle.read(chunk, 0, chunk.length, head.length));
|
|
324
|
+
}
|
|
325
|
+
return undefined;
|
|
326
|
+
} finally {
|
|
327
|
+
await handle.close();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/* The info of the last info or reset line in a log, unvalidated; undefined when no line carries one. */
|
|
332
|
+
function lastLoggedInfo(raw: string): unknown {
|
|
333
|
+
const lines = raw.split('\n');
|
|
334
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
335
|
+
// Most lines are items and deltas, which are larger and never carry this key.
|
|
336
|
+
if (!lines[i]!.includes('"info"')) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const event = (parseOrUndefined(lines[i]!) as { event?: { type?: unknown; info?: unknown } } | null | undefined)?.event;
|
|
340
|
+
if (event?.type === 'info' || event?.type === 'reset') {
|
|
341
|
+
return event.info;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return undefined;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function settledInfo(info: unknown): boolean {
|
|
348
|
+
return typeof info === 'object' && info !== null && (info as { activeTurnId?: unknown }).activeTurnId === null && !('resumeAt' in info);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function parseOrUndefined(text: string): unknown {
|
|
352
|
+
try {
|
|
353
|
+
return JSON.parse(text);
|
|
354
|
+
} catch {
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
256
359
|
async function exists(path: string): Promise<boolean> {
|
|
257
360
|
try {
|
|
258
361
|
await stat(path);
|