@camox/api-contract 0.71.0 → 0.72.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 CHANGED
@@ -42,17 +42,17 @@ export declare const LOOK_PATH_PARAM = "path";
42
42
  export declare const LOOK_LOCALE_PARAM = "locale";
43
43
  /**
44
44
  * The look document's Block Definition mode: instead of a page, the block of the Block Definition
45
- * `LOOK_BLOCK_DEFINITION_PARAM` names, alone in the site's shell, with its default content and
46
- * Block Settings and the overrides `LOOK_CONTENT_PARAM` and `LOOK_SETTINGS_PARAM` give as JSON, in
47
- * the shape the content tools take.
45
+ * `LOOK_BLOCK_DEFINITION_PARAM` names, alone in the site's shell, with empty fields and the
46
+ * content and Block Settings `LOOK_CONTENT_PARAM` and `LOOK_SETTINGS_PARAM` give as JSON merged
47
+ * on, in the shape the content tools take.
48
48
  */
49
49
  export declare const LOOK_BLOCK_DEFINITION_PARAM = "blockDefinition";
50
50
  export declare const LOOK_CONTENT_PARAM = "content";
51
51
  export declare const LOOK_SETTINGS_PARAM = "settings";
52
52
  /**
53
53
  * The look document's Layout Definition mode: instead of a page, the curated Layout
54
- * `LOOK_LAYOUT_DEFINITION_PARAM` names, alone in the site's shell, its blocks with their default
55
- * content and Block Settings and the overrides `LOOK_BLOCKS_PARAM` gives as JSON, by Block
54
+ * `LOOK_LAYOUT_DEFINITION_PARAM` names, alone in the site's shell, its blocks with empty fields
55
+ * and the content and Block Settings `LOOK_BLOCKS_PARAM` gives as JSON merged on, by Block
56
56
  * Definition name (`{ navbar: { content, settings } }`), around a labelled placeholder where a
57
57
  * page's content goes, as tall as `LOOK_PLACEHOLDER_PARAM` says: one of `LOOK_PLACEHOLDER_HEIGHTS`,
58
58
  * the first without.
package/dist/drafts.js CHANGED
@@ -42,17 +42,17 @@ const LOOK_PATH_PARAM = "path";
42
42
  const LOOK_LOCALE_PARAM = "locale";
43
43
  /**
44
44
  * The look document's Block Definition mode: instead of a page, the block of the Block Definition
45
- * `LOOK_BLOCK_DEFINITION_PARAM` names, alone in the site's shell, with its default content and
46
- * Block Settings and the overrides `LOOK_CONTENT_PARAM` and `LOOK_SETTINGS_PARAM` give as JSON, in
47
- * the shape the content tools take.
45
+ * `LOOK_BLOCK_DEFINITION_PARAM` names, alone in the site's shell, with empty fields and the
46
+ * content and Block Settings `LOOK_CONTENT_PARAM` and `LOOK_SETTINGS_PARAM` give as JSON merged
47
+ * on, in the shape the content tools take.
48
48
  */
49
49
  const LOOK_BLOCK_DEFINITION_PARAM = "blockDefinition";
50
50
  const LOOK_CONTENT_PARAM = "content";
51
51
  const LOOK_SETTINGS_PARAM = "settings";
52
52
  /**
53
53
  * The look document's Layout Definition mode: instead of a page, the curated Layout
54
- * `LOOK_LAYOUT_DEFINITION_PARAM` names, alone in the site's shell, its blocks with their default
55
- * content and Block Settings and the overrides `LOOK_BLOCKS_PARAM` gives as JSON, by Block
54
+ * `LOOK_LAYOUT_DEFINITION_PARAM` names, alone in the site's shell, its blocks with empty fields
55
+ * and the content and Block Settings `LOOK_BLOCKS_PARAM` gives as JSON merged on, by Block
56
56
  * Definition name (`{ navbar: { content, settings } }`), around a labelled placeholder where a
57
57
  * page's content goes, as tall as `LOOK_PLACEHOLDER_PARAM` says: one of `LOOK_PLACEHOLDER_HEIGHTS`,
58
58
  * the first without.
package/dist/index.d.ts CHANGED
@@ -17,6 +17,29 @@ export declare function artifactTitle(source: string): string;
17
17
  */
18
18
  export declare function artifactSourceHash(source: string): Promise<string>;
19
19
  //#endregion
20
+ //#region src/empty-values.d.ts
21
+ /**
22
+ * What a field holds before anyone writes it: the empty value of its kind. Fields declare no
23
+ * default, so content missing a field, or holding a value of the wrong kind, reads as this.
24
+ * Shared by the SDK (rendering, new repeatable items, authoring forms) and the API (creating
25
+ * content, conforming it on read), so both agree on what an unset field is.
26
+ */
27
+ /** A link that goes nowhere yet. */
28
+ export declare const EMPTY_LINK: {
29
+ readonly type: "external";
30
+ readonly text: "";
31
+ readonly href: "";
32
+ readonly newTab: false;
33
+ };
34
+ /**
35
+ * The empty value of a field, from its stored JSON Schema (`fieldType` and, for an enum, its
36
+ * options): `""` for text, a link to nowhere, `false`, an enum's first option, `null` for one
37
+ * asset or record, and `[]` for lists and repeaters. A fresh copy each call.
38
+ */
39
+ export declare function emptyFieldValue(fieldSchema: unknown): unknown;
40
+ /** The empty value of every field of a content or settings schema, by name. */
41
+ export declare function emptyFieldValues(schema: unknown): Record<string, unknown>;
42
+ //#endregion
20
43
  //#region src/record-templates.d.ts
21
44
  /**
22
45
  * A collection's `toTitle` and `toDescription` are record templates: text with `{{field}}` tokens
package/dist/index.js CHANGED
@@ -43,6 +43,51 @@ async function artifactSourceHash(source) {
43
43
  return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
44
44
  }
45
45
 
46
+ //#endregion
47
+ //#region src/empty-values.ts
48
+ /**
49
+ * What a field holds before anyone writes it: the empty value of its kind. Fields declare no
50
+ * default, so content missing a field, or holding a value of the wrong kind, reads as this.
51
+ * Shared by the SDK (rendering, new repeatable items, authoring forms) and the API (creating
52
+ * content, conforming it on read), so both agree on what an unset field is.
53
+ */
54
+ /** A link that goes nowhere yet. */
55
+ const EMPTY_LINK = {
56
+ type: "external",
57
+ text: "",
58
+ href: "",
59
+ newTab: false
60
+ };
61
+ function isObject(value) {
62
+ return typeof value === "object" && value !== null && !Array.isArray(value);
63
+ }
64
+ /**
65
+ * The empty value of a field, from its stored JSON Schema (`fieldType` and, for an enum, its
66
+ * options): `""` for text, a link to nowhere, `false`, an enum's first option, `null` for one
67
+ * asset or record, and `[]` for lists and repeaters. A fresh copy each call.
68
+ */
69
+ function emptyFieldValue(fieldSchema) {
70
+ const schema = isObject(fieldSchema) ? fieldSchema : {};
71
+ switch (schema.fieldType) {
72
+ case "Link": return { ...EMPTY_LINK };
73
+ case "Boolean": return false;
74
+ case "Enum": return (Array.isArray(schema.enum) ? schema.enum : []).find((option) => typeof option === "string") ?? "";
75
+ case "Image":
76
+ case "File":
77
+ case "Reference": return null;
78
+ case "ImageList":
79
+ case "FileList":
80
+ case "ReferenceList":
81
+ case "Repeater": return [];
82
+ default: return "";
83
+ }
84
+ }
85
+ /** The empty value of every field of a content or settings schema, by name. */
86
+ function emptyFieldValues(schema) {
87
+ const properties = isObject(schema) && isObject(schema.properties) ? schema.properties : {};
88
+ return Object.fromEntries(Object.entries(properties).map(([name, field]) => [name, emptyFieldValue(field)]));
89
+ }
90
+
46
91
  //#endregion
47
92
  //#region src/record-templates.ts
48
93
  /**
@@ -144,4 +189,4 @@ function isSettingCondition(value) {
144
189
  }
145
190
 
146
191
  //#endregion
147
- export { ARTICLE_TYPES, ICON_CATALOG_VERSION, PAGE_TEXT_LINK_PREFIX, QUERY_LIMIT_CAP, SYSTEM_ORDER_KEYS, UNTITLED_ARTIFACT, artifactSourceHash, artifactSourceTitle, artifactTitle, generateUuidv7, isPageUrlMarker, isQueryBacked, isSettingCondition, isSystemOrderKey, linkedRecordId, nonStringTemplateFields, projectIdSchema, recordLink, recordText, referenceQueryProblem, scopeIconSvgIds, templateFields, uuidv7JsonSchema, uuidv7Schema, validateIconValue };
192
+ export { ARTICLE_TYPES, EMPTY_LINK, ICON_CATALOG_VERSION, PAGE_TEXT_LINK_PREFIX, QUERY_LIMIT_CAP, SYSTEM_ORDER_KEYS, UNTITLED_ARTIFACT, artifactSourceHash, artifactSourceTitle, artifactTitle, emptyFieldValue, emptyFieldValues, generateUuidv7, isPageUrlMarker, isQueryBacked, isSettingCondition, isSystemOrderKey, linkedRecordId, nonStringTemplateFields, projectIdSchema, recordLink, recordText, referenceQueryProblem, scopeIconSvgIds, templateFields, uuidv7JsonSchema, uuidv7Schema, validateIconValue };
package/dist/router.d.ts CHANGED
@@ -4728,40 +4728,6 @@ declare const schema: {
4728
4728
  identity: undefined;
4729
4729
  generated: undefined;
4730
4730
  }, {}, {}>;
4731
- defaultContent: import("drizzle-orm/sqlite-core").SQLiteColumn<{
4732
- name: "default_content";
4733
- tableName: "block_definitions";
4734
- dataType: "json";
4735
- columnType: "SQLiteTextJson";
4736
- data: unknown;
4737
- driverParam: string;
4738
- notNull: false;
4739
- hasDefault: false;
4740
- isPrimaryKey: false;
4741
- isAutoincrement: false;
4742
- hasRuntimeDefault: false;
4743
- enumValues: undefined;
4744
- baseColumn: never;
4745
- identity: undefined;
4746
- generated: undefined;
4747
- }, {}, {}>;
4748
- defaultSettings: import("drizzle-orm/sqlite-core").SQLiteColumn<{
4749
- name: "default_settings";
4750
- tableName: "block_definitions";
4751
- dataType: "json";
4752
- columnType: "SQLiteTextJson";
4753
- data: unknown;
4754
- driverParam: string;
4755
- notNull: false;
4756
- hasDefault: false;
4757
- isPrimaryKey: false;
4758
- isAutoincrement: false;
4759
- hasRuntimeDefault: false;
4760
- enumValues: undefined;
4761
- baseColumn: never;
4762
- identity: undefined;
4763
- generated: undefined;
4764
- }, {}, {}>;
4765
4731
  layoutOnly: import("drizzle-orm/sqlite-core").SQLiteColumn<{
4766
4732
  name: "layout_only";
4767
4733
  tableName: "block_definitions";
@@ -9934,11 +9900,9 @@ declare const TRANSLATED_ENTITIES: readonly ["page", "block", "sharedContent", "
9934
9900
  type TranslatedEntity = (typeof TRANSLATED_ENTITIES)[number];
9935
9901
  /**
9936
9902
  * A job translating a default-locale entity into one locale of a draft (ADR 0015). Jobs are keyed
9937
- * by entity, locale, draft and source, and debounced like generation jobs. A job translating
9938
- * `defaults` translates the code-owned defaults of a block placed in that locale instead, or of
9939
- * the shared content a synced block created there, which have no default-locale source. A job
9940
- * merging with `live` merges the draft's localization with Live's, which Live changed since the
9941
- * draft copied it, so it's no longer a conflict (ADR 0016).
9903
+ * by entity, locale, draft and source, and debounced like generation jobs. A job merging with
9904
+ * `live` merges the draft's localization with Live's, which Live changed since the draft copied
9905
+ * it, so it's no longer a conflict (ADR 0016).
9942
9906
  */
9943
9907
  type TranslationJob = {
9944
9908
  kind: "translation";
@@ -9946,7 +9910,7 @@ type TranslationJob = {
9946
9910
  entityId: string;
9947
9911
  locale: string;
9948
9912
  draftId: string;
9949
- source?: "defaults" | "live";
9913
+ source?: "live";
9950
9914
  };
9951
9915
  /**
9952
9916
  * A job writing a draft's Draft Overview from its changes, keyed by draft: a newer request
@@ -10243,8 +10207,6 @@ type Field = FieldBase & ({
10243
10207
  itemContentSchema: unknown;
10244
10208
  /** The settings schema of the repeater's items, if they have settings. */
10245
10209
  itemSettingsSchema: unknown;
10246
- /** How many items a new block starts with. */
10247
- defaultItems: number;
10248
10210
  } | {
10249
10211
  kind: "Reference";
10250
10212
  collectionId: string;
@@ -17027,8 +16989,6 @@ declare const router: {
17027
16989
  description: string;
17028
16990
  contentSchema: unknown;
17029
16991
  settingsSchema: unknown;
17030
- defaultContent: unknown;
17031
- defaultSettings: unknown;
17032
16992
  layoutOnly: boolean | null;
17033
16993
  synced: boolean;
17034
16994
  schemaHash: string;
@@ -17043,8 +17003,6 @@ declare const router: {
17043
17003
  description: string;
17044
17004
  contentSchema: unknown;
17045
17005
  settingsSchema: unknown;
17046
- defaultContent: unknown;
17047
- defaultSettings: unknown;
17048
17006
  layoutOnly: boolean | null;
17049
17007
  synced: boolean;
17050
17008
  schemaHash: string;
@@ -17772,8 +17730,6 @@ declare const router: {
17772
17730
  description: import("zod").ZodString;
17773
17731
  contentSchema: import("zod").ZodUnknown;
17774
17732
  settingsSchema: import("zod").ZodOptional<import("zod").ZodUnknown>;
17775
- defaultContent: import("zod").ZodOptional<import("zod").ZodUnknown>;
17776
- defaultSettings: import("zod").ZodOptional<import("zod").ZodUnknown>;
17777
17733
  layoutOnly: import("zod").ZodOptional<import("zod").ZodBoolean>;
17778
17734
  synced: import("zod").ZodOptional<import("zod").ZodBoolean>;
17779
17735
  }, import("zod/v4/core").$strip>>;
@@ -17792,20 +17748,10 @@ declare const router: {
17792
17748
  }, import("zod/v4/core").$strip>>;
17793
17749
  blocks: import("zod").ZodArray<import("zod").ZodPreprocess<import("zod").ZodObject<{
17794
17750
  type: import("zod").ZodString;
17795
- content: import("zod").ZodUnknown;
17796
- settings: import("zod").ZodOptional<import("zod").ZodUnknown>;
17797
17751
  slot: import("zod").ZodOptional<import("zod").ZodEnum<{
17798
17752
  after: "after";
17799
17753
  before: "before";
17800
17754
  }>>;
17801
- repeatableItems: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodObject<{
17802
- tempId: import("zod").ZodString;
17803
- parentTempId: import("zod").ZodNullable<import("zod").ZodString>;
17804
- fieldName: import("zod").ZodString;
17805
- content: import("zod").ZodUnknown;
17806
- settings: import("zod").ZodOptional<import("zod").ZodUnknown>;
17807
- position: import("zod").ZodString;
17808
- }, import("zod/v4/core").$strip>>>;
17809
17755
  }, import("zod/v4/core").$strip>, unknown>>;
17810
17756
  }, import("zod/v4/core").$strip>>;
17811
17757
  collections: import("zod").ZodArray<import("zod").ZodPipe<import("zod").ZodObject<{
@@ -17938,19 +17884,6 @@ declare const router: {
17938
17884
  }>>>;
17939
17885
  homepage: import("zod").ZodNullable<import("zod").ZodObject<{
17940
17886
  layoutId: import("zod").ZodString;
17941
- blocks: import("zod").ZodArray<import("zod").ZodObject<{
17942
- type: import("zod").ZodString;
17943
- content: import("zod").ZodUnknown;
17944
- settings: import("zod").ZodOptional<import("zod").ZodUnknown>;
17945
- repeatableItems: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodObject<{
17946
- tempId: import("zod").ZodString;
17947
- parentTempId: import("zod").ZodNullable<import("zod").ZodString>;
17948
- fieldName: import("zod").ZodString;
17949
- content: import("zod").ZodUnknown;
17950
- settings: import("zod").ZodOptional<import("zod").ZodUnknown>;
17951
- position: import("zod").ZodString;
17952
- }, import("zod/v4/core").$strip>>>;
17953
- }, import("zod/v4/core").$strip>>;
17954
17887
  }, import("zod/v4/core").$strip>>;
17955
17888
  }, import("zod/v4/core").$strip>;
17956
17889
  }, import("zod/v4/core").$strip>, import("@orpc/server").Schema<{
@@ -17974,13 +17907,12 @@ declare const router: {
17974
17907
  deletedDefinitionTypes: string[];
17975
17908
  count: number;
17976
17909
  } | null;
17977
- homepage: {
17910
+ homepage: ({
17978
17911
  created: false;
17979
17912
  } | {
17980
17913
  created: true;
17981
17914
  pageId: string;
17982
- blockCount: number;
17983
- } | null;
17915
+ }) | null;
17984
17916
  }, {
17985
17917
  blocks: {
17986
17918
  count: number;
@@ -18002,13 +17934,12 @@ declare const router: {
18002
17934
  deletedDefinitionTypes: string[];
18003
17935
  count: number;
18004
17936
  } | null;
18005
- homepage: {
17937
+ homepage: ({
18006
17938
  created: false;
18007
17939
  } | {
18008
17940
  created: true;
18009
17941
  pageId: string;
18010
- blockCount: number;
18011
- } | null;
17942
+ }) | null;
18012
17943
  }>, Record<never, never>, Record<never, never>>;
18013
17944
  };
18014
17945
  drafts: {
package/dist/threads.d.ts CHANGED
@@ -34,8 +34,8 @@ export declare const BUILD_STATUS_ENTRY = "camox.build-status";
34
34
  export declare const RECONCILE_ENTRY = "camox.reconcile";
35
35
  /**
36
36
  * The kind of the transcript entry opening a draft's thread a Live thread handed a request off
37
- * to: the Brief, which the model reads and editors see as from Camox, ahead of the editor's
38
- * messages. In a draft Camox opened, such as its Patrol Draft, Camox's own messages are Briefs
37
+ * to: the Brief, which the model reads ahead of the editor's messages, and editors see collapsed
38
+ * after them, so the request reads on as they sent it. In a draft Camox opened, such as its Patrol Draft, Camox's own messages are Briefs
39
39
  * too, with their `operationId` as data, and start the agent's run, which acts for Camox.
40
40
  */
41
41
  export declare const BRIEF_ENTRY = "camox.brief";
@@ -48,8 +48,8 @@ export declare const DRAFT_ENDED_ENTRY = "camox.draft-ended";
48
48
  /**
49
49
  * The kind of the transcript entry a Live thread starts afresh from once its agent handed a
50
50
  * request off to a draft (ADR 0036): a reset of Pi's context, with a `startedDraftCardSchema` as
51
- * data. Editors see it as the draft's card, alone, and the model reads a note naming the draft.
52
- * Everything before it is left out of both.
51
+ * data. Editors see an empty thread, and the model reads a note naming the draft. Everything
52
+ * before it is left out of both.
53
53
  */
54
54
  export declare const DRAFT_CARD_ENTRY = "camox.draft-card";
55
55
  /**
@@ -330,6 +330,15 @@ export type ThreadMessage = {
330
330
  from: "camox";
331
331
  text: string;
332
332
  } |
333
+ /**
334
+ * The Brief the agent on Live wrote as it handed the request off to the draft, shown after the
335
+ * editor's messages it opened the thread with, though the model reads it before them.
336
+ */
337
+ {
338
+ id: string;
339
+ from: "brief";
340
+ text: string;
341
+ } |
333
342
  /** The agent's final message of a turn, in Markdown. */
334
343
  {
335
344
  id: string;
@@ -470,6 +479,11 @@ type ThreadStep = {
470
479
  {
471
480
  type: "camox";
472
481
  text: string;
482
+ } |
483
+ /** The Brief opening a draft's thread a Live thread handed a request off to. */
484
+ {
485
+ type: "brief";
486
+ text: string;
473
487
  } | {
474
488
  type: "answer";
475
489
  text: string;
package/dist/threads.js CHANGED
@@ -32,8 +32,8 @@ const BUILD_STATUS_ENTRY = "camox.build-status";
32
32
  const RECONCILE_ENTRY = "camox.reconcile";
33
33
  /**
34
34
  * The kind of the transcript entry opening a draft's thread a Live thread handed a request off
35
- * to: the Brief, which the model reads and editors see as from Camox, ahead of the editor's
36
- * messages. In a draft Camox opened, such as its Patrol Draft, Camox's own messages are Briefs
35
+ * to: the Brief, which the model reads ahead of the editor's messages, and editors see collapsed
36
+ * after them, so the request reads on as they sent it. In a draft Camox opened, such as its Patrol Draft, Camox's own messages are Briefs
37
37
  * too, with their `operationId` as data, and start the agent's run, which acts for Camox.
38
38
  */
39
39
  const BRIEF_ENTRY = "camox.brief";
@@ -46,8 +46,8 @@ const DRAFT_ENDED_ENTRY = "camox.draft-ended";
46
46
  /**
47
47
  * The kind of the transcript entry a Live thread starts afresh from once its agent handed a
48
48
  * request off to a draft (ADR 0036): a reset of Pi's context, with a `startedDraftCardSchema` as
49
- * data. Editors see it as the draft's card, alone, and the model reads a note naming the draft.
50
- * Everything before it is left out of both.
49
+ * data. Editors see an empty thread, and the model reads a note naming the draft. Everything
50
+ * before it is left out of both.
51
51
  */
52
52
  const DRAFT_CARD_ENTRY = "camox.draft-card";
53
53
  /**
@@ -375,14 +375,7 @@ function threadStepOf(entry) {
375
375
  ...push
376
376
  };
377
377
  }
378
- if (entry.kind === "camox.draft-card") {
379
- const card = startedDraftCardSchema.safeParse(entry.data).data;
380
- return card && {
381
- id,
382
- type: "draft",
383
- ...card
384
- };
385
- }
378
+ if (entry.kind === "camox.draft-card") return void 0;
386
379
  const message = entry.model?.find((candidate) => candidate.role !== "system");
387
380
  if (!message) return void 0;
388
381
  if (message.role === "user") {
@@ -407,7 +400,7 @@ function threadStepOf(entry) {
407
400
  }
408
401
  if (entry.kind === "camox.brief") return {
409
402
  id,
410
- type: "camox",
403
+ type: z.object({ operationId: z.string() }).safeParse(entry.data).success ? "camox" : "brief",
411
404
  text: textOf(message.content)
412
405
  };
413
406
  if (entry.kind === "camox.reconcile") {
@@ -474,101 +467,118 @@ function threadViewOf({ steps, running, toolCalls = [], callingTools = false, ch
474
467
  });
475
468
  endTurn();
476
469
  };
477
- for (const step of steps) switch (step.type) {
478
- case "editor":
479
- if (unfinished) fail();
480
- messages.push({
481
- id: step.id,
482
- from: "editor",
483
- sender: step.sender,
484
- text: step.text
485
- });
486
- waiting = step.id;
487
- break;
488
- case "connectedAgent":
489
- if (unfinished) fail();
490
- messages.push({
491
- id: step.id,
492
- from: "connectedAgent",
493
- member: step.member,
494
- client: step.client,
495
- text: step.text
496
- });
497
- waiting = step.id;
498
- break;
499
- case "camox":
500
- if (unfinished) fail();
501
- messages.push({
502
- id: step.id,
503
- from: "camox",
504
- text: step.text
505
- });
506
- waiting = step.id;
507
- break;
508
- case "stopped":
509
- if (waiting === void 0) break;
510
- messages.push({
511
- id: step.id,
512
- from: "stopped",
513
- sender: step.sender
514
- });
515
- endTurn();
516
- break;
517
- case "answer":
518
- if (waiting === void 0 && messages.at(-1)?.from === "stopped") break;
519
- messages.push({
520
- id: step.id,
521
- from: "agent",
522
- text: step.text
523
- });
524
- {
525
- const before = {
526
- waiting,
527
- status
470
+ /** A hand-off's Brief, shown once the editors' messages it opened the thread with are. */
471
+ let brief;
472
+ const showBrief = () => {
473
+ if (brief) messages.push(brief);
474
+ brief = void 0;
475
+ };
476
+ for (const step of steps) {
477
+ if (step.type !== "editor" && step.type !== "connectedAgent") showBrief();
478
+ switch (step.type) {
479
+ case "brief":
480
+ brief = {
481
+ id: step.id,
482
+ from: "brief",
483
+ text: step.text
528
484
  };
485
+ break;
486
+ case "editor":
487
+ if (unfinished) fail();
488
+ messages.push({
489
+ id: step.id,
490
+ from: "editor",
491
+ sender: step.sender,
492
+ text: step.text
493
+ });
494
+ waiting = step.id;
495
+ break;
496
+ case "connectedAgent":
497
+ if (unfinished) fail();
498
+ messages.push({
499
+ id: step.id,
500
+ from: "connectedAgent",
501
+ member: step.member,
502
+ client: step.client,
503
+ text: step.text
504
+ });
505
+ waiting = step.id;
506
+ break;
507
+ case "camox":
508
+ if (unfinished) fail();
509
+ messages.push({
510
+ id: step.id,
511
+ from: "camox",
512
+ text: step.text
513
+ });
514
+ waiting = step.id;
515
+ break;
516
+ case "stopped":
517
+ if (waiting === void 0) break;
518
+ messages.push({
519
+ id: step.id,
520
+ from: "stopped",
521
+ sender: step.sender
522
+ });
523
+ endTurn();
524
+ break;
525
+ case "answer":
526
+ if (waiting === void 0 && messages.at(-1)?.from === "stopped") break;
527
+ messages.push({
528
+ id: step.id,
529
+ from: "agent",
530
+ text: step.text
531
+ });
532
+ {
533
+ const before = {
534
+ waiting,
535
+ status
536
+ };
537
+ endTurn();
538
+ answered = before;
539
+ }
540
+ break;
541
+ case "draft":
542
+ if (waiting === void 0 && messages.at(-1)?.from === "stopped") break;
543
+ {
544
+ const draft = drafts[step.draftId];
545
+ messages.push({
546
+ id: step.id,
547
+ from: "draft",
548
+ draftId: step.draftId,
549
+ name: draft?.name ?? step.name,
550
+ ...draft?.ended && { ended: draft.ended }
551
+ });
552
+ }
529
553
  endTurn();
530
- answered = before;
531
- }
532
- break;
533
- case "draft":
534
- if (waiting === void 0 && messages.at(-1)?.from === "stopped") break;
535
- {
536
- const draft = drafts[step.draftId];
554
+ break;
555
+ case "continued":
556
+ if (!answered) break;
557
+ if (messages.at(-1)?.from === "agent") messages.pop();
558
+ waiting = answered.waiting;
559
+ status = answered.status === "fixing" ? "fixing" : void 0;
560
+ answered = void 0;
561
+ break;
562
+ case "build":
563
+ status = step.status;
564
+ break;
565
+ case "push":
537
566
  messages.push({
538
567
  id: step.id,
539
- from: "draft",
540
- draftId: step.draftId,
541
- name: draft?.name ?? step.name,
542
- ...draft?.ended && { ended: draft.ended }
568
+ from: "push",
569
+ sender: step.pusher,
570
+ subjects: step.subjects,
571
+ outcome: step.outcome
543
572
  });
544
- }
545
- endTurn();
546
- break;
547
- case "continued":
548
- if (!answered) break;
549
- if (messages.at(-1)?.from === "agent") messages.pop();
550
- waiting = answered.waiting;
551
- status = answered.status === "fixing" ? "fixing" : void 0;
552
- answered = void 0;
553
- break;
554
- case "build":
555
- status = step.status;
556
- break;
557
- case "push":
558
- messages.push({
559
- id: step.id,
560
- from: "push",
561
- sender: step.pusher,
562
- subjects: step.subjects,
563
- outcome: step.outcome
564
- });
565
- break;
566
- case "tools":
567
- unfinished = false;
568
- status = void 0;
569
- break;
570
- case "unfinished": unfinished = true;
573
+ break;
574
+ case "tools":
575
+ unfinished = false;
576
+ status = void 0;
577
+ break;
578
+ case "unfinished": unfinished = true;
579
+ }
571
580
  }
581
+ showBrief();
572
582
  if (!running) fail();
573
583
  if (!running) return {
574
584
  messages,
@@ -607,7 +617,7 @@ const callsChangingTool = (block) => block.type === "toolCall" && changes(block.
607
617
  const callsLook = (block) => block.type === "toolCall" && block.name === "look";
608
618
  /**
609
619
  * Adds an entry to the thread's state as Pi places it. An entry heading a new context, such as
610
- * the card a Live thread starts afresh from, leaves out every step before its head, as Pi's
620
+ * the one a Live thread starts afresh from, leaves out every step before its head, as Pi's
611
621
  * snapshots do.
612
622
  */
613
623
  const append = (state, entry) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camox/api-contract",
3
- "version": "0.71.0",
3
+ "version": "0.72.0",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "src",
package/src/drafts.ts CHANGED
@@ -50,9 +50,9 @@ export const LOOK_LOCALE_PARAM = "locale";
50
50
 
51
51
  /**
52
52
  * The look document's Block Definition mode: instead of a page, the block of the Block Definition
53
- * `LOOK_BLOCK_DEFINITION_PARAM` names, alone in the site's shell, with its default content and
54
- * Block Settings and the overrides `LOOK_CONTENT_PARAM` and `LOOK_SETTINGS_PARAM` give as JSON, in
55
- * the shape the content tools take.
53
+ * `LOOK_BLOCK_DEFINITION_PARAM` names, alone in the site's shell, with empty fields and the
54
+ * content and Block Settings `LOOK_CONTENT_PARAM` and `LOOK_SETTINGS_PARAM` give as JSON merged
55
+ * on, in the shape the content tools take.
56
56
  */
57
57
  export const LOOK_BLOCK_DEFINITION_PARAM = "blockDefinition";
58
58
  export const LOOK_CONTENT_PARAM = "content";
@@ -60,8 +60,8 @@ export const LOOK_SETTINGS_PARAM = "settings";
60
60
 
61
61
  /**
62
62
  * The look document's Layout Definition mode: instead of a page, the curated Layout
63
- * `LOOK_LAYOUT_DEFINITION_PARAM` names, alone in the site's shell, its blocks with their default
64
- * content and Block Settings and the overrides `LOOK_BLOCKS_PARAM` gives as JSON, by Block
63
+ * `LOOK_LAYOUT_DEFINITION_PARAM` names, alone in the site's shell, its blocks with empty fields
64
+ * and the content and Block Settings `LOOK_BLOCKS_PARAM` gives as JSON merged on, by Block
65
65
  * Definition name (`{ navbar: { content, settings } }`), around a labelled placeholder where a
66
66
  * page's content goes, as tall as `LOOK_PLACEHOLDER_PARAM` says: one of `LOOK_PLACEHOLDER_HEIGHTS`,
67
67
  * the first without.
@@ -0,0 +1,52 @@
1
+ /**
2
+ * What a field holds before anyone writes it: the empty value of its kind. Fields declare no
3
+ * default, so content missing a field, or holding a value of the wrong kind, reads as this.
4
+ * Shared by the SDK (rendering, new repeatable items, authoring forms) and the API (creating
5
+ * content, conforming it on read), so both agree on what an unset field is.
6
+ */
7
+
8
+ /** A link that goes nowhere yet. */
9
+ export const EMPTY_LINK = { type: "external", text: "", href: "", newTab: false } as const;
10
+
11
+ function isObject(value: unknown): value is Record<string, unknown> {
12
+ return typeof value === "object" && value !== null && !Array.isArray(value);
13
+ }
14
+
15
+ /**
16
+ * The empty value of a field, from its stored JSON Schema (`fieldType` and, for an enum, its
17
+ * options): `""` for text, a link to nowhere, `false`, an enum's first option, `null` for one
18
+ * asset or record, and `[]` for lists and repeaters. A fresh copy each call.
19
+ */
20
+ export function emptyFieldValue(fieldSchema: unknown): unknown {
21
+ const schema = isObject(fieldSchema) ? fieldSchema : {};
22
+ switch (schema.fieldType) {
23
+ case "Link":
24
+ return { ...EMPTY_LINK };
25
+ case "Boolean":
26
+ return false;
27
+ case "Enum": {
28
+ const options = Array.isArray(schema.enum) ? schema.enum : [];
29
+ const first = options.find((option) => typeof option === "string");
30
+ return first ?? "";
31
+ }
32
+ case "Image":
33
+ case "File":
34
+ case "Reference":
35
+ return null;
36
+ case "ImageList":
37
+ case "FileList":
38
+ case "ReferenceList":
39
+ case "Repeater":
40
+ return [];
41
+ default:
42
+ return "";
43
+ }
44
+ }
45
+
46
+ /** The empty value of every field of a content or settings schema, by name. */
47
+ export function emptyFieldValues(schema: unknown): Record<string, unknown> {
48
+ const properties = isObject(schema) && isObject(schema.properties) ? schema.properties : {};
49
+ return Object.fromEntries(
50
+ Object.entries(properties).map(([name, field]) => [name, emptyFieldValue(field)]),
51
+ );
52
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./artifact.ts";
2
+ export * from "./empty-values.ts";
2
3
  export * from "./icons.ts";
3
4
  export * from "./ids.ts";
4
5
  export * from "./record-templates.ts";
package/src/threads.ts CHANGED
@@ -44,8 +44,8 @@ export const RECONCILE_ENTRY = "camox.reconcile";
44
44
 
45
45
  /**
46
46
  * The kind of the transcript entry opening a draft's thread a Live thread handed a request off
47
- * to: the Brief, which the model reads and editors see as from Camox, ahead of the editor's
48
- * messages. In a draft Camox opened, such as its Patrol Draft, Camox's own messages are Briefs
47
+ * to: the Brief, which the model reads ahead of the editor's messages, and editors see collapsed
48
+ * after them, so the request reads on as they sent it. In a draft Camox opened, such as its Patrol Draft, Camox's own messages are Briefs
49
49
  * too, with their `operationId` as data, and start the agent's run, which acts for Camox.
50
50
  */
51
51
  export const BRIEF_ENTRY = "camox.brief";
@@ -60,8 +60,8 @@ export const DRAFT_ENDED_ENTRY = "camox.draft-ended";
60
60
  /**
61
61
  * The kind of the transcript entry a Live thread starts afresh from once its agent handed a
62
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.
63
+ * data. Editors see an empty thread, and the model reads a note naming the draft. Everything
64
+ * before it is left out of both.
65
65
  */
66
66
  export const DRAFT_CARD_ENTRY = "camox.draft-card";
67
67
 
@@ -302,6 +302,11 @@ export type ThreadMessage =
302
302
  }
303
303
  /** Camox's own message, starting a turn it began itself: why it did. */
304
304
  | { id: string; from: "camox"; text: string }
305
+ /**
306
+ * The Brief the agent on Live wrote as it handed the request off to the draft, shown after the
307
+ * editor's messages it opened the thread with, though the model reads it before them.
308
+ */
309
+ | { id: string; from: "brief"; text: string }
305
310
  /** The agent's final message of a turn, in Markdown. */
306
311
  | { id: string; from: "agent"; text: string }
307
312
  /**
@@ -406,6 +411,8 @@ type ThreadStep = { id: string } & (
406
411
  | { type: "connectedAgent"; member: ThreadSender; client: ConnectedAgentClient; text: string }
407
412
  /** Camox starting a turn of its own, saying why. */
408
413
  | { type: "camox"; text: string }
414
+ /** The Brief opening a draft's thread a Live thread handed a request off to. */
415
+ | { type: "brief"; text: string }
409
416
  | { type: "answer"; text: string }
410
417
  /** A round of tool calls, or their results: the run goes on after it. */
411
418
  | { type: "tools" }
@@ -560,11 +567,8 @@ function threadStepOf(entry: EntryRecord): ThreadStep | undefined {
560
567
  const push = pushDataSchema.safeParse(entry.data).data;
561
568
  return push && { id, type: "push", ...push };
562
569
  }
563
- // The model reads a note: editors see the card it starts the Live thread afresh with.
564
- if (entry.kind === DRAFT_CARD_ENTRY) {
565
- const card = startedDraftCardSchema.safeParse(entry.data).data;
566
- return card && { id, type: "draft", ...card };
567
- }
570
+ // Only the model reads it, as a note naming the draft: editors see the Live thread start empty.
571
+ if (entry.kind === DRAFT_CARD_ENTRY) return undefined;
568
572
  const message = entry.model?.find((candidate) => candidate.role !== "system");
569
573
  if (!message) return undefined;
570
574
  if (message.role === "user") {
@@ -583,7 +587,12 @@ function threadStepOf(entry: EntryRecord): ThreadStep | undefined {
583
587
  }
584
588
  );
585
589
  }
586
- if (entry.kind === BRIEF_ENTRY) return { id, type: "camox", text: textOf(message.content) };
590
+ if (entry.kind === BRIEF_ENTRY) {
591
+ // Only Camox's own messages, in a draft it opened, carry an operation id: a hand-off's
592
+ // Brief is written ahead of the editor's messages, which start the run.
593
+ const own = z.object({ operationId: z.string() }).safeParse(entry.data).success;
594
+ return { id, type: own ? "camox" : "brief", text: textOf(message.content) };
595
+ }
587
596
  if (entry.kind === RECONCILE_ENTRY) {
588
597
  const reconcile = reconcileDataSchema.safeParse(entry.data).data;
589
598
  return reconcile && { id, type: "camox", text: reconcile.message };
@@ -648,9 +657,19 @@ export function threadViewOf({
648
657
  if (waiting !== undefined) messages.push({ id: `${waiting}:failed`, from: "failed" });
649
658
  endTurn();
650
659
  };
660
+ /** A hand-off's Brief, shown once the editors' messages it opened the thread with are. */
661
+ let brief: Extract<ThreadMessage, { from: "brief" }> | undefined;
662
+ const showBrief = () => {
663
+ if (brief) messages.push(brief);
664
+ brief = undefined;
665
+ };
651
666
 
652
667
  for (const step of steps) {
668
+ if (step.type !== "editor" && step.type !== "connectedAgent") showBrief();
653
669
  switch (step.type) {
670
+ case "brief":
671
+ brief = { id: step.id, from: "brief", text: step.text };
672
+ break;
654
673
  case "editor":
655
674
  // Pi places a steering message only once a model call is settled: after a round of tool
656
675
  // calls, or with the answer ending the run. Never between a failed call and its retry, so
@@ -738,6 +757,7 @@ export function threadViewOf({
738
757
  break;
739
758
  }
740
759
  }
760
+ showBrief();
741
761
  if (!running) fail();
742
762
  if (!running) return { messages, running };
743
763
  return {
@@ -798,7 +818,7 @@ const callsLook = (block: AssistantMessage["content"][number]) =>
798
818
 
799
819
  /**
800
820
  * Adds an entry to the thread's state as Pi places it. An entry heading a new context, such as
801
- * the card a Live thread starts afresh from, leaves out every step before its head, as Pi's
821
+ * the one a Live thread starts afresh from, leaves out every step before its head, as Pi's
802
822
  * snapshots do.
803
823
  */
804
824
  const append = (state: ThreadState, entry: EntryRecord | undefined): ThreadState => {