@company-semantics/contracts 93.3.0 → 95.0.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/package.json +1 -1
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +32 -7
- package/src/billing/README.md +55 -3
- package/src/billing/__tests__/price-list-tiers.test.ts +129 -0
- package/src/billing/__tests__/processing-modes.test.ts +19 -0
- package/src/billing/__tests__/schemas.test.ts +44 -21
- package/src/billing/index.ts +16 -0
- package/src/billing/schemas.ts +145 -24
- package/src/billing/types.ts +23 -1
- package/src/billing/vocabulary.ts +37 -0
- package/src/chat/README.md +26 -0
- package/src/chat/__tests__/chat-exports.test.ts +61 -0
- package/src/chat/__tests__/message-actions.test.ts +208 -0
- package/src/chat/__tests__/message-mentions.test.ts +4 -0
- package/src/chat/__tests__/multi-party.test.ts +3 -0
- package/src/chat/__tests__/stream-metadata.test.ts +73 -0
- package/src/chat/__tests__/turn-kind.test.ts +34 -0
- package/src/chat/__tests__/turn-wire.test.ts +93 -0
- package/src/chat/index.ts +17 -0
- package/src/chat/message-actions.ts +143 -0
- package/src/chat/schemas.ts +30 -0
- package/src/chat/stream-metadata.ts +40 -0
- package/src/chat/turn-kind.ts +43 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat message action eligibility (chat-action-system).
|
|
3
|
+
*
|
|
4
|
+
* Which MESSAGE actions (copy, feedback) and CONVERSATION actions (regenerate,
|
|
5
|
+
* edit) a chat turn offers is decided by ONE pure function over wire facts:
|
|
6
|
+
* the turn's role, its TurnKind and its position. The app renders the action
|
|
7
|
+
* row from `messageActionsFor`; the backend enforces the SAME function on every
|
|
8
|
+
* regenerate, edit and feedback request. Hiding a button is never the security
|
|
9
|
+
* boundary.
|
|
10
|
+
*
|
|
11
|
+
* Execution actions (Cancel, Continue, Retry operation, Run again) are NOT
|
|
12
|
+
* here: they act on durable work, not on a message, and reach a person as
|
|
13
|
+
* affordances from the closed catalog (affordance-system).
|
|
14
|
+
*
|
|
15
|
+
* INVARIANTS:
|
|
16
|
+
* - PURE and TOTAL: no I/O, no clock, no defaults. Every input yields a list.
|
|
17
|
+
* - An unsettled turn offers NOTHING — the row appears once, when it settles.
|
|
18
|
+
* - Regenerate is never offered on a workflow_launch, workflow_settlement,
|
|
19
|
+
* input_request_launch, proactive or refusal turn: it would ask the model for
|
|
20
|
+
* new prose without re-running the work.
|
|
21
|
+
* - Regenerate and edit are offered only on the latest turn of the active
|
|
22
|
+
* branch; edit only to the reader who authored it.
|
|
23
|
+
* - An input the schema does not recognize (an unknown kind or role) offers
|
|
24
|
+
* NOTHING. Unknown is never permissive.
|
|
25
|
+
*
|
|
26
|
+
* LOAD ORDER: imports only zod and `./turn-kind` (a leaf); never `./schemas`.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { z } from "zod";
|
|
30
|
+
import { TurnKindSchema, type TurnKind } from "./turn-kind";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The message and conversation actions a turn's action row can carry. CLOSED
|
|
34
|
+
* and append-only. The order is the order the row draws them in, and the
|
|
35
|
+
* order `messageActionsFor` returns them in.
|
|
36
|
+
*/
|
|
37
|
+
export const MESSAGE_ACTION_IDS = [
|
|
38
|
+
/** Copy the turn's readable prose. Side-effect-free. */
|
|
39
|
+
"copy",
|
|
40
|
+
/** Rate the turn up or down. Measures a message; never approves anything. */
|
|
41
|
+
"feedback",
|
|
42
|
+
/** A new answer to the same request; supersedes the old one. Never re-runs work. */
|
|
43
|
+
"regenerate",
|
|
44
|
+
/** Rewrite the reader's own prompt; supersedes the branch after it. */
|
|
45
|
+
"edit",
|
|
46
|
+
] as const;
|
|
47
|
+
|
|
48
|
+
export const MessageActionIdSchema = z.enum(MESSAGE_ACTION_IDS);
|
|
49
|
+
export type MessageActionId = z.infer<typeof MessageActionIdSchema>;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The wire facts eligibility is decided from — all of them known to the app
|
|
53
|
+
* when it draws a turn and to the backend when it receives a request.
|
|
54
|
+
*/
|
|
55
|
+
export const MessageActionContextSchema = z.object({
|
|
56
|
+
role: z.enum(["user", "assistant"]),
|
|
57
|
+
turnKind: TurnKindSchema,
|
|
58
|
+
/**
|
|
59
|
+
* The turn is done: not streaming, and no launched workflow behind it is in
|
|
60
|
+
* flight or awaiting settlement delivery.
|
|
61
|
+
*/
|
|
62
|
+
settled: z.boolean(),
|
|
63
|
+
/** The turn is the latest of its role on the active branch. */
|
|
64
|
+
isLatestOnActiveBranch: z.boolean(),
|
|
65
|
+
/** The reader is the turn's author (a user turn they wrote). */
|
|
66
|
+
authoredByReader: z.boolean(),
|
|
67
|
+
});
|
|
68
|
+
export type MessageActionContext = z.infer<typeof MessageActionContextSchema>;
|
|
69
|
+
|
|
70
|
+
/** `always` is offered on every settled turn; `latest` only on the latest. */
|
|
71
|
+
type ActionRule = {
|
|
72
|
+
always: readonly MessageActionId[];
|
|
73
|
+
latest: readonly MessageActionId[];
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The eligibility table. `satisfies Record<TurnKind, …>` makes a new TurnKind a
|
|
78
|
+
* compile error here until its eligibility is decided. Only a conversation
|
|
79
|
+
* turn is a request/answer pair, so only it offers regenerate or edit; a user
|
|
80
|
+
* row of any other kind (none exists today) is copy-only.
|
|
81
|
+
*/
|
|
82
|
+
const TURN_ACTIONS = {
|
|
83
|
+
conversation: {
|
|
84
|
+
user: { always: ["copy"], latest: ["edit"] },
|
|
85
|
+
assistant: { always: ["copy", "feedback"], latest: ["regenerate"] },
|
|
86
|
+
},
|
|
87
|
+
workflow_launch: {
|
|
88
|
+
user: { always: ["copy"], latest: [] },
|
|
89
|
+
assistant: { always: ["copy", "feedback"], latest: [] },
|
|
90
|
+
},
|
|
91
|
+
workflow_settlement: {
|
|
92
|
+
user: { always: ["copy"], latest: [] },
|
|
93
|
+
assistant: { always: ["copy", "feedback"], latest: [] },
|
|
94
|
+
},
|
|
95
|
+
input_request_launch: {
|
|
96
|
+
user: { always: ["copy"], latest: [] },
|
|
97
|
+
assistant: { always: ["copy", "feedback"], latest: [] },
|
|
98
|
+
},
|
|
99
|
+
proactive: {
|
|
100
|
+
user: { always: ["copy"], latest: [] },
|
|
101
|
+
assistant: { always: ["copy"], latest: [] },
|
|
102
|
+
},
|
|
103
|
+
refusal: {
|
|
104
|
+
user: { always: ["copy"], latest: [] },
|
|
105
|
+
assistant: { always: ["copy"], latest: [] },
|
|
106
|
+
},
|
|
107
|
+
} as const satisfies Record<
|
|
108
|
+
TurnKind,
|
|
109
|
+
Record<MessageActionContext["role"], ActionRule>
|
|
110
|
+
>;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The actions a turn offers, in MESSAGE_ACTION_IDS order.
|
|
114
|
+
*
|
|
115
|
+
* - Input that does not parse (unknown kind, unknown role) → [].
|
|
116
|
+
* - Not settled → [].
|
|
117
|
+
* - User turn → its `always`, plus `latest` iff latest on the active branch
|
|
118
|
+
* AND authored by the reader.
|
|
119
|
+
* - Assistant turn → its `always`, plus `latest` iff latest on the active
|
|
120
|
+
* branch.
|
|
121
|
+
*/
|
|
122
|
+
export function messageActionsFor(
|
|
123
|
+
ctx: MessageActionContext,
|
|
124
|
+
): readonly MessageActionId[] {
|
|
125
|
+
const parsed = MessageActionContextSchema.safeParse(ctx);
|
|
126
|
+
if (!parsed.success || !parsed.data.settled) return [];
|
|
127
|
+
const { role, turnKind, isLatestOnActiveBranch, authoredByReader } =
|
|
128
|
+
parsed.data;
|
|
129
|
+
const rule: ActionRule = TURN_ACTIONS[turnKind][role];
|
|
130
|
+
const latest =
|
|
131
|
+
role === "user"
|
|
132
|
+
? isLatestOnActiveBranch && authoredByReader
|
|
133
|
+
: isLatestOnActiveBranch;
|
|
134
|
+
return latest ? [...rule.always, ...rule.latest] : [...rule.always];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Whether `action` is among `messageActionsFor(ctx)` — the backend's check. */
|
|
138
|
+
export function isMessageActionAllowed(
|
|
139
|
+
ctx: MessageActionContext,
|
|
140
|
+
action: MessageActionId,
|
|
141
|
+
): boolean {
|
|
142
|
+
return messageActionsFor(ctx).includes(action);
|
|
143
|
+
}
|
package/src/chat/schemas.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
proactiveChatFields,
|
|
26
26
|
TitleSourceSchema,
|
|
27
27
|
} from "./enums";
|
|
28
|
+
import { TurnKindSchema } from "./turn-kind";
|
|
28
29
|
|
|
29
30
|
// The chat vocabulary and the SSE event contract, re-exported so this module
|
|
30
31
|
// stays the single import site for every chat schema.
|
|
@@ -285,6 +286,35 @@ export const ChatMessageSchema = z.object({
|
|
|
285
286
|
*
|
|
286
287
|
*/
|
|
287
288
|
discussionScope: ChatDiscussionScopeSchema.optional(),
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* The PRESENTATION GROUP this turn belongs to: every row of one launched
|
|
292
|
+
* piece of work draws as ONE response, with one action row
|
|
293
|
+
* (chat-action-system). REQUIRED, and null means the turn stands alone —
|
|
294
|
+
* every row has an answer, so there is no absence to make optional.
|
|
295
|
+
*
|
|
296
|
+
* A group is a SEPARATE CONCEPT FROM THE WORKFLOW it may name
|
|
297
|
+
* (INV-LAUNCHTURN-2): it is a presentation fact, never a workflow or run id,
|
|
298
|
+
* and a client must not join on it as one. The settlement row and the review
|
|
299
|
+
* hand-off INHERIT the anchor's group, so a launch, its settlement and its
|
|
300
|
+
* hand-off render together. Superseded rows are never on this wire — the
|
|
301
|
+
* transcript carries the active branch only.
|
|
302
|
+
*/
|
|
303
|
+
presentationGroupId: z.string().uuid().nullable(),
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* What KIND of turn this row is — derived by the server from what persisted
|
|
307
|
+
* it, never inferred by a client from content. REQUIRED: every row is exactly
|
|
308
|
+
* one kind. Not a principal kind; `author` is unchanged by it.
|
|
309
|
+
*/
|
|
310
|
+
turnKind: TurnKindSchema,
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The READER's current rating of this turn — the person this response was
|
|
314
|
+
* projected for, never an aggregate of other participants. REQUIRED so a
|
|
315
|
+
* reload restores the rating; null on user turns and on unrated turns.
|
|
316
|
+
*/
|
|
317
|
+
myFeedback: z.enum(["up", "down"]).nullable(),
|
|
288
318
|
});
|
|
289
319
|
|
|
290
320
|
// =============================================================================
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live-stream message metadata (chat-action-system).
|
|
3
|
+
*
|
|
4
|
+
* The `messageMetadata` the backend attaches to the AI SDK `start` chunk of a
|
|
5
|
+
* streamed assistant turn. It exists so a LIVE message is already the
|
|
6
|
+
* persisted message: the app adopts `messageId` as the live message's id, so
|
|
7
|
+
* the live message's id IS the persisted row id (doctrine rule 10), and it
|
|
8
|
+
* carries its presentation group and turn kind before any reload. Without
|
|
9
|
+
* it, a client could only draw a live turn as a loose message and regroup it
|
|
10
|
+
* after the next fetch — the flicker the presentation group exists to end.
|
|
11
|
+
*
|
|
12
|
+
* Every field mirrors the same-named field on `ChatMessage`, with the same
|
|
13
|
+
* meaning: a client that reads the live metadata and later reloads the
|
|
14
|
+
* transcript sees the same id, group, kind and timestamp.
|
|
15
|
+
*
|
|
16
|
+
* LOAD ORDER: imports only zod, `../api/primitives` and the `./turn-kind`
|
|
17
|
+
* leaf. It must NEVER import `./schemas`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
import { IsoDateTime } from "../api/primitives";
|
|
22
|
+
import { TurnKindSchema } from "./turn-kind";
|
|
23
|
+
|
|
24
|
+
export const ChatStreamMessageMetadataSchema = z.object({
|
|
25
|
+
/** The persisted row id of this message — the live message adopts it. */
|
|
26
|
+
messageId: z.string().uuid(),
|
|
27
|
+
/**
|
|
28
|
+
* The presentation group this turn joins; null when it stands alone.
|
|
29
|
+
* REQUIRED, exactly as on `ChatMessage` — every turn has an answer.
|
|
30
|
+
*/
|
|
31
|
+
presentationGroupId: z.string().uuid().nullable(),
|
|
32
|
+
/** What kind of turn this is, derived by the server. REQUIRED. */
|
|
33
|
+
turnKind: TurnKindSchema,
|
|
34
|
+
/** The persisted row's creation time, so the live turn orders as it will on reload. */
|
|
35
|
+
createdAt: IsoDateTime,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export type ChatStreamMessageMetadata = z.infer<
|
|
39
|
+
typeof ChatStreamMessageMetadataSchema
|
|
40
|
+
>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat turn kind (chat-action-system).
|
|
3
|
+
*
|
|
4
|
+
* Every chat message row is exactly one KIND of turn. The kind is a wire
|
|
5
|
+
* fact the server derives from what persisted the row — never something a
|
|
6
|
+
* client infers from content — so a client can decide how to draw a turn
|
|
7
|
+
* and which message actions it offers from the wire alone.
|
|
8
|
+
*
|
|
9
|
+
* A turn kind is NOT a principal kind: it never changes who authored the
|
|
10
|
+
* row (`author` stays a PrincipalRef), and it is not the presentation group
|
|
11
|
+
* — a group is a separate concept from the workflow it may name.
|
|
12
|
+
*
|
|
13
|
+
* LOAD ORDER: this module is a LEAF. It imports only zod and must NEVER
|
|
14
|
+
* import `./schemas` (or any module that does) — `./schemas`,
|
|
15
|
+
* `./stream-metadata` and `./message-actions` all import this file, and the
|
|
16
|
+
* reverse edge would reach its declarations in the temporal dead zone.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The kinds of chat turn. CLOSED and append-only: a new kind is a new
|
|
23
|
+
* eligibility decision, and every `Record<TurnKind, …>` table must fail to
|
|
24
|
+
* compile until that decision is made. Each kind is defined by the
|
|
25
|
+
* persisted fact that produces it.
|
|
26
|
+
*/
|
|
27
|
+
export const TURN_KINDS = [
|
|
28
|
+
/** A user turn, or a model-authored answer to one — no launch behind it. */
|
|
29
|
+
"conversation",
|
|
30
|
+
/** A launch-only anchor whose launch kind names a workflow. */
|
|
31
|
+
"workflow_launch",
|
|
32
|
+
/** A settlement row: its launch identity carries the `workflow-settled:` prefix. */
|
|
33
|
+
"workflow_settlement",
|
|
34
|
+
/** A launch-only anchor whose launch kind names an input request. */
|
|
35
|
+
"input_request_launch",
|
|
36
|
+
/** An opener written by the ProactiveChatWriter, not in reply to a turn. */
|
|
37
|
+
"proactive",
|
|
38
|
+
/** The row recording a launch the server refused. */
|
|
39
|
+
"refusal",
|
|
40
|
+
] as const;
|
|
41
|
+
|
|
42
|
+
export const TurnKindSchema = z.enum(TURN_KINDS);
|
|
43
|
+
export type TurnKind = z.infer<typeof TurnKindSchema>;
|