@camox/api-contract 0.62.0 → 0.65.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/src/patrol.ts CHANGED
@@ -4,8 +4,11 @@
4
4
  * Live, and names each report's subject, so the studio needs no entity loaded to show it.
5
5
  */
6
6
 
7
- /** A patroller's stable id. The registry in the API lists them all. */
8
- export type PatrollerId = "translator" | "seoManager";
7
+ /** Every patroller's stable id. The registry in the API lists them all. */
8
+ export const PATROLLER_IDS = ["translator", "seoManager", "linkChecker", "proofreader"] as const;
9
+
10
+ /** A patroller's stable id. */
11
+ export type PatrollerId = (typeof PATROLLER_IDS)[number];
9
12
 
10
13
  /**
11
14
  * What a structured report says was done. Each has a failed form, by the report's outcome. A
@@ -16,7 +19,10 @@ export type ReportVerb =
16
19
  | "mergedWithLive"
17
20
  | "wroteSeo"
18
21
  | "describedFile"
19
- | "publishedCatchUp";
22
+ | "publishedCatchUp"
23
+ | "removedLink"
24
+ | "fixedLink"
25
+ | "fixedMistake";
20
26
 
21
27
  /** What a report can be about. */
22
28
  export type ReportEntity =
@@ -51,8 +57,11 @@ export const reportLocale = (
51
57
  /** Whether the work was done, or failed. A failure shows in its report only. */
52
58
  export type ReportOutcome = "done" | "failed";
53
59
 
54
- /** Whether a report needs a human. Nothing raises one yet. */
55
- export type AttentionLevel = "none" | "needed";
60
+ /**
61
+ * Where a patroller is on duty, in drafts or on Live: it works as you go, checks when you
62
+ * publish, or is off there (ADR 0034).
63
+ */
64
+ export type PatrollerDuty = "asYouGo" | "atPublish" | "off";
56
65
 
57
66
  /**
58
67
  * Where the studio shows a report's subject: a page on the Canvas, with the block and repeatable
@@ -68,7 +77,6 @@ export type ReportView = {
68
77
  id: string;
69
78
  summary: ReportSummary;
70
79
  outcome: ReportOutcome;
71
- attention: AttentionLevel;
72
80
  /** When the work last settled, in milliseconds since the epoch. */
73
81
  at: number;
74
82
  /** Whether the report is Live's, seen from a draft or from Live. */
@@ -80,6 +88,98 @@ export type ReportView = {
80
88
  place: ReportPlace | null;
81
89
  };
82
90
 
91
+ /**
92
+ * What a finding is about (ADR 0034), by its kind:
93
+ * - `missingPage`: a link to a page that doesn't exist, the page's name as it was last known.
94
+ * - `brokenLink`: an external link that failed, and how.
95
+ * - `misspelling`, `grammar`: the flagged text, in the sentence it's in.
96
+ * A link's `url` is as content stores it: a page link's is `camox:page:<id>`, and `text` is what
97
+ * the link reads.
98
+ */
99
+ export type FindingAbout =
100
+ | { kind: "missingPage"; url: string; text: string; page: string | null }
101
+ | { kind: "brokenLink"; url: string; text: string; failure: LinkFailure }
102
+ | { kind: "misspelling" | "grammar"; text: string; sentence: string };
103
+
104
+ /** How a link failed: an HTTP error status, no connection (DNS included), or no answer in time. */
105
+ export type LinkFailure =
106
+ | { kind: "status"; status: number }
107
+ | { kind: "unreachable" }
108
+ | { kind: "timeout" };
109
+
110
+ export type FindingKind = FindingAbout["kind"];
111
+
112
+ /**
113
+ * The fix a finding suggests, which applying makes: removing the link, keeping its text, or
114
+ * replacing the flagged text with `replacement` in its sentence, keeping its formatting.
115
+ */
116
+ export type FindingFix = { kind: "removeLink" } | { kind: "replaceText"; replacement: string };
117
+
118
+ /** A finding, open, in the scope viewed. */
119
+ export type FindingView = {
120
+ /** What applying or dismissing it names. */
121
+ id: number;
122
+ about: FindingAbout;
123
+ /** The fix it suggests, if any. */
124
+ fix: FindingFix | null;
125
+ /** The content it's in, named in its locale. */
126
+ subject: ReportSubject;
127
+ /** The locale of the content it's in, by its tag. */
128
+ locale: string;
129
+ /** The content field it's in, by name. */
130
+ field: string;
131
+ /** When it was first raised, in milliseconds since the epoch. */
132
+ raisedAt: number;
133
+ /** Whether it's Live's. Live's are fixed in the Patrol Draft. */
134
+ live: boolean;
135
+ /** Where its content shows, as for reports. */
136
+ place: ReportPlace | null;
137
+ };
138
+
139
+ /** What a finding is about, after where it is: "links to About, a page that no longer exists". */
140
+ function aboutSentence(about: FindingAbout): string {
141
+ switch (about.kind) {
142
+ case "missingPage":
143
+ return about.page === null
144
+ ? "links to a page that no longer exists"
145
+ : `links to ${about.page}, a page that no longer exists`;
146
+ case "brokenLink": {
147
+ const { failure } = about;
148
+ const how =
149
+ failure.kind === "status"
150
+ ? `answers with an error (${failure.status})`
151
+ : failure.kind === "timeout"
152
+ ? "doesn't answer in time"
153
+ : "can't be reached";
154
+ return `links to ${about.url}, which ${how}`;
155
+ }
156
+ case "misspelling":
157
+ return "may be misspelled";
158
+ case "grammar":
159
+ return "may be a grammar mistake";
160
+ }
161
+ }
162
+
163
+ /**
164
+ * A finding as one sentence, as the studio, emails, Briefs and refusals word it: "“About us” in
165
+ * Footer links to About, a page that no longer exists".
166
+ */
167
+ export function findingSentence(finding: Pick<FindingView, "about" | "subject">): string {
168
+ return `“${finding.about.text}” in ${finding.subject.name} ${aboutSentence(finding.about)}`;
169
+ }
170
+
171
+ /** A member's dismissal of what a finding was about: no finding like it is raised again. */
172
+ export type DismissalView = {
173
+ id: number;
174
+ about: FindingAbout;
175
+ by: { id: string; name: string };
176
+ /** When it was dismissed, in milliseconds since the epoch. */
177
+ at: number;
178
+ };
179
+
180
+ /** What applying a finding's fix did: applied it, or found its content changed and rechecked. */
181
+ export type ApplyResult = { applied: boolean };
182
+
83
183
  /** How many subjects a patroller is on in one locale, a locale's tag. */
84
184
  export type LocaleCount = { locale: string; count: number };
85
185
 
@@ -96,18 +196,38 @@ export type PatrollerView = {
96
196
  id: PatrollerId;
97
197
  /** Its role, as editors know it: "Translator", "SEO manager". */
98
198
  role: string;
199
+ /** Its duty in the scope viewed. Off there, it still has its reports. */
200
+ duty: PatrollerDuty;
99
201
  /** Null when it isn't working in the scope. */
100
202
  working: PatrollerWorking | null;
101
- /** Its latest activity in the scope, or null when it never worked there. */
203
+ /**
204
+ * When it last finished a check in the scope, whether it found anything or not, or null when it
205
+ * never checked there.
206
+ */
207
+ lastCheckedAt: number | null;
208
+ /**
209
+ * Its latest activity in the scope: its last check, its newest report or when its pending work
210
+ * started, whichever is later. Null when it never worked there.
211
+ */
102
212
  lastActivityAt: number | null;
103
213
  /** Its newest reports, newest first. */
104
214
  reports: ReportView[];
105
215
  /** How many more reports it has than `reports` shows, per locale, the most first. */
106
216
  more: { locale: string | null; count: number }[];
217
+ /** Its open findings in the scope viewed, the oldest first. Their count is the row's badge. */
218
+ findings: FindingView[];
219
+ /** The project's dismissals of its findings, the newest first. */
220
+ dismissals: DismissalView[];
107
221
  };
108
222
 
109
- /** Every patroller of the registry, the latest activity first; never-worked ones last. */
110
- export type PatrolView = { patrollers: PatrollerView[] };
223
+ /**
224
+ * Every patroller of the registry, the latest activity first; never-worked ones last. With the
225
+ * project's open Patrol Draft, where Live's findings are fixed, if any.
226
+ */
227
+ export type PatrolView = {
228
+ patrollers: PatrollerView[];
229
+ patrolDraft: { id: string; name: string | null } | null;
230
+ };
111
231
 
112
232
  /** How many reports the Patrol shows of each patroller; the rest are counted. */
113
233
  export const REPORTS_SHOWN = 5;
package/src/threads.ts CHANGED
@@ -16,6 +16,15 @@ import type { DraftOutcome } from "./drafts.ts";
16
16
  */
17
17
  export const EDITOR_MESSAGE_ENTRY = "camox.editor-message";
18
18
 
19
+ /**
20
+ * The kind of the transcript entry recording a Connected Agent's message (ADR 0030), with its
21
+ * member and client as data (`connectedAgentMessageDataSchema`). The model reads it as a user
22
+ * message, as it does an editor's: its first text part is what the Connected Agent wrote, and a
23
+ * second one, which only the model reads, says who sent it. Studios show it as the member's AI,
24
+ * never as the member.
25
+ */
26
+ export const CONNECTED_AGENT_MESSAGE_ENTRY = "camox.connected-agent-message";
27
+
19
28
  /** The kind of the transcript entry recording that an editor stopped a run, with them as data. */
20
29
  export const RUN_STOPPED_ENTRY = "camox.run-stopped";
21
30
 
@@ -36,7 +45,8 @@ export const RECONCILE_ENTRY = "camox.reconcile";
36
45
  /**
37
46
  * The kind of the transcript entry opening a draft's thread a Live thread handed a request off
38
47
  * to: the Brief, which the model reads and editors see as from Camox, ahead of the editor's
39
- * 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.
40
50
  */
41
51
  export const BRIEF_ENTRY = "camox.brief";
42
52
 
@@ -67,6 +77,9 @@ export const START_DRAFT_TOOL = "startDraft";
67
77
  /** The agent's tool building its code changes mid-turn, which the end of a turn also does. */
68
78
  export const BUILD_TOOL = "build";
69
79
 
80
+ /** The agent's tool rendering the draft in a browser and returning screenshots to the model. */
81
+ export const LOOK_TOOL = "look";
82
+
70
83
  /**
71
84
  * The tools whose calls change a draft: the content tools that write, and the code tools that
72
85
  * write files. The model calls the code tools, and code mode scripts call both. A test in the API
@@ -76,6 +89,7 @@ export const CHANGING_TOOLS: ReadonlySet<string> = new Set([
76
89
  // Content
77
90
  "addBackToLocale",
78
91
  "addLocale",
92
+ "applyFindingFix",
79
93
  "createBlock",
80
94
  "createCollectionRecord",
81
95
  "createPage",
@@ -132,6 +146,24 @@ export type ThreadSender = z.infer<typeof threadSenderSchema>;
132
146
  export const isSender = (data: unknown): data is ThreadSender =>
133
147
  threadSenderSchema.safeParse(data).success;
134
148
 
149
+ /**
150
+ * The app a Connected Agent runs in, by the name it gave: verified when it comes from a Client ID
151
+ * Metadata Document, self-asserted otherwise.
152
+ */
153
+ export const connectedAgentClientSchema = z.object({ name: z.string(), verified: z.boolean() });
154
+ export type ConnectedAgentClient = z.infer<typeof connectedAgentClientSchema>;
155
+
156
+ /**
157
+ * What a Connected Agent's message entry records: the member it acts as, its client, and the
158
+ * operation id it was sent with.
159
+ */
160
+ export const connectedAgentMessageDataSchema = z.object({
161
+ member: threadSenderSchema,
162
+ client: connectedAgentClientSchema,
163
+ operationId: z.string().optional(),
164
+ });
165
+ export type ConnectedAgentMessageData = z.infer<typeof connectedAgentMessageDataSchema>;
166
+
135
167
  /** What a push's entry records: who pushed, their commits' subjects, oldest first, and the build. */
136
168
  export const pushDataSchema = z.object({
137
169
  pusher: threadSenderSchema,
@@ -205,6 +237,17 @@ export type TouchedBatch = z.infer<typeof touchedBatchSchema>;
205
237
  /** A message the studio shows in a thread. */
206
238
  export type ThreadMessage =
207
239
  | { id: string; from: "editor"; sender: ThreadSender; text: string }
240
+ /**
241
+ * A member's Connected Agent's message: the member's own AI, in its client, writing as itself.
242
+ * Never shown as the member's own words.
243
+ */
244
+ | {
245
+ id: string;
246
+ from: "connectedAgent";
247
+ member: ThreadSender;
248
+ client: ConnectedAgentClient;
249
+ text: string;
250
+ }
208
251
  /** Camox's own message, starting a turn it began itself: why it did. */
209
252
  | { id: string; from: "camox"; text: string }
210
253
  /** The agent's final message of a turn, in Markdown. */
@@ -236,6 +279,8 @@ export type ThreadMessage =
236
279
  * - `changing`: a call of that round changes the draft (`CHANGING_TOOLS`), as soon as it's written
237
280
  * or, in a code mode script, as soon as the script calls one.
238
281
  * - `building`: its `build` call runs, or the end of the turn builds its code changes.
282
+ * - `checking`: it looks at what it designed, from writing a `look` call until the model is
283
+ * called again.
239
284
  * - `fixing`: the end of the turn's build failed, and the agent fixes it.
240
285
  * - `startingDraft`: on Live, its `startDraft` call runs.
241
286
  */
@@ -244,6 +289,7 @@ export type AgentActivity =
244
289
  | "exploring"
245
290
  | "changing"
246
291
  | "building"
292
+ | "checking"
247
293
  | "fixing"
248
294
  | "startingDraft";
249
295
 
@@ -275,6 +321,8 @@ export type ThreadState = {
275
321
  * or making it until the model is called again.
276
322
  */
277
323
  changing?: boolean;
324
+ /** Whether the model's latest round calls `look`: from writing the call until it's called again. */
325
+ checking?: boolean;
278
326
  /** The drafts the thread's cards name, as the thread last looked them up. */
279
327
  drafts?: DraftCardStates;
280
328
  };
@@ -290,6 +338,7 @@ type ThreadStep = { id: string } & (
290
338
  /** The draft the message went to, as the agent on Live handed the request off. */
291
339
  handedOffTo?: string;
292
340
  }
341
+ | { type: "connectedAgent"; member: ThreadSender; client: ConnectedAgentClient; text: string }
293
342
  /** Camox starting a turn of its own, saying why. */
294
343
  | { type: "camox"; text: string }
295
344
  | { type: "answer"; text: string }
@@ -461,6 +510,18 @@ function threadStepOf(entry: EntryRecord): ThreadStep | undefined {
461
510
  }
462
511
  );
463
512
  }
513
+ if (entry.kind === CONNECTED_AGENT_MESSAGE_ENTRY) {
514
+ const data = connectedAgentMessageDataSchema.safeParse(entry.data).data;
515
+ return (
516
+ data && {
517
+ id,
518
+ type: "connectedAgent",
519
+ member: senderOf(data.member)!,
520
+ client: { name: data.client.name, verified: data.client.verified },
521
+ text: editorTextOf(message.content),
522
+ }
523
+ );
524
+ }
464
525
  if (entry.kind === BRIEF_ENTRY) return { id, type: "camox", text: textOf(message.content) };
465
526
  if (entry.kind === RECONCILE_ENTRY) {
466
527
  const reconcile = reconcileDataSchema.safeParse(entry.data).data;
@@ -496,6 +557,7 @@ export function threadViewOf({
496
557
  toolCalls = [],
497
558
  callingTools = false,
498
559
  changing = false,
560
+ checking = false,
499
561
  drafts = {},
500
562
  }: ThreadState): ThreadView {
501
563
  const messages: ThreadMessage[] = [];
@@ -547,6 +609,18 @@ export function threadViewOf({
547
609
  waiting = step.id;
548
610
  break;
549
611
  }
612
+ case "connectedAgent":
613
+ // Like an editor's message: it starts a turn, or steers the one going.
614
+ if (unfinished) fail();
615
+ messages.push({
616
+ id: step.id,
617
+ from: "connectedAgent",
618
+ member: step.member,
619
+ client: step.client,
620
+ text: step.text,
621
+ });
622
+ waiting = step.id;
623
+ break;
550
624
  case "camox":
551
625
  if (unfinished) fail();
552
626
  messages.push({ id: step.id, from: "camox", text: step.text });
@@ -617,7 +691,7 @@ export function threadViewOf({
617
691
  return {
618
692
  messages,
619
693
  running,
620
- activity: activityOf({ status, toolCalls, callingTools, changing }),
694
+ activity: activityOf({ status, toolCalls, callingTools, changing, checking }),
621
695
  };
622
696
  }
623
697
 
@@ -627,16 +701,19 @@ function activityOf({
627
701
  toolCalls,
628
702
  callingTools,
629
703
  changing,
704
+ checking,
630
705
  }: {
631
706
  status: BuildStatus | undefined;
632
707
  toolCalls: NonNullable<ThreadState["toolCalls"]>;
633
708
  callingTools: boolean;
634
709
  changing: boolean;
710
+ checking: boolean;
635
711
  }): AgentActivity {
636
712
  const calling = (name: string) => toolCalls.some((call) => call.name === name);
637
713
  // Fixing a build error takes thinking, reading and writing, which all read as fixing, but its
638
714
  // builds show as building.
639
715
  if (status === "building" || calling(BUILD_TOOL)) return "building";
716
+ if (checking) return "checking";
640
717
  if (status === "fixing") return "fixing";
641
718
  if (calling(START_DRAFT_TOOL)) return "startingDraft";
642
719
  if (changing) return "changing";
@@ -651,6 +728,10 @@ const changes = (name: string) => CHANGING_TOOLS.has(name);
651
728
  const callsChangingTool = (block: AssistantMessage["content"][number]) =>
652
729
  block.type === "toolCall" && changes(block.name);
653
730
 
731
+ /** Whether a block of the model's message is a call of `look`. */
732
+ const callsLook = (block: AssistantMessage["content"][number]) =>
733
+ block.type === "toolCall" && block.name === LOOK_TOOL;
734
+
654
735
  const append = (state: ThreadState, entry: EntryRecord | undefined): ThreadState => {
655
736
  const step = entry && threadStepOf(entry);
656
737
  if (!step || state.steps.some((known) => known.id === step.id)) return state;
@@ -671,6 +752,7 @@ export function reduceThreadState(state: ThreadState, event: AgentEvent): Thread
671
752
  running: event.run !== undefined,
672
753
  toolCalls,
673
754
  callingTools: generated.some((part) => part.type === "toolCall"),
755
+ checking: generated.some(callsLook) || toolCalls.some((call) => call.name === LOOK_TOOL),
674
756
  // A script's calls count while the script runs, finished or not.
675
757
  changing:
676
758
  generated.some(callsChangingTool) ||
@@ -689,11 +771,18 @@ export function reduceThreadState(state: ThreadState, event: AgentEvent): Thread
689
771
  case "run_start":
690
772
  return { ...state, running: true };
691
773
  case "run_end":
692
- return { ...state, running: false, toolCalls: [], callingTools: false, changing: false };
774
+ return {
775
+ ...state,
776
+ running: false,
777
+ toolCalls: [],
778
+ callingTools: false,
779
+ changing: false,
780
+ checking: false,
781
+ };
693
782
  case "message_start":
694
783
  // The model is called again: it thinks until it writes a tool call.
695
784
  return event.message.role === "assistant"
696
- ? { ...state, callingTools: false, changing: false }
785
+ ? { ...state, callingTools: false, changing: false, checking: false }
697
786
  : state;
698
787
  case "message_update": {
699
788
  // A call's name may come with its start, a later change to its block, or the whole message.
@@ -709,6 +798,7 @@ export function reduceThreadState(state: ThreadState, event: AgentEvent): Thread
709
798
  ...state,
710
799
  callingTools: state.callingTools || calls.some((block) => block.type === "toolCall"),
711
800
  changing: state.changing || calls.some(callsChangingTool),
801
+ checking: state.checking || calls.some(callsLook),
712
802
  };
713
803
  }
714
804
  case "tool_execution_start": {
@@ -717,6 +807,7 @@ export function reduceThreadState(state: ThreadState, event: AgentEvent): Thread
717
807
  return {
718
808
  ...state,
719
809
  changing,
810
+ checking: state.checking || event.toolName === LOOK_TOOL,
720
811
  toolCalls: [...(state.toolCalls ?? []), { callId: event.toolCallId, name: event.toolName }],
721
812
  };
722
813
  }