@fungi.computer/parakeet 0.1.0 → 0.2.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/README.md +45 -6
- package/dist/account-commands.js +3 -11
- package/dist/commands/advertised.d.ts +3 -1
- package/dist/commands/advertised.js +89 -4
- package/dist/commands/interactions.d.ts +22 -7
- package/dist/commands/interactions.js +41 -17
- package/dist/commands/slash.js +3 -5
- package/dist/commands/use-slash-commands.js +2 -2
- package/dist/components/AgentHead.d.ts +2 -0
- package/dist/components/AgentHead.js +5 -4
- package/dist/components/ChatComposer.d.ts +3 -1
- package/dist/components/ChatComposer.js +79 -15
- package/dist/components/ChatMessage.d.ts +4 -0
- package/dist/components/ChatMessage.js +6 -2
- package/dist/components/ClientRows.js +2 -2
- package/dist/components/InteractionPanel.d.ts +2 -4
- package/dist/components/InteractionPanel.js +2 -2
- package/dist/components/TimelineItem.d.ts +2 -0
- package/dist/components/TimelineItem.js +31 -7
- package/dist/conversation-opening.d.ts +47 -0
- package/dist/conversation-opening.js +173 -0
- package/dist/conversation.d.ts +47 -3
- package/dist/conversation.js +584 -86
- package/dist/core/author-labels.d.ts +2 -0
- package/dist/core/build-timeline.d.ts +4 -2
- package/dist/core/build-timeline.js +46 -7
- package/dist/core/client-actions.js +6 -3
- package/dist/core/client-timeline.d.ts +9 -2
- package/dist/core/client-timeline.js +15 -4
- package/dist/core/draft-storage.d.ts +63 -0
- package/dist/core/draft-storage.js +50 -0
- package/dist/core/model-identity.d.ts +2 -0
- package/dist/core/model-identity.js +6 -0
- package/dist/core/queued-recall.d.ts +35 -0
- package/dist/core/queued-recall.js +138 -0
- package/dist/core/session-projection.js +17 -0
- package/dist/core/session-query.d.ts +54 -11
- package/dist/core/session-query.js +38 -12
- package/dist/core/tool-groups.d.ts +1 -1
- package/dist/core/tool-groups.js +3 -1
- package/dist/core/tool-view.js +2 -1
- package/dist/core/use-timeline-item-key.d.ts +8 -0
- package/dist/core/use-timeline-item-key.js +29 -0
- package/dist/headless.d.ts +6 -4
- package/dist/headless.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/model-commands.js +64 -39
- package/dist/parakeet.js +17 -18
- package/dist/session-commands.d.ts +0 -9
- package/dist/session-commands.js +39 -68
- package/dist/session.d.ts +1 -1
- package/dist/store/types.d.ts +10 -4
- package/dist/styles.css +27 -12
- package/docs/api/commands/slash.md +24 -21
- package/docs/api/headless.md +375 -232
- package/docs/api/index.md +157 -45
- package/package.json +16 -10
- package/src/styles.css +0 -9
|
@@ -5,6 +5,8 @@ export type ParakeetAuthor = Readonly<{
|
|
|
5
5
|
id: string;
|
|
6
6
|
/** The name shown beside the line. */
|
|
7
7
|
name: string;
|
|
8
|
+
/** Another Agent wrote the line; it shows as a compact block, not a bubble. */
|
|
9
|
+
kind?: "agent" | undefined;
|
|
8
10
|
}>;
|
|
9
11
|
/**
|
|
10
12
|
* The host's reading of a message's opaque metadata: the author to show, or
|
|
@@ -20,6 +20,8 @@ export type PromptRequestIdOf = (commandId: string) => string | undefined;
|
|
|
20
20
|
export type DurableTimelineProjection = TimelineProjection & Readonly<{
|
|
21
21
|
completedMessageIds: ReadonlySet<string>;
|
|
22
22
|
commandIds: ReadonlySet<string>;
|
|
23
|
+
requestIds: ReadonlySet<string>;
|
|
24
|
+
positionAnchors: ReadonlyMap<number, string | null>;
|
|
23
25
|
commandAnchors: ReadonlyMap<string, string>;
|
|
24
26
|
}>;
|
|
25
27
|
/**
|
|
@@ -29,8 +31,8 @@ export type DurableTimelineProjection = TimelineProjection & Readonly<{
|
|
|
29
31
|
* that changed nothing. The model's own context is unaffected by this purely
|
|
30
32
|
* client-side projection.
|
|
31
33
|
*/
|
|
32
|
-
export declare function buildDurableTimeline(messages: readonly SessionMessage[], compactions?: readonly SessionCompactionMarker[], activity?: SessionActivity, authorOf?: ParakeetAuthorOf, requestIdOf?: PromptRequestIdOf, notices?: readonly SessionCompactionNotice[]): DurableTimelineProjection;
|
|
34
|
+
export declare function buildDurableTimeline(messages: readonly SessionMessage[], compactions?: readonly SessionCompactionMarker[], activity?: SessionActivity, authorOf?: ParakeetAuthorOf, requestIdOf?: PromptRequestIdOf, notices?: readonly SessionCompactionNotice[], positions?: readonly number[], shown?: (item: TimelineItem) => boolean): DurableTimelineProjection;
|
|
33
35
|
/** Project replaceable live work without copying settled history. */
|
|
34
|
-
export declare function buildTimelineTail(durable: DurableTimelineProjection, progress: SessionProgress | null, queued: readonly SessionPendingPrompt[], admissions?: readonly LocalPromptAdmission[], authorOf?: ParakeetAuthorOf
|
|
36
|
+
export declare function buildTimelineTail(durable: DurableTimelineProjection, progress: SessionProgress | null, queued: readonly SessionPendingPrompt[], admissions?: readonly LocalPromptAdmission[], authorOf?: ParakeetAuthorOf): readonly TimelineItem[];
|
|
35
37
|
/** Overlay live progress and an optimistic prompt over stable durable rows. */
|
|
36
38
|
export declare function buildTimeline(durable: DurableTimelineProjection, progress: SessionProgress | null, queued: readonly SessionPendingPrompt[], admissions?: readonly LocalPromptAdmission[]): TimelineProjection;
|
|
@@ -118,7 +118,7 @@ function appendAssistant(items, toolIndexes, assistant) {
|
|
|
118
118
|
items.push(item);
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
|
-
function completeTool(items, toolIndexes, value) {
|
|
121
|
+
function completeTool(items, toolIndexes, value, data) {
|
|
122
122
|
const toolIndex = toolIndexes.get(value.toolCallId);
|
|
123
123
|
if (toolIndex === undefined)
|
|
124
124
|
return;
|
|
@@ -128,6 +128,7 @@ function completeTool(items, toolIndexes, value) {
|
|
|
128
128
|
items[toolIndex] = {
|
|
129
129
|
...tool,
|
|
130
130
|
result: value,
|
|
131
|
+
data,
|
|
131
132
|
status: value.isError ? "error" : "completed",
|
|
132
133
|
};
|
|
133
134
|
}
|
|
@@ -138,16 +139,18 @@ function completeTool(items, toolIndexes, value) {
|
|
|
138
139
|
* that changed nothing. The model's own context is unaffected by this purely
|
|
139
140
|
* client-side projection.
|
|
140
141
|
*/
|
|
141
|
-
export function buildDurableTimeline(messages, compactions = [], activity, authorOf = noAuthor, requestIdOf = noRequestId, notices = []) {
|
|
142
|
+
export function buildDurableTimeline(messages, compactions = [], activity, authorOf = noAuthor, requestIdOf = noRequestId, notices = [], positions = [], shown = () => true) {
|
|
142
143
|
const items = [];
|
|
143
144
|
const toolIndexes = new Map();
|
|
144
145
|
const commandIds = new Set();
|
|
146
|
+
const itemSequences = new Map();
|
|
145
147
|
const itemCommands = new Map();
|
|
146
148
|
let model;
|
|
147
149
|
let tokens = 0;
|
|
148
150
|
let cost = 0;
|
|
149
151
|
for (const source of orderedSources(messages, compactions, notices)) {
|
|
150
152
|
if (source.kind === "marker") {
|
|
153
|
+
itemSequences.set(source.marker.id, source.sequence);
|
|
151
154
|
items.push({
|
|
152
155
|
id: source.marker.id,
|
|
153
156
|
kind: "compaction_divider",
|
|
@@ -158,6 +161,7 @@ export function buildDurableTimeline(messages, compactions = [], activity, autho
|
|
|
158
161
|
continue;
|
|
159
162
|
}
|
|
160
163
|
if (source.kind === "notice") {
|
|
164
|
+
itemSequences.set(source.notice.id, source.sequence);
|
|
161
165
|
items.push({
|
|
162
166
|
id: source.notice.id,
|
|
163
167
|
kind: "compaction_notice",
|
|
@@ -186,7 +190,7 @@ export function buildDurableTimeline(messages, compactions = [], activity, autho
|
|
|
186
190
|
author: authorOf(message.meta),
|
|
187
191
|
origin: message.origin,
|
|
188
192
|
};
|
|
189
|
-
items.push(withRequestId(confirmed, requestIdOf(commandId)));
|
|
193
|
+
items.push(withRequestId(confirmed, message.requestId ?? requestIdOf(commandId)));
|
|
190
194
|
break;
|
|
191
195
|
}
|
|
192
196
|
case "assistant": {
|
|
@@ -198,7 +202,7 @@ export function buildDurableTimeline(messages, compactions = [], activity, autho
|
|
|
198
202
|
break;
|
|
199
203
|
}
|
|
200
204
|
case "toolResult":
|
|
201
|
-
completeTool(items, toolIndexes, value);
|
|
205
|
+
completeTool(items, toolIndexes, value, message.data);
|
|
202
206
|
if (message.commandId !== null) {
|
|
203
207
|
const commands = itemCommands.get(value.toolCallId) ?? new Set();
|
|
204
208
|
commands.add(message.commandId);
|
|
@@ -206,6 +210,8 @@ export function buildDurableTimeline(messages, compactions = [], activity, autho
|
|
|
206
210
|
}
|
|
207
211
|
break;
|
|
208
212
|
}
|
|
213
|
+
for (const item of items.slice(firstItem))
|
|
214
|
+
itemSequences.set(item.id, source.sequence);
|
|
209
215
|
if (message.commandId !== null) {
|
|
210
216
|
for (const item of items.slice(firstItem))
|
|
211
217
|
itemCommands.set(item.id, new Set([message.commandId]));
|
|
@@ -234,7 +240,18 @@ export function buildDurableTimeline(messages, compactions = [], activity, autho
|
|
|
234
240
|
if (active?.kind === "tool")
|
|
235
241
|
items[toolIndex] = { ...active, status: "running" };
|
|
236
242
|
}
|
|
237
|
-
|
|
243
|
+
// Later calls must not grow a tool group across a committed event.
|
|
244
|
+
const breakBefore = new Set();
|
|
245
|
+
let previousSequence = 0;
|
|
246
|
+
for (const item of items) {
|
|
247
|
+
const sequence = itemSequences.get(item.id);
|
|
248
|
+
if (sequence === undefined)
|
|
249
|
+
continue;
|
|
250
|
+
if (positions.some((position) => previousSequence <= position && position < sequence))
|
|
251
|
+
breakBefore.add(item.id);
|
|
252
|
+
previousSequence = sequence;
|
|
253
|
+
}
|
|
254
|
+
const grouped = groupToolCalls(items, breakBefore);
|
|
238
255
|
const commandAnchors = new Map();
|
|
239
256
|
for (const row of grouped) {
|
|
240
257
|
if (row.kind === "assistant_message" && !assistantMessageVisible(row))
|
|
@@ -244,18 +261,37 @@ export function buildDurableTimeline(messages, compactions = [], activity, autho
|
|
|
244
261
|
for (const command of itemCommands.get(member.id) ?? [])
|
|
245
262
|
commandAnchors.set(command, row.id);
|
|
246
263
|
}
|
|
264
|
+
const positionAnchors = new Map();
|
|
265
|
+
for (const position of positions) {
|
|
266
|
+
let anchor = null;
|
|
267
|
+
for (const row of grouped) {
|
|
268
|
+
if (!shown(row))
|
|
269
|
+
continue;
|
|
270
|
+
if (row.kind === "assistant_message" && !assistantMessageVisible(row))
|
|
271
|
+
continue;
|
|
272
|
+
const members = row.kind === "tool_group" ? row.tools : [row];
|
|
273
|
+
if (members.some((member) => {
|
|
274
|
+
const sequence = itemSequences.get(member.id);
|
|
275
|
+
return sequence !== undefined && sequence <= position;
|
|
276
|
+
}))
|
|
277
|
+
anchor = row.id;
|
|
278
|
+
}
|
|
279
|
+
positionAnchors.set(position, anchor);
|
|
280
|
+
}
|
|
247
281
|
const base = {
|
|
248
282
|
commandAnchors,
|
|
283
|
+
positionAnchors,
|
|
249
284
|
completedMessageIds: completedMessageIds(messages),
|
|
250
285
|
items: grouped,
|
|
251
286
|
commandIds,
|
|
287
|
+
requestIds: new Set(messages.flatMap((message) => message.requestId === undefined ? [] : [message.requestId])),
|
|
252
288
|
};
|
|
253
289
|
return model === undefined
|
|
254
290
|
? base
|
|
255
291
|
: { ...base, model, usage: { cost, tokens } };
|
|
256
292
|
}
|
|
257
293
|
/** Project replaceable live work without copying settled history. */
|
|
258
|
-
export function buildTimelineTail(durable, progress, queued, admissions = [], authorOf = noAuthor
|
|
294
|
+
export function buildTimelineTail(durable, progress, queued, admissions = [], authorOf = noAuthor) {
|
|
259
295
|
const visibleProgress = progress === null ||
|
|
260
296
|
durable.completedMessageIds.has(progress.messageId) ||
|
|
261
297
|
(progress.text === "" && progress.thinking === "")
|
|
@@ -280,9 +316,12 @@ export function buildTimelineTail(durable, progress, queued, admissions = [], au
|
|
|
280
316
|
origin: prompt.origin,
|
|
281
317
|
author: authorOf(prompt.meta),
|
|
282
318
|
};
|
|
283
|
-
items.push(withRequestId(waiting,
|
|
319
|
+
items.push(withRequestId(waiting, prompt.requestId));
|
|
284
320
|
}
|
|
285
321
|
for (const admission of admissions) {
|
|
322
|
+
if (durable.requestIds.has(admission.requestId) ||
|
|
323
|
+
queued.some((prompt) => prompt.requestId === admission.requestId))
|
|
324
|
+
continue;
|
|
286
325
|
if (admission.commandId !== undefined &&
|
|
287
326
|
(queuedCommandIds.has(admission.commandId) ||
|
|
288
327
|
durable.commandIds.has(admission.commandId)))
|
|
@@ -69,13 +69,16 @@ export function createClientActions(options) {
|
|
|
69
69
|
return;
|
|
70
70
|
publish(intent, { pending: true });
|
|
71
71
|
const notificationOnly = {
|
|
72
|
+
// Waiting cancellation has no interactive retry surface.
|
|
73
|
+
run: (operation) => operation(),
|
|
72
74
|
signal: controller.signal,
|
|
73
75
|
prompt: () => Promise.reject(new WhistleActionError("client_waiting_prompt", "A waiting cancellation cannot request an answer")),
|
|
74
76
|
notify: (notice) => {
|
|
75
|
-
|
|
77
|
+
// A success only confirms the person's own action: no echo.
|
|
78
|
+
if (retired() || notice.type === "success")
|
|
76
79
|
return;
|
|
77
|
-
options.interactions.
|
|
78
|
-
|
|
80
|
+
options.interactions.toast({
|
|
81
|
+
variant: "info",
|
|
79
82
|
message: notice.type === "handoff"
|
|
80
83
|
? (notice.message ?? notice.url)
|
|
81
84
|
: notice.message,
|
|
@@ -7,8 +7,15 @@ export type PlacedClientCell = Readonly<{
|
|
|
7
7
|
/** Producer event, unchanged by host placement. */
|
|
8
8
|
cell: ClientTimelineCellV1;
|
|
9
9
|
}>;
|
|
10
|
-
/**
|
|
11
|
-
export declare function placeClientTimeline(entries: readonly ClientContribution[], anchors: ReadonlyMap<string, string>): {
|
|
10
|
+
/** Fixed positions and command anchors follow loaded history, never event time. */
|
|
11
|
+
export declare function placeClientTimeline(entries: readonly ClientContribution[], anchors: ReadonlyMap<string, string>, positions?: ReadonlyMap<number, string | null>): {
|
|
12
|
+
/** Events before the first visible row of the root history page. */
|
|
13
|
+
before: Readonly<{
|
|
14
|
+
/** Namespace of the entry that produced this committed event. */
|
|
15
|
+
entryId: string;
|
|
16
|
+
/** Producer event, unchanged by host placement. */
|
|
17
|
+
cell: ClientTimelineCellV1;
|
|
18
|
+
}>[];
|
|
12
19
|
/** Loaded durable row IDs mapped to their following committed events. */
|
|
13
20
|
after: Map<string, Readonly<{
|
|
14
21
|
/** Namespace of the entry that produced this committed event. */
|
|
@@ -31,20 +31,29 @@ function mergeCells(cells) {
|
|
|
31
31
|
}
|
|
32
32
|
return merged;
|
|
33
33
|
}
|
|
34
|
-
/**
|
|
35
|
-
export function placeClientTimeline(entries, anchors) {
|
|
34
|
+
/** Fixed positions and command anchors follow loaded history, never event time. */
|
|
35
|
+
export function placeClientTimeline(entries, anchors, positions = new Map()) {
|
|
36
36
|
const anchored = new Map();
|
|
37
|
+
const before = [];
|
|
37
38
|
const unanchored = [];
|
|
38
39
|
for (const { entryId, value } of entries) {
|
|
39
40
|
for (const cell of value.timeline.cells) {
|
|
40
41
|
const placed = { entryId, cell };
|
|
41
|
-
if (cell.commandId === null) {
|
|
42
|
+
if (cell.afterSequence === undefined && cell.commandId === null) {
|
|
42
43
|
unanchored.push(placed);
|
|
43
44
|
continue;
|
|
44
45
|
}
|
|
45
|
-
const itemId =
|
|
46
|
+
const itemId = cell.afterSequence !== undefined
|
|
47
|
+
? positions.get(cell.afterSequence)
|
|
48
|
+
: cell.commandId === null
|
|
49
|
+
? undefined
|
|
50
|
+
: anchors.get(cell.commandId);
|
|
46
51
|
if (itemId === undefined)
|
|
47
52
|
continue;
|
|
53
|
+
if (itemId === null) {
|
|
54
|
+
before.push(placed);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
48
57
|
const existing = anchored.get(itemId);
|
|
49
58
|
if (existing === undefined)
|
|
50
59
|
anchored.set(itemId, [placed]);
|
|
@@ -53,6 +62,8 @@ export function placeClientTimeline(entries, anchors) {
|
|
|
53
62
|
}
|
|
54
63
|
}
|
|
55
64
|
return {
|
|
65
|
+
/** Events before the first visible row of the root history page. */
|
|
66
|
+
before: mergeCells(before),
|
|
56
67
|
/** Loaded durable row IDs mapped to their following committed events. */
|
|
57
68
|
after: new Map([...anchored].map(([id, cells]) => [id, mergeCells(cells)])),
|
|
58
69
|
/** Null-anchor events for the separate Extension changes section. */
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Durable owner identity, independent of the current service endpoint. */
|
|
3
|
+
export type ParakeetDraftIdentity = Readonly<{
|
|
4
|
+
/** Agent that owns the Session. */
|
|
5
|
+
agentId: string;
|
|
6
|
+
/** Durable Session identity, not its transport address. */
|
|
7
|
+
sessionId: string;
|
|
8
|
+
}>;
|
|
9
|
+
/** Hosts must commit a replacement before returning, or throw without changing it. */
|
|
10
|
+
export type ParakeetDraftStorage = Readonly<{
|
|
11
|
+
/** Read the complete journal, or null when no draft has been saved. */
|
|
12
|
+
getItem: (key: string) => string | null;
|
|
13
|
+
/** Atomically replace the journal; throw if it cannot be committed. */
|
|
14
|
+
setItem: (key: string, value: string) => void;
|
|
15
|
+
}>;
|
|
16
|
+
declare const journal: z.ZodObject<{
|
|
17
|
+
draft: z.ZodString;
|
|
18
|
+
images: z.ZodOptional<z.ZodReadonly<z.ZodArray<z.ZodObject<{
|
|
19
|
+
type: z.ZodLiteral<"image">;
|
|
20
|
+
data: z.ZodString;
|
|
21
|
+
mimeType: z.ZodString;
|
|
22
|
+
}, z.core.$strip>>>>;
|
|
23
|
+
delivery: z.ZodOptional<z.ZodEnum<{
|
|
24
|
+
steer: "steer";
|
|
25
|
+
followUp: "followUp";
|
|
26
|
+
}>>;
|
|
27
|
+
recall: z.ZodOptional<z.ZodObject<{
|
|
28
|
+
commandId: z.ZodString;
|
|
29
|
+
requestId: z.ZodString;
|
|
30
|
+
text: z.ZodString;
|
|
31
|
+
images: z.ZodOptional<z.ZodReadonly<z.ZodArray<z.ZodObject<{
|
|
32
|
+
type: z.ZodLiteral<"image">;
|
|
33
|
+
data: z.ZodString;
|
|
34
|
+
mimeType: z.ZodString;
|
|
35
|
+
}, z.core.$strip>>>>;
|
|
36
|
+
delivery: z.ZodEnum<{
|
|
37
|
+
steer: "steer";
|
|
38
|
+
followUp: "followUp";
|
|
39
|
+
}>;
|
|
40
|
+
}, z.core.$strip>>;
|
|
41
|
+
pending: z.ZodArray<z.ZodObject<{
|
|
42
|
+
requestId: z.ZodString;
|
|
43
|
+
content: z.ZodString;
|
|
44
|
+
submittedDraft: z.ZodString;
|
|
45
|
+
delivery: z.ZodEnum<{
|
|
46
|
+
steer: "steer";
|
|
47
|
+
followUp: "followUp";
|
|
48
|
+
}>;
|
|
49
|
+
images: z.ZodOptional<z.ZodReadonly<z.ZodArray<z.ZodObject<{
|
|
50
|
+
type: z.ZodLiteral<"image">;
|
|
51
|
+
data: z.ZodString;
|
|
52
|
+
mimeType: z.ZodString;
|
|
53
|
+
}, z.core.$strip>>>>;
|
|
54
|
+
restoreDraft: z.ZodBoolean;
|
|
55
|
+
}, z.core.$strip>>;
|
|
56
|
+
}, z.core.$strip>;
|
|
57
|
+
type DraftJournal = z.infer<typeof journal>;
|
|
58
|
+
/** One owner-scoped journal commits pending text and composer text together. */
|
|
59
|
+
export declare function draftJournal(scope: string, storage: ParakeetDraftStorage | undefined): {
|
|
60
|
+
read(): DraftJournal;
|
|
61
|
+
write(value: DraftJournal): void;
|
|
62
|
+
};
|
|
63
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const images = z
|
|
3
|
+
.array(z.object({
|
|
4
|
+
type: z.literal("image"),
|
|
5
|
+
data: z.string(),
|
|
6
|
+
mimeType: z.string(),
|
|
7
|
+
}))
|
|
8
|
+
.readonly();
|
|
9
|
+
const delivery = z.enum(["steer", "followUp"]);
|
|
10
|
+
const recalledPrompt = z.object({
|
|
11
|
+
commandId: z.string(),
|
|
12
|
+
requestId: z.string(),
|
|
13
|
+
text: z.string(),
|
|
14
|
+
images: images.optional(),
|
|
15
|
+
delivery,
|
|
16
|
+
});
|
|
17
|
+
const pendingDraft = z.object({
|
|
18
|
+
requestId: z.string().min(1),
|
|
19
|
+
content: z.string(),
|
|
20
|
+
submittedDraft: z.string(),
|
|
21
|
+
delivery,
|
|
22
|
+
images: images.optional(),
|
|
23
|
+
restoreDraft: z.boolean(),
|
|
24
|
+
});
|
|
25
|
+
const journal = z.object({
|
|
26
|
+
draft: z.string(),
|
|
27
|
+
images: images.optional(),
|
|
28
|
+
delivery: delivery.optional(),
|
|
29
|
+
recall: recalledPrompt.optional(),
|
|
30
|
+
pending: z.array(pendingDraft).max(32),
|
|
31
|
+
});
|
|
32
|
+
/** One owner-scoped journal commits pending text and composer text together. */
|
|
33
|
+
export function draftJournal(scope, storage) {
|
|
34
|
+
const key = `fungi.parakeet.drafts.v1:${scope}`;
|
|
35
|
+
return {
|
|
36
|
+
read() {
|
|
37
|
+
if (storage === undefined)
|
|
38
|
+
return { draft: "", pending: [] };
|
|
39
|
+
const saved = storage.getItem(key);
|
|
40
|
+
return saved === null
|
|
41
|
+
? { draft: "", pending: [] }
|
|
42
|
+
: journal.parse(JSON.parse(saved));
|
|
43
|
+
},
|
|
44
|
+
write(value) {
|
|
45
|
+
if (storage === undefined)
|
|
46
|
+
throw new Error("Durable draft storage is unavailable");
|
|
47
|
+
storage.setItem(key, JSON.stringify(value));
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import type { ParakeetModel } from "../conversation.js";
|
|
2
2
|
/** The durable assistant's provider/model ids, shared by browser and terminal. */
|
|
3
3
|
export declare function formatModelIdentity(model: ParakeetModel): string;
|
|
4
|
+
/** The model identity with its thinking level, "provider/model (level)"; no level, no suffix. */
|
|
5
|
+
export declare function formatModelWithLevel(model: ParakeetModel, level: string | undefined): string;
|
|
@@ -5,3 +5,9 @@ export function formatModelIdentity(model) {
|
|
|
5
5
|
? model.model
|
|
6
6
|
: `${prefix}${model.model}`;
|
|
7
7
|
}
|
|
8
|
+
/** The model identity with its thinking level, "provider/model (level)"; no level, no suffix. */
|
|
9
|
+
export function formatModelWithLevel(model, level) {
|
|
10
|
+
return level === undefined
|
|
11
|
+
? formatModelIdentity(model)
|
|
12
|
+
: `${formatModelIdentity(model)} (${level})`;
|
|
13
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { SessionPendingPrompt, ShiitakeSession } from "@fungi.computer/shiitake/client";
|
|
2
|
+
/** Ephemeral composer reservation, never an admission journal or queue. */
|
|
3
|
+
export type QueuedRecallState = Readonly<{
|
|
4
|
+
/** Whether recall is available, reserved, or restored into the composer. */
|
|
5
|
+
kind: "idle" | "recalling" | "restored";
|
|
6
|
+
}> | Readonly<{
|
|
7
|
+
/** A definitive refusal or an unresolved owner response. */
|
|
8
|
+
kind: "too_late" | "uncertain";
|
|
9
|
+
/** Explanation suitable for the host's existing toast surface. */
|
|
10
|
+
message: string;
|
|
11
|
+
}>;
|
|
12
|
+
/** Result of one recall gesture; an empty queue permits local input history. */
|
|
13
|
+
export type QueuedRecallOutcome = "ignored" | "empty" | "restored" | "too_late" | "uncertain";
|
|
14
|
+
/** Composer boundary consumed by the queued-recall owner. */
|
|
15
|
+
export type RecallComposer = Readonly<{
|
|
16
|
+
session: Pick<ShiitakeSession, "latestPending" | "cancel">;
|
|
17
|
+
recovery: SessionPendingPrompt | undefined;
|
|
18
|
+
/** Commit in the existing Session draft journal before cancellation can start. */
|
|
19
|
+
reserve: (prompt: SessionPendingPrompt) => void;
|
|
20
|
+
/** Clear that journal's reservation after a definitive TooLate. */
|
|
21
|
+
forget: () => void;
|
|
22
|
+
/** Undefined when text, attachments or a pending paste make recall unsafe. */
|
|
23
|
+
emptyDraftRevision: () => number | undefined;
|
|
24
|
+
/** Atomically restore the full payload iff this empty revision still owns input. */
|
|
25
|
+
restore: (prompt: SessionPendingPrompt, revision: number) => boolean;
|
|
26
|
+
/** Hosts use their existing toast seam and derive read-only from `recalling`. */
|
|
27
|
+
onState: (state: QueuedRecallState) => void;
|
|
28
|
+
}>;
|
|
29
|
+
export type QueuedRecall = Readonly<{
|
|
30
|
+
readonly state: QueuedRecallState;
|
|
31
|
+
recall: () => Promise<QueuedRecallOutcome>;
|
|
32
|
+
dispose: () => void;
|
|
33
|
+
}>;
|
|
34
|
+
/** One instance belongs to one mounted Session composer, not a surface or Agent. */
|
|
35
|
+
export declare function createQueuedRecall(composer: RecallComposer): QueuedRecall;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/** A private UI wait ceiling; it does not cancel or reinterpret owner work. */
|
|
2
|
+
const recallWaitMs = 5_000;
|
|
3
|
+
const tooLateMessage = "Too late: that message already started";
|
|
4
|
+
function failureMessage(cause) {
|
|
5
|
+
return cause instanceof Error
|
|
6
|
+
? cause.message
|
|
7
|
+
: "Could not recall queued message";
|
|
8
|
+
}
|
|
9
|
+
/** One instance belongs to one mounted Session composer, not a surface or Agent. */
|
|
10
|
+
export function createQueuedRecall(composer) {
|
|
11
|
+
let state = { kind: "idle" };
|
|
12
|
+
let target = composer.recovery === undefined ? undefined : { prompt: composer.recovery };
|
|
13
|
+
let disposed = false;
|
|
14
|
+
const isDisposed = () => disposed;
|
|
15
|
+
let releaseWait;
|
|
16
|
+
let pendingDecision;
|
|
17
|
+
const update = (next) => {
|
|
18
|
+
state = next;
|
|
19
|
+
composer.onState(next);
|
|
20
|
+
};
|
|
21
|
+
async function decide(revision, attempt) {
|
|
22
|
+
if (target === undefined) {
|
|
23
|
+
const prompt = await composer.session.latestPending();
|
|
24
|
+
// A late lookup must never begin cancellation after input was unlocked.
|
|
25
|
+
if (!attempt.active || isDisposed())
|
|
26
|
+
return { kind: "ignored" };
|
|
27
|
+
if (composer.emptyDraftRevision() !== revision)
|
|
28
|
+
return { kind: "ignored" };
|
|
29
|
+
if (prompt === null)
|
|
30
|
+
return { kind: "empty" };
|
|
31
|
+
composer.reserve(prompt);
|
|
32
|
+
target = { prompt };
|
|
33
|
+
}
|
|
34
|
+
const selected = target;
|
|
35
|
+
const result = selected.result ??
|
|
36
|
+
(await composer.session.cancel({
|
|
37
|
+
commandId: selected.prompt.commandId,
|
|
38
|
+
onlyIfPending: true,
|
|
39
|
+
}));
|
|
40
|
+
// An ambiguous timeout retains this result for a deliberate subsequent Up.
|
|
41
|
+
// Never mutate another target or restore input from a late response.
|
|
42
|
+
if (!isDisposed() && target === selected)
|
|
43
|
+
selected.result = result;
|
|
44
|
+
return { kind: "decided", target: selected, result };
|
|
45
|
+
}
|
|
46
|
+
async function recall() {
|
|
47
|
+
const revision = composer.emptyDraftRevision();
|
|
48
|
+
if (isDisposed() || state.kind === "recalling" || revision === undefined)
|
|
49
|
+
return "ignored";
|
|
50
|
+
// A UI timeout cannot abort the owner's Promise. Do not accumulate hidden
|
|
51
|
+
// requests or waiters while it is unsettled; another Up can recover its
|
|
52
|
+
// result once it arrives, or retry this exact target after a rejection.
|
|
53
|
+
if (pendingDecision !== undefined)
|
|
54
|
+
return "uncertain";
|
|
55
|
+
update({ kind: "recalling" });
|
|
56
|
+
const attempt = { active: true };
|
|
57
|
+
const interrupted = new Promise((resolve) => {
|
|
58
|
+
releaseWait = () => {
|
|
59
|
+
resolve({ kind: "ignored" });
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
let timer;
|
|
63
|
+
const timeout = new Promise((resolve) => {
|
|
64
|
+
timer = setTimeout(() => {
|
|
65
|
+
attempt.active = false;
|
|
66
|
+
resolve({
|
|
67
|
+
kind: "uncertain",
|
|
68
|
+
message: "Recall is unconfirmed. Press Up on an empty composer to retry.",
|
|
69
|
+
});
|
|
70
|
+
}, recallWaitMs);
|
|
71
|
+
});
|
|
72
|
+
pendingDecision = decide(revision, attempt)
|
|
73
|
+
.catch((cause) => ({
|
|
74
|
+
kind: "uncertain",
|
|
75
|
+
message: failureMessage(cause),
|
|
76
|
+
}))
|
|
77
|
+
.finally(() => {
|
|
78
|
+
pendingDecision = undefined;
|
|
79
|
+
});
|
|
80
|
+
const decision = await Promise.race([
|
|
81
|
+
pendingDecision,
|
|
82
|
+
timeout,
|
|
83
|
+
interrupted,
|
|
84
|
+
]);
|
|
85
|
+
attempt.active = false;
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
releaseWait = undefined;
|
|
88
|
+
if (isDisposed())
|
|
89
|
+
return "ignored";
|
|
90
|
+
if (decision.kind === "ignored" || decision.kind === "empty") {
|
|
91
|
+
update({ kind: "idle" });
|
|
92
|
+
return decision.kind;
|
|
93
|
+
}
|
|
94
|
+
if (decision.kind === "uncertain") {
|
|
95
|
+
update(decision);
|
|
96
|
+
return "uncertain";
|
|
97
|
+
}
|
|
98
|
+
if (decision.result.status === "too_late") {
|
|
99
|
+
try {
|
|
100
|
+
composer.forget();
|
|
101
|
+
}
|
|
102
|
+
catch (cause) {
|
|
103
|
+
update({ kind: "uncertain", message: failureMessage(cause) });
|
|
104
|
+
return "uncertain";
|
|
105
|
+
}
|
|
106
|
+
target = undefined;
|
|
107
|
+
update({ kind: "too_late", message: tooLateMessage });
|
|
108
|
+
return "too_late";
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
if (composer.restore(decision.target.prompt, revision)) {
|
|
112
|
+
target = undefined;
|
|
113
|
+
update({ kind: "restored" });
|
|
114
|
+
return "restored";
|
|
115
|
+
}
|
|
116
|
+
update({
|
|
117
|
+
kind: "uncertain",
|
|
118
|
+
message: "Message recalled. Clear the composer and press Up to restore it.",
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
catch (cause) {
|
|
122
|
+
// Keep the accepted payload if, for example, the existing draft journal
|
|
123
|
+
// refused the restore. A later Up retries restoration, not cancellation.
|
|
124
|
+
update({ kind: "uncertain", message: failureMessage(cause) });
|
|
125
|
+
}
|
|
126
|
+
return "uncertain";
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
get state() {
|
|
130
|
+
return state;
|
|
131
|
+
},
|
|
132
|
+
recall,
|
|
133
|
+
dispose: () => {
|
|
134
|
+
disposed = true;
|
|
135
|
+
releaseWait?.();
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -116,6 +116,18 @@ export function applySessionFrame(current, frame) {
|
|
|
116
116
|
compactions: mergeCompactions([], frame.baseline.compactions ?? []),
|
|
117
117
|
compactionNotices: mergeCompactionNotices([], frame.baseline.compactionNotices ?? []),
|
|
118
118
|
};
|
|
119
|
+
if (frame.kind === "tool_output") {
|
|
120
|
+
const completed = new Set(current.messages.flatMap((message) => message.value.role === "toolResult" ? [message.value.toolCallId] : []));
|
|
121
|
+
const toolOutput = frame.toolOutput
|
|
122
|
+
.filter((slot) => !completed.has(slot.toolCallId))
|
|
123
|
+
.map((slot) => {
|
|
124
|
+
const previous = current.toolOutput.find((value) => value.toolCallId === slot.toolCallId);
|
|
125
|
+
return previous !== undefined && previous.revision > slot.revision
|
|
126
|
+
? previous
|
|
127
|
+
: slot;
|
|
128
|
+
});
|
|
129
|
+
return { ...current, activity: frame.activity, toolOutput };
|
|
130
|
+
}
|
|
119
131
|
if (frame.kind === "progress") {
|
|
120
132
|
const completed = completedMessageIds(current.messages).has(frame.progress.messageId);
|
|
121
133
|
const currentProgress = current.progress;
|
|
@@ -141,6 +153,9 @@ export function applySessionFrame(current, frame) {
|
|
|
141
153
|
? current.pending
|
|
142
154
|
: frame.pending;
|
|
143
155
|
const messages = mergeMessages(current.messages, frame.messages);
|
|
156
|
+
const completedTools = new Set(messages.flatMap((message) => message.value.role === "toolResult" ? [message.value.toolCallId] : []));
|
|
157
|
+
const toolOutput = current.toolOutput.filter((slot) => frame.activity.queue.state !== "idle" &&
|
|
158
|
+
!completedTools.has(slot.toolCallId));
|
|
144
159
|
const terminals = mergeTerminals(current.terminals, frame.terminals);
|
|
145
160
|
const currentCompactions = current.compactions ?? [];
|
|
146
161
|
const compactions = mergeCompactions(currentCompactions, frame.compactions ?? []);
|
|
@@ -160,6 +175,7 @@ export function applySessionFrame(current, frame) {
|
|
|
160
175
|
compactions === currentCompactions &&
|
|
161
176
|
compactionNotices === currentCompactionNotices &&
|
|
162
177
|
progress === current.progress &&
|
|
178
|
+
toolOutput.length === current.toolOutput.length &&
|
|
163
179
|
pending === current.pending &&
|
|
164
180
|
sameExtensionSlots(frame.extensions, current.extensions) &&
|
|
165
181
|
sameAttention(frame.attention, current.attention) &&
|
|
@@ -175,6 +191,7 @@ export function applySessionFrame(current, frame) {
|
|
|
175
191
|
compactions,
|
|
176
192
|
compactionNotices,
|
|
177
193
|
progress,
|
|
194
|
+
toolOutput,
|
|
178
195
|
activity: sameActivity(current.activity, frame.activity)
|
|
179
196
|
? current.activity
|
|
180
197
|
: frame.activity,
|