@camox/api-contract 0.63.0 → 0.66.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/src/threads.ts CHANGED
@@ -45,7 +45,8 @@ export const RECONCILE_ENTRY = "camox.reconcile";
45
45
  /**
46
46
  * The kind of the transcript entry opening a draft's thread a Live thread handed a request off
47
47
  * to: the Brief, which the model reads and editors see as from Camox, ahead of the editor's
48
- * messages.
48
+ * messages. In a draft Camox opened, such as its Patrol Draft, Camox's own messages are Briefs
49
+ * too, with their `operationId` as data, and start the agent's run, which acts for Camox.
49
50
  */
50
51
  export const BRIEF_ENTRY = "camox.brief";
51
52
 
@@ -56,6 +57,14 @@ export const BRIEF_ENTRY = "camox.brief";
56
57
  */
57
58
  export const DRAFT_ENDED_ENTRY = "camox.draft-ended";
58
59
 
60
+ /**
61
+ * The kind of the transcript entry a Live thread starts afresh from once its agent handed a
62
+ * request off to a draft (ADR 0036): a reset of Pi's context, with a `startedDraftCardSchema` as
63
+ * data. Editors see it as the draft's card, alone, and the model reads a note naming the draft.
64
+ * Everything before it is left out of both.
65
+ */
66
+ export const DRAFT_CARD_ENTRY = "camox.draft-card";
67
+
59
68
  /**
60
69
  * The kind of the transcript entry recording a member's push to the draft's code from their own
61
70
  * checkout (ADR 0026), with a `pushDataSchema` as data. The model reads it as context, never
@@ -76,6 +85,29 @@ export const START_DRAFT_TOOL = "startDraft";
76
85
  /** The agent's tool building its code changes mid-turn, which the end of a turn also does. */
77
86
  export const BUILD_TOOL = "build";
78
87
 
88
+ /** The agent's tool rendering the draft in a browser and returning screenshots to the model. */
89
+ export const LOOK_TOOL = "look";
90
+
91
+ /**
92
+ * The agent's tool handing an artifact's source to its Artifact Agent, which writes and changes
93
+ * it in a conversation of its own and answers the agent (ADR 0035).
94
+ */
95
+ export const ARTIFACT_TOOL = "artifact";
96
+
97
+ /**
98
+ * The kind of the entry recording a request the agent sent an Artifact Agent, in the Artifact
99
+ * Agent's own conversation, which editors never see: who the run acts for and the operation ids
100
+ * of its messages, as data (`artifactRequestDataSchema`).
101
+ */
102
+ export const ARTIFACT_REQUEST_ENTRY = "camox.artifact-request";
103
+
104
+ /** What an Artifact Agent's request records: the member the run acts for, and its messages. */
105
+ export const artifactRequestDataSchema = z.object({
106
+ member: z.string(),
107
+ operationIds: z.array(z.string()),
108
+ });
109
+ export type ArtifactRequestData = z.infer<typeof artifactRequestDataSchema>;
110
+
79
111
  /**
80
112
  * The tools whose calls change a draft: the content tools that write, and the code tools that
81
113
  * write files. The model calls the code tools, and code mode scripts call both. A test in the API
@@ -85,6 +117,7 @@ export const CHANGING_TOOLS: ReadonlySet<string> = new Set([
85
117
  // Content
86
118
  "addBackToLocale",
87
119
  "addLocale",
120
+ "applyFindingFix",
88
121
  "createBlock",
89
122
  "createCollectionRecord",
90
123
  "createPage",
@@ -167,13 +200,9 @@ export const pushDataSchema = z.object({
167
200
  });
168
201
  export type PushData = z.infer<typeof pushDataSchema>;
169
202
 
170
- /**
171
- * What an editor's message entry records: who sent it, the operation id it was sent with, and for
172
- * a message sent on Live as the agent handed the request off, the draft it went to with it.
173
- */
203
+ /** What an editor's message entry records: who sent it, and the operation id it was sent with. */
174
204
  export const editorMessageDataSchema = threadSenderSchema.extend({
175
205
  operationId: z.string().optional(),
176
- handedOffTo: z.string().optional(),
177
206
  });
178
207
 
179
208
  /**
@@ -274,16 +303,33 @@ export type ThreadMessage =
274
303
  * - `changing`: a call of that round changes the draft (`CHANGING_TOOLS`), as soon as it's written
275
304
  * or, in a code mode script, as soon as the script calls one.
276
305
  * - `building`: its `build` call runs, or the end of the turn builds its code changes.
306
+ * - `checking`: it looks at what it designed, from writing a `look` call until the model is
307
+ * called again.
277
308
  * - `fixing`: the end of the turn's build failed, and the agent fixes it.
278
309
  * - `startingDraft`: on Live, its `startDraft` call runs.
279
310
  */
280
- export type AgentActivity =
281
- | "thinking"
282
- | "exploring"
283
- | "changing"
284
- | "building"
285
- | "fixing"
286
- | "startingDraft";
311
+ export const agentActivitySchema = z.enum([
312
+ "thinking",
313
+ "exploring",
314
+ "changing",
315
+ "building",
316
+ "checking",
317
+ "fixing",
318
+ "startingDraft",
319
+ ]);
320
+ export type AgentActivity = z.infer<typeof agentActivitySchema>;
321
+
322
+ /**
323
+ * What an `artifact` call's result carries for the studio: the Artifact Agent's answer, whether
324
+ * it's the agent's answer to the turn too, which then ends on it, and while it works, what the
325
+ * Artifact Agent is doing.
326
+ */
327
+ export const artifactDetailsSchema = z.object({
328
+ answer: z.string().optional(),
329
+ final: z.boolean().optional(),
330
+ activity: agentActivitySchema.optional(),
331
+ });
332
+ export type ArtifactDetails = z.infer<typeof artifactDetailsSchema>;
287
333
 
288
334
  /** Everything the studio shows for a thread. */
289
335
  export type ThreadView = {
@@ -313,6 +359,10 @@ export type ThreadState = {
313
359
  * or making it until the model is called again.
314
360
  */
315
361
  changing?: boolean;
362
+ /** Whether the model's latest round calls `look`: from writing the call until it's called again. */
363
+ checking?: boolean;
364
+ /** What the Artifact Agent the running `artifact` call waits for is doing, as it last said. */
365
+ artifactActivity?: AgentActivity;
316
366
  /** The drafts the thread's cards name, as the thread last looked them up. */
317
367
  drafts?: DraftCardStates;
318
368
  };
@@ -321,13 +371,7 @@ export const EMPTY_THREAD_STATE: ThreadState = { steps: [], running: false };
321
371
 
322
372
  /** One transcript entry, as far as the studio's view is concerned. */
323
373
  type ThreadStep = { id: string } & (
324
- | {
325
- type: "editor";
326
- sender: ThreadSender;
327
- text: string;
328
- /** The draft the message went to, as the agent on Live handed the request off. */
329
- handedOffTo?: string;
330
- }
374
+ | { type: "editor"; sender: ThreadSender; text: string }
331
375
  | { type: "connectedAgent"; member: ThreadSender; client: ConnectedAgentClient; text: string }
332
376
  /** Camox starting a turn of its own, saying why. */
333
377
  | { type: "camox"; text: string }
@@ -485,20 +529,16 @@ function threadStepOf(entry: EntryRecord): ThreadStep | undefined {
485
529
  const push = pushDataSchema.safeParse(entry.data).data;
486
530
  return push && { id, type: "push", ...push };
487
531
  }
532
+ // The model reads a note: editors see the card it starts the Live thread afresh with.
533
+ if (entry.kind === DRAFT_CARD_ENTRY) {
534
+ const card = startedDraftCardSchema.safeParse(entry.data).data;
535
+ return card && { id, type: "draft", ...card };
536
+ }
488
537
  const message = entry.model?.find((candidate) => candidate.role !== "system");
489
538
  if (!message) return undefined;
490
539
  if (message.role === "user") {
491
540
  if (entry.kind === EDITOR_MESSAGE_ENTRY) {
492
- const handedOffTo = editorMessageDataSchema.safeParse(entry.data).data?.handedOffTo;
493
- return (
494
- sender && {
495
- id,
496
- type: "editor",
497
- sender,
498
- text: editorTextOf(message.content),
499
- ...(handedOffTo && { handedOffTo }),
500
- }
501
- );
541
+ return sender && { id, type: "editor", sender, text: editorTextOf(message.content) };
502
542
  }
503
543
  if (entry.kind === CONNECTED_AGENT_MESSAGE_ENTRY) {
504
544
  const data = connectedAgentMessageDataSchema.safeParse(entry.data).data;
@@ -523,6 +563,11 @@ function threadStepOf(entry: EntryRecord): ThreadStep | undefined {
523
563
  }
524
564
  if (message.role === "assistant") return assistantStep(id, message);
525
565
  if (message.role === "toolResult") {
566
+ // An Artifact Agent's answer that ends the turn is the agent's: editors never see it apart.
567
+ if (message.toolName === ARTIFACT_TOOL && !message.isError) {
568
+ const details = artifactDetailsSchema.safeParse(message.details).data;
569
+ if (details?.final && details.answer) return { id, type: "answer", text: details.answer };
570
+ }
526
571
  const card =
527
572
  message.toolName === START_DRAFT_TOOL && !message.isError
528
573
  ? startedDraftCardSchema.safeParse(message.details).data
@@ -547,6 +592,8 @@ export function threadViewOf({
547
592
  toolCalls = [],
548
593
  callingTools = false,
549
594
  changing = false,
595
+ checking = false,
596
+ artifactActivity,
550
597
  drafts = {},
551
598
  }: ThreadState): ThreadView {
552
599
  const messages: ThreadMessage[] = [];
@@ -572,24 +619,7 @@ export function threadViewOf({
572
619
 
573
620
  for (const step of steps) {
574
621
  switch (step.type) {
575
- case "editor": {
576
- // A message sent as the agent on Live handed the request off went to the draft with it:
577
- // it belongs to the turn ending on that draft's card, which it shows just before. Pi
578
- // placed it after the card, which leaves the transcript the model read as it was.
579
- const card = step.handedOffTo
580
- ? messages.findLastIndex(
581
- (message) => message.from === "draft" && message.draftId === step.handedOffTo,
582
- )
583
- : -1;
584
- if (card !== -1) {
585
- messages.splice(card, 0, {
586
- id: step.id,
587
- from: "editor",
588
- sender: step.sender,
589
- text: step.text,
590
- });
591
- break;
592
- }
622
+ case "editor":
593
623
  // Pi places a steering message only once a model call is settled: after a round of tool
594
624
  // calls, or with the answer ending the run. Never between a failed call and its retry, so
595
625
  // an editor's message after an unfinished call means that call's turn is over.
@@ -597,7 +627,6 @@ export function threadViewOf({
597
627
  messages.push({ id: step.id, from: "editor", sender: step.sender, text: step.text });
598
628
  waiting = step.id;
599
629
  break;
600
- }
601
630
  case "connectedAgent":
602
631
  // Like an editor's message: it starts a turn, or steers the one going.
603
632
  if (unfinished) fail();
@@ -680,7 +709,7 @@ export function threadViewOf({
680
709
  return {
681
710
  messages,
682
711
  running,
683
- activity: activityOf({ status, toolCalls, callingTools, changing }),
712
+ activity: activityOf({ status, toolCalls, callingTools, changing, checking, artifactActivity }),
684
713
  };
685
714
  }
686
715
 
@@ -690,16 +719,23 @@ function activityOf({
690
719
  toolCalls,
691
720
  callingTools,
692
721
  changing,
722
+ checking,
723
+ artifactActivity,
693
724
  }: {
694
725
  status: BuildStatus | undefined;
695
726
  toolCalls: NonNullable<ThreadState["toolCalls"]>;
696
727
  callingTools: boolean;
697
728
  changing: boolean;
729
+ checking: boolean;
730
+ artifactActivity: AgentActivity | undefined;
698
731
  }): AgentActivity {
699
732
  const calling = (name: string) => toolCalls.some((call) => call.name === name);
700
733
  // Fixing a build error takes thinking, reading and writing, which all read as fixing, but its
701
734
  // builds show as building.
702
735
  if (status === "building" || calling(BUILD_TOOL)) return "building";
736
+ // An Artifact Agent works on the agent's behalf: editors see what it does as the agent's.
737
+ if (calling(ARTIFACT_TOOL)) return artifactActivity ?? "changing";
738
+ if (checking) return "checking";
703
739
  if (status === "fixing") return "fixing";
704
740
  if (calling(START_DRAFT_TOOL)) return "startingDraft";
705
741
  if (changing) return "changing";
@@ -714,8 +750,25 @@ const changes = (name: string) => CHANGING_TOOLS.has(name);
714
750
  const callsChangingTool = (block: AssistantMessage["content"][number]) =>
715
751
  block.type === "toolCall" && changes(block.name);
716
752
 
753
+ /** Whether a block of the model's message is a call of `look`. */
754
+ const callsLook = (block: AssistantMessage["content"][number]) =>
755
+ block.type === "toolCall" && block.name === LOOK_TOOL;
756
+
757
+ /**
758
+ * Adds an entry to the thread's state as Pi places it. An entry heading a new context, such as
759
+ * the card a Live thread starts afresh from, leaves out every step before its head, as Pi's
760
+ * snapshots do.
761
+ */
717
762
  const append = (state: ThreadState, entry: EntryRecord | undefined): ThreadState => {
718
- const step = entry && threadStepOf(entry);
763
+ if (!entry) return state;
764
+ const step = threadStepOf(entry);
765
+ if (entry.head !== undefined) {
766
+ const head = Number(entry.head);
767
+ const kept = state.steps.filter(
768
+ (known) => Number(known.id) >= head && known.id !== String(entry.id),
769
+ );
770
+ return { ...state, steps: step ? [step, ...kept] : kept };
771
+ }
719
772
  if (!step || state.steps.some((known) => known.id === step.id)) return state;
720
773
  return { ...state, steps: [...state.steps, step] };
721
774
  };
@@ -728,12 +781,17 @@ export function reduceThreadState(state: ThreadState, event: AgentEvent): Thread
728
781
  tool.status === "done" ? [] : [{ callId: tool.callId, name: tool.name }],
729
782
  );
730
783
  const generated = event.generation?.message?.content ?? [];
784
+ const artifact = event.tools.find(
785
+ (tool) => tool.name === ARTIFACT_TOOL && tool.status !== "done",
786
+ );
731
787
  return {
732
788
  ...state,
733
789
  steps: event.entries.flatMap((entry) => threadStepOf(entry) ?? []),
790
+ artifactActivity: artifactDetailsSchema.safeParse(artifact?.details).data?.activity,
734
791
  running: event.run !== undefined,
735
792
  toolCalls,
736
793
  callingTools: generated.some((part) => part.type === "toolCall"),
794
+ checking: generated.some(callsLook) || toolCalls.some((call) => call.name === LOOK_TOOL),
737
795
  // A script's calls count while the script runs, finished or not.
738
796
  changing:
739
797
  generated.some(callsChangingTool) ||
@@ -752,11 +810,19 @@ export function reduceThreadState(state: ThreadState, event: AgentEvent): Thread
752
810
  case "run_start":
753
811
  return { ...state, running: true };
754
812
  case "run_end":
755
- return { ...state, running: false, toolCalls: [], callingTools: false, changing: false };
813
+ return {
814
+ ...state,
815
+ running: false,
816
+ toolCalls: [],
817
+ callingTools: false,
818
+ changing: false,
819
+ checking: false,
820
+ artifactActivity: undefined,
821
+ };
756
822
  case "message_start":
757
823
  // The model is called again: it thinks until it writes a tool call.
758
824
  return event.message.role === "assistant"
759
- ? { ...state, callingTools: false, changing: false }
825
+ ? { ...state, callingTools: false, changing: false, checking: false }
760
826
  : state;
761
827
  case "message_update": {
762
828
  // A call's name may come with its start, a later change to its block, or the whole message.
@@ -772,6 +838,7 @@ export function reduceThreadState(state: ThreadState, event: AgentEvent): Thread
772
838
  ...state,
773
839
  callingTools: state.callingTools || calls.some((block) => block.type === "toolCall"),
774
840
  changing: state.changing || calls.some(callsChangingTool),
841
+ checking: state.checking || calls.some(callsLook),
775
842
  };
776
843
  }
777
844
  case "tool_execution_start": {
@@ -780,14 +847,22 @@ export function reduceThreadState(state: ThreadState, event: AgentEvent): Thread
780
847
  return {
781
848
  ...state,
782
849
  changing,
850
+ checking: state.checking || event.toolName === LOOK_TOOL,
783
851
  toolCalls: [...(state.toolCalls ?? []), { callId: event.toolCallId, name: event.toolName }],
784
852
  };
785
853
  }
854
+ case "tool_execution_update": {
855
+ if (event.toolName !== ARTIFACT_TOOL || event.parentToolCallId !== undefined) return state;
856
+ const activity = artifactDetailsSchema.safeParse(event.details).data?.activity;
857
+ return activity ? { ...state, artifactActivity: activity } : state;
858
+ }
786
859
  case "tool_execution_end":
787
860
  return append(
788
861
  {
789
862
  ...state,
790
863
  toolCalls: state.toolCalls?.filter((call) => call.callId !== event.toolCallId),
864
+ ...(event.toolName === ARTIFACT_TOOL &&
865
+ event.parentToolCallId === undefined && { artifactActivity: undefined }),
791
866
  },
792
867
  event.entry,
793
868
  );