@camox/api-contract 0.54.0 → 0.57.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/drafts.d.ts +2 -2
- package/dist/drafts.js +1 -1
- package/dist/patrol.d.ts +114 -0
- package/dist/patrol.js +8 -0
- package/dist/query-keys.d.ts +4 -1
- package/dist/query-keys.js +2 -1
- package/dist/router.d.ts +473 -248
- package/dist/threads.d.ts +79 -26
- package/dist/threads.js +90 -24
- package/package.json +5 -1
- package/src/drafts.ts +2 -2
- package/src/patrol.ts +113 -0
- package/src/query-keys.ts +5 -1
- package/src/threads.ts +131 -34
package/src/threads.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { AssistantMessage, Message } from "@earendil-works/pi-ai";
|
|
|
2
2
|
import type { AgentEvent, EntryRecord } from "@earendil-works/pi-durable";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
5
|
+
import type { DraftOutcome } from "./drafts.ts";
|
|
6
|
+
|
|
5
7
|
// A thread with Camox's agent, as the studio shows it. The thread's Durable Object sends Pi's own
|
|
6
8
|
// agent events over its socket, and this module is the one place that interprets them: the
|
|
7
9
|
// studio and the API's tests fold them with the same code.
|
|
@@ -29,6 +31,41 @@ export const BUILD_STATUS_ENTRY = "camox.build-status";
|
|
|
29
31
|
*/
|
|
30
32
|
export const RECONCILE_ENTRY = "camox.reconcile";
|
|
31
33
|
|
|
34
|
+
/**
|
|
35
|
+
* The kind of the transcript entry opening a draft's thread a Live thread handed a request off
|
|
36
|
+
* to: the Brief, which the model reads and editors see as from Camox, ahead of the editor's
|
|
37
|
+
* messages.
|
|
38
|
+
*/
|
|
39
|
+
export const BRIEF_ENTRY = "camox.brief";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The kind of the transcript entry telling the agent on Live that a draft it started ended,
|
|
43
|
+
* published or deleted, with the draft's id as data (`{ draftId }`). The model reads
|
|
44
|
+
* it at the end of the conversation; editors never see it, since the draft's card says so.
|
|
45
|
+
*/
|
|
46
|
+
export const DRAFT_ENDED_ENTRY = "camox.draft-ended";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The agent's tool on Live handing a request off to a draft it starts (ADR 0025). A run whose
|
|
50
|
+
* call of it went through ends on the draft's card.
|
|
51
|
+
*/
|
|
52
|
+
export const START_DRAFT_TOOL = "startDraft";
|
|
53
|
+
|
|
54
|
+
/** The draft a `startDraft` call started, as its result's details name it for the card. */
|
|
55
|
+
export const startedDraftCardSchema = z.object({ draftId: z.string(), name: z.string() });
|
|
56
|
+
export type StartedDraftCard = z.infer<typeof startedDraftCardSchema>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Where a draft a Live thread's card names stands, as the thread looks it up whenever it's shown
|
|
60
|
+
* (ADR 0025): its current name, or its last once it ended, and how it ended. Never written into
|
|
61
|
+
* the transcript, so the model's cached prompt keeps up. A name cleared by a rename is null: the
|
|
62
|
+
* card keeps the name the agent gave it.
|
|
63
|
+
*/
|
|
64
|
+
export type DraftCardState = { name: string | null; ended?: DraftOutcome };
|
|
65
|
+
|
|
66
|
+
/** The drafts a Live thread's cards name, by id, as the thread last looked them up. */
|
|
67
|
+
export type DraftCardStates = Readonly<Record<string, DraftCardState>>;
|
|
68
|
+
|
|
32
69
|
export const reconcileDataSchema = z.object({ message: z.string() });
|
|
33
70
|
|
|
34
71
|
export const buildStatusSchema = z.enum(["building", "fixing"]);
|
|
@@ -48,15 +85,19 @@ export type ThreadSender = z.infer<typeof threadSenderSchema>;
|
|
|
48
85
|
export const isSender = (data: unknown): data is ThreadSender =>
|
|
49
86
|
threadSenderSchema.safeParse(data).success;
|
|
50
87
|
|
|
51
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* What an editor's message entry records: who sent it, the operation id it was sent with, and for
|
|
90
|
+
* a message sent on Live as the agent handed the request off, the draft it went to with it.
|
|
91
|
+
*/
|
|
52
92
|
export const editorMessageDataSchema = threadSenderSchema.extend({
|
|
53
93
|
operationId: z.string().optional(),
|
|
94
|
+
handedOffTo: z.string().optional(),
|
|
54
95
|
});
|
|
55
96
|
|
|
56
97
|
/**
|
|
57
|
-
* A draft
|
|
58
|
-
*
|
|
59
|
-
*
|
|
98
|
+
* A draft the agent on Live started with `startDraft`, and the operation ids of the messages of
|
|
99
|
+
* the run that started it: the studio that sent one of them enters the draft, if it still shows
|
|
100
|
+
* Live.
|
|
60
101
|
*/
|
|
61
102
|
export const startedDraftSchema = z.object({
|
|
62
103
|
draftId: z.string(),
|
|
@@ -119,22 +160,16 @@ export type ThreadMessage =
|
|
|
119
160
|
*/
|
|
120
161
|
| { id: string; from: "failed" }
|
|
121
162
|
/** In place of the agent's message, for a turn an editor stopped: `sender` stopped it. */
|
|
122
|
-
| { id: string; from: "stopped"; sender: ThreadSender }
|
|
123
|
-
|
|
124
|
-
/** Where a Live thread's messages went when it started a draft, beside its transcript. */
|
|
125
|
-
type LiveThreadHandoff = {
|
|
163
|
+
| { id: string; from: "stopped"; sender: ThreadSender }
|
|
126
164
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
165
|
+
* In place of the agent's message, for a Live turn handed off to a draft the agent started:
|
|
166
|
+
* the draft's card, with its current name, which opens it. Once the draft ended, it has its
|
|
167
|
+
* last name and how it ended, and opens nothing. The turn is finished.
|
|
130
168
|
*/
|
|
131
|
-
|
|
132
|
-
/** The draft the editor's previous Live thread continued in, for a fresh Live thread. */
|
|
133
|
-
continuedIn?: StartedDraft;
|
|
134
|
-
};
|
|
169
|
+
| ({ id: string; from: "draft"; ended?: DraftOutcome } & StartedDraftCard);
|
|
135
170
|
|
|
136
171
|
/** Everything the studio shows for a thread. */
|
|
137
|
-
export type ThreadView =
|
|
172
|
+
export type ThreadView = {
|
|
138
173
|
messages: readonly ThreadMessage[];
|
|
139
174
|
/** Whether a run is going: the agent is working on a turn, and stopping would end it. */
|
|
140
175
|
running: boolean;
|
|
@@ -157,20 +192,28 @@ export const EMPTY_THREAD_VIEW: ThreadView = { messages: [], running: false };
|
|
|
157
192
|
* and whether a run is going. The studio and the tests fold Pi's events into this, and derive
|
|
158
193
|
* the view from it with `threadViewOf`, so a snapshot and the events that led to it show the same.
|
|
159
194
|
*/
|
|
160
|
-
export type ThreadState =
|
|
195
|
+
export type ThreadState = {
|
|
161
196
|
steps: readonly ThreadStep[];
|
|
162
197
|
running: boolean;
|
|
163
198
|
/** The model's calls still running, by call id. A tool's own calls aren't the agent's. */
|
|
164
199
|
toolCalls?: readonly string[];
|
|
165
200
|
/** Whether the model's latest message makes tool calls: it writes them, then they run. */
|
|
166
201
|
callingTools?: boolean;
|
|
202
|
+
/** The drafts the thread's cards name, as the thread last looked them up. */
|
|
203
|
+
drafts?: DraftCardStates;
|
|
167
204
|
};
|
|
168
205
|
|
|
169
206
|
export const EMPTY_THREAD_STATE: ThreadState = { steps: [], running: false };
|
|
170
207
|
|
|
171
208
|
/** One transcript entry, as far as the studio's view is concerned. */
|
|
172
209
|
type ThreadStep = { id: string } & (
|
|
173
|
-
| {
|
|
210
|
+
| {
|
|
211
|
+
type: "editor";
|
|
212
|
+
sender: ThreadSender;
|
|
213
|
+
text: string;
|
|
214
|
+
/** The draft the message went to, as the agent on Live handed the request off. */
|
|
215
|
+
handedOffTo?: string;
|
|
216
|
+
}
|
|
174
217
|
/** Camox starting a turn of its own, saying why. */
|
|
175
218
|
| { type: "camox"; text: string }
|
|
176
219
|
| { type: "answer"; text: string }
|
|
@@ -179,6 +222,8 @@ type ThreadStep = { id: string } & (
|
|
|
179
222
|
/** A model call that ended without an answer: failed, aborted, or cut short. */
|
|
180
223
|
| { type: "unfinished" }
|
|
181
224
|
| { type: "stopped"; sender: ThreadSender }
|
|
225
|
+
/** A `startDraft` call that went through, which ends the turn on the draft's card. */
|
|
226
|
+
| ({ type: "draft" } & StartedDraftCard)
|
|
182
227
|
/** Where the build of the turn's code stands, as the turn ends. */
|
|
183
228
|
| { type: "build"; status: BuildStatus }
|
|
184
229
|
/**
|
|
@@ -211,13 +256,16 @@ export type ThreadServerMessage =
|
|
|
211
256
|
| { type: "events"; events: readonly AgentEvent[] }
|
|
212
257
|
/** The thread recorded the message sent with this operation id (once, however often sent). */
|
|
213
258
|
| { type: "accepted"; operationId: string }
|
|
214
|
-
/**
|
|
259
|
+
/**
|
|
260
|
+
* The agent on Live started a draft with `startDraft`: the studio that sent a message of the
|
|
261
|
+
* run enters it, if it still shows Live. Live only: a snapshot never replays it.
|
|
262
|
+
*/
|
|
215
263
|
| { type: "started-draft"; startedDraft: StartedDraft }
|
|
216
264
|
/**
|
|
217
|
-
*
|
|
218
|
-
* the
|
|
265
|
+
* Where the drafts a Live thread's cards name stand, after each snapshot and whenever one of
|
|
266
|
+
* them is renamed or ends. It replaces what the studio knew of them.
|
|
219
267
|
*/
|
|
220
|
-
| { type: "
|
|
268
|
+
| { type: "drafts"; drafts: DraftCardStates }
|
|
221
269
|
/**
|
|
222
270
|
* A code-mode script that wrote something ended, failed or not: what its successful writes
|
|
223
271
|
* touched. Sent once per such script, live only: a snapshot never replays past touches.
|
|
@@ -232,7 +280,8 @@ export type ThreadServerMessage =
|
|
|
232
280
|
*/
|
|
233
281
|
export const THREAD_ENDED_CLOSE_CODE = 4000;
|
|
234
282
|
|
|
235
|
-
|
|
283
|
+
/** The text of a message's content: its text parts, joined. */
|
|
284
|
+
export const textOf = (content: Message["content"]) =>
|
|
236
285
|
typeof content === "string"
|
|
237
286
|
? content
|
|
238
287
|
: content
|
|
@@ -264,12 +313,24 @@ function threadStepOf(entry: EntryRecord): ThreadStep | undefined {
|
|
|
264
313
|
const build = buildStatusDataSchema.safeParse(entry.data).data;
|
|
265
314
|
return build && { id, type: "build", status: build.status };
|
|
266
315
|
}
|
|
316
|
+
// Only the model reads it: as a user message, it would otherwise read as Pi continuing a run.
|
|
317
|
+
if (entry.kind === DRAFT_ENDED_ENTRY) return undefined;
|
|
267
318
|
const message = entry.model?.find((candidate) => candidate.role !== "system");
|
|
268
319
|
if (!message) return undefined;
|
|
269
320
|
if (message.role === "user") {
|
|
270
321
|
if (entry.kind === EDITOR_MESSAGE_ENTRY) {
|
|
271
|
-
|
|
322
|
+
const handedOffTo = editorMessageDataSchema.safeParse(entry.data).data?.handedOffTo;
|
|
323
|
+
return (
|
|
324
|
+
sender && {
|
|
325
|
+
id,
|
|
326
|
+
type: "editor",
|
|
327
|
+
sender,
|
|
328
|
+
text: textOf(message.content),
|
|
329
|
+
...(handedOffTo && { handedOffTo }),
|
|
330
|
+
}
|
|
331
|
+
);
|
|
272
332
|
}
|
|
333
|
+
if (entry.kind === BRIEF_ENTRY) return { id, type: "camox", text: textOf(message.content) };
|
|
273
334
|
if (entry.kind === RECONCILE_ENTRY) {
|
|
274
335
|
const reconcile = reconcileDataSchema.safeParse(entry.data).data;
|
|
275
336
|
return reconcile && { id, type: "camox", text: reconcile.message };
|
|
@@ -279,15 +340,21 @@ function threadStepOf(entry: EntryRecord): ThreadStep | undefined {
|
|
|
279
340
|
return { id, type: "continued" };
|
|
280
341
|
}
|
|
281
342
|
if (message.role === "assistant") return assistantStep(id, message);
|
|
282
|
-
if (message.role === "toolResult")
|
|
343
|
+
if (message.role === "toolResult") {
|
|
344
|
+
const card =
|
|
345
|
+
message.toolName === START_DRAFT_TOOL && !message.isError
|
|
346
|
+
? startedDraftCardSchema.safeParse(message.details).data
|
|
347
|
+
: undefined;
|
|
348
|
+
return card ? { id, type: "draft", ...card } : { id, type: "tools" };
|
|
349
|
+
}
|
|
283
350
|
return undefined;
|
|
284
351
|
}
|
|
285
352
|
|
|
286
353
|
/**
|
|
287
354
|
* What the studio shows for a thread. A turn starts with an editor's message, or Camox's own
|
|
288
355
|
* when it began the turn itself, and messages sent
|
|
289
|
-
* while it runs join it. It ends with the agent's final message
|
|
290
|
-
* is never shown. A stopped turn shows who stopped it instead, and whatever its run wrote after
|
|
356
|
+
* while it runs join it. It ends with the agent's final message, or on Live, the card of the
|
|
357
|
+
* draft it handed the request off to; text written beside tool calls is never shown. A stopped turn shows who stopped it instead, and whatever its run wrote after
|
|
291
358
|
* the stop is left out. A turn that ended without either shows a failed line in its place: its
|
|
292
359
|
* run is over, or a later message started another run after its last model call came back
|
|
293
360
|
* unfinished. While Pi retries a failed call, the run goes on, so the turn shows as working.
|
|
@@ -297,7 +364,7 @@ export function threadViewOf({
|
|
|
297
364
|
running,
|
|
298
365
|
toolCalls = [],
|
|
299
366
|
callingTools = false,
|
|
300
|
-
|
|
367
|
+
drafts = {},
|
|
301
368
|
}: ThreadState): ThreadView {
|
|
302
369
|
const messages: ThreadMessage[] = [];
|
|
303
370
|
// The latest editor's message of a turn still waiting for the agent's message.
|
|
@@ -322,7 +389,24 @@ export function threadViewOf({
|
|
|
322
389
|
|
|
323
390
|
for (const step of steps) {
|
|
324
391
|
switch (step.type) {
|
|
325
|
-
case "editor":
|
|
392
|
+
case "editor": {
|
|
393
|
+
// A message sent as the agent on Live handed the request off went to the draft with it:
|
|
394
|
+
// it belongs to the turn ending on that draft's card, which it shows just before. Pi
|
|
395
|
+
// placed it after the card, which leaves the transcript the model read as it was.
|
|
396
|
+
const card = step.handedOffTo
|
|
397
|
+
? messages.findLastIndex(
|
|
398
|
+
(message) => message.from === "draft" && message.draftId === step.handedOffTo,
|
|
399
|
+
)
|
|
400
|
+
: -1;
|
|
401
|
+
if (card !== -1) {
|
|
402
|
+
messages.splice(card, 0, {
|
|
403
|
+
id: step.id,
|
|
404
|
+
from: "editor",
|
|
405
|
+
sender: step.sender,
|
|
406
|
+
text: step.text,
|
|
407
|
+
});
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
326
410
|
// Pi places a steering message only once a model call is settled: after a round of tool
|
|
327
411
|
// calls, or with the answer ending the run. Never between a failed call and its retry, so
|
|
328
412
|
// an editor's message after an unfinished call means that call's turn is over.
|
|
@@ -330,6 +414,7 @@ export function threadViewOf({
|
|
|
330
414
|
messages.push({ id: step.id, from: "editor", sender: step.sender, text: step.text });
|
|
331
415
|
waiting = step.id;
|
|
332
416
|
break;
|
|
417
|
+
}
|
|
333
418
|
case "camox":
|
|
334
419
|
if (unfinished) fail();
|
|
335
420
|
messages.push({ id: step.id, from: "camox", text: step.text });
|
|
@@ -350,6 +435,21 @@ export function threadViewOf({
|
|
|
350
435
|
answered = before;
|
|
351
436
|
}
|
|
352
437
|
break;
|
|
438
|
+
case "draft":
|
|
439
|
+
// The handoff answers the turn: the agent says nothing more.
|
|
440
|
+
if (waiting === undefined && messages.at(-1)?.from === "stopped") break;
|
|
441
|
+
{
|
|
442
|
+
const draft = drafts[step.draftId];
|
|
443
|
+
messages.push({
|
|
444
|
+
id: step.id,
|
|
445
|
+
from: "draft",
|
|
446
|
+
draftId: step.draftId,
|
|
447
|
+
name: draft?.name ?? step.name,
|
|
448
|
+
...(draft?.ended && { ended: draft.ended }),
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
endTurn();
|
|
452
|
+
break;
|
|
353
453
|
case "continued":
|
|
354
454
|
// The answer before wasn't the turn's last. Once the end of the turn gives up fixing,
|
|
355
455
|
// the agent explains without a status.
|
|
@@ -373,7 +473,6 @@ export function threadViewOf({
|
|
|
373
473
|
if (!running) fail();
|
|
374
474
|
const usingTools = running && (callingTools || toolCalls.length > 0);
|
|
375
475
|
return {
|
|
376
|
-
...drafts,
|
|
377
476
|
messages,
|
|
378
477
|
running,
|
|
379
478
|
...(running && status && { status }),
|
|
@@ -439,10 +538,8 @@ export function reduceThreadFrame(state: ThreadState, frame: ThreadServerMessage
|
|
|
439
538
|
switch (frame.type) {
|
|
440
539
|
case "events":
|
|
441
540
|
return reduceThreadEvents(state, frame.events);
|
|
442
|
-
case "
|
|
443
|
-
return { ...state,
|
|
444
|
-
case "continued-in":
|
|
445
|
-
return { ...state, continuedIn: frame.continuedIn };
|
|
541
|
+
case "drafts":
|
|
542
|
+
return { ...state, drafts: frame.drafts };
|
|
446
543
|
default:
|
|
447
544
|
return state;
|
|
448
545
|
}
|