@opencxh/domain 1.255.0 → 1.257.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.
@@ -5,7 +5,7 @@ import { ReactionSummary } from '../reaction/types';
5
5
  export type CallStatus = "new" | "connecting" | "ringing" | "connected" | "held" | "ended" | "failed";
6
6
  export type CallDirection = "inbound" | "outbound";
7
7
  export type CallType = "audio" | "video" | "data" | "screen-share";
8
- export type ActivityType = "VOICE_CALL_STARTED" | "VOICE_CALL_ANSWERED" | "VOICE_CALL_HOLD" | "VOICE_CALL_UNHOLD" | "VOICE_CALL_ENDED" | "VOICE_CALL_MISSED" | "VOICE_CALL_VOICEMAIL" | "VIDEO_CALL_STARTED" | "VIDEO_CALL_ANSWERED" | "VIDEO_CALL_HOLD" | "VIDEO_CALL_UNHOLD" | "VIDEO_CALL_ENDED" | "VIDEO_CALL_MISSED" | "EMAIL_RECEIVED" | "EMAIL_SENT" | "CHAT_MESSAGE_SENT" | "CHAT_MESSAGE_RECEIVED" | "CHAT_MEMBER_JOINED" | "CHAT_MEMBER_LEFT" | "CHAT_RENAMED" | "CHAT_CALL_STARTED" | "CHAT_CALL_ENDED" | "CHAT_EVENT" | "AI_MESSAGE_ADDED" | "AI_ACTION_PROPOSED" | "PLAYBOOK_STARTED" | "PLAYBOOK_COMPLETED" | "PLAYBOOK_ESCALATED" | "MEETING_SCHEDULED" | "MEETING_STARTED" | "MEETING_ENDED" | "MEETING_PARTICIPANT_JOINED" | "MEETING_PARTICIPANT_LEFT" | "COMMENT_ADDED" | "FILE_UPLOADED" | "INTERACTION_CREATED" | "INTERACTION_STATUS_CHANGED" | "INTERACTION_ASSIGNED" | "SLA_BREACHED" | "VOICE_CALL_FAILED" | "VIDEO_CALL_FAILED" | "TRANSCRIPT_ADDED" | "WORK_COMMENT_ADDED" | "WORK_ITEM_ASSIGNED" | "WORK_ITEM_STATUS_CHANGED";
8
+ export type ActivityType = "VOICE_CALL_STARTED" | "VOICE_CALL_ANSWERED" | "VOICE_CALL_HOLD" | "VOICE_CALL_UNHOLD" | "VOICE_CALL_ENDED" | "VOICE_CALL_MISSED" | "VOICE_CALL_VOICEMAIL" | "VIDEO_CALL_STARTED" | "VIDEO_CALL_ANSWERED" | "VIDEO_CALL_HOLD" | "VIDEO_CALL_UNHOLD" | "VIDEO_CALL_ENDED" | "VIDEO_CALL_MISSED" | "EMAIL_RECEIVED" | "EMAIL_SENT" | "CHAT_MESSAGE_SENT" | "CHAT_MESSAGE_RECEIVED" | "CHAT_MEMBER_JOINED" | "CHAT_MEMBER_LEFT" | "CHAT_RENAMED" | "CHAT_CALL_STARTED" | "CHAT_CALL_ENDED" | "CHAT_EVENT" | "AI_MESSAGE_ADDED" | "AI_ACTION_PROPOSED" | "PLAYBOOK_STARTED" | "PLAYBOOK_COMPLETED" | "PLAYBOOK_ESCALATED" | "AGENT_DELEGATED" | "AGENT_ANSWERED" | "MEETING_SCHEDULED" | "MEETING_STARTED" | "MEETING_ENDED" | "MEETING_PARTICIPANT_JOINED" | "MEETING_PARTICIPANT_LEFT" | "COMMENT_ADDED" | "FILE_UPLOADED" | "INTERACTION_CREATED" | "INTERACTION_STATUS_CHANGED" | "INTERACTION_ASSIGNED" | "SLA_BREACHED" | "VOICE_CALL_FAILED" | "VIDEO_CALL_FAILED" | "TRANSCRIPT_ADDED" | "WORK_COMMENT_ADDED" | "WORK_ITEM_ASSIGNED" | "WORK_ITEM_STATUS_CHANGED";
9
9
  export interface Attachment {
10
10
  id: string;
11
11
  filename: string;
@@ -299,6 +299,24 @@ export type PlaybookLifecyclePayload = {
299
299
  /** Who the run acted for; saves the UI from fetching the playbook again. */
300
300
  actor?: ActingIdentity;
301
301
  };
302
+ /**
303
+ * One agent asking another, and the answer coming back — the two markers that make delegation
304
+ * visible on the timeline instead of only inside a second thread.
305
+ *
306
+ * Extends the playbook lifecycle payload rather than standing beside it, because these *are*
307
+ * playbook-authored markers: they carry the same `runId` that damps re-triggering, and every
308
+ * reader that already handles a lifecycle marker handles these unchanged.
309
+ */
310
+ export type AgentDelegationPayload = PlaybookLifecyclePayload & {
311
+ /** The agent that asked. */
312
+ fromAgentName: string;
313
+ /** The agent that was asked. Absent on the answer, where `fromAgentName` is the answerer. */
314
+ toAgentName?: string;
315
+ /** The sub-question, as the delegating agent wrote it. Trimmed for the timeline. */
316
+ ask?: string;
317
+ /** The delegated assignment, so the UI can link to its thread. */
318
+ assignmentId?: string;
319
+ };
302
320
  export type Activity = (BaseActivity & {
303
321
  type: "VOICE_CALL_STARTED";
304
322
  payload: VoiceCallPayload;
@@ -392,6 +410,12 @@ export type Activity = (BaseActivity & {
392
410
  }) | (BaseActivity & {
393
411
  type: "PLAYBOOK_ESCALATED";
394
412
  payload: PlaybookLifecyclePayload;
413
+ }) | (BaseActivity & {
414
+ type: "AGENT_DELEGATED";
415
+ payload: AgentDelegationPayload;
416
+ }) | (BaseActivity & {
417
+ type: "AGENT_ANSWERED";
418
+ payload: AgentDelegationPayload;
395
419
  }) | (BaseActivity & {
396
420
  type: "MEETING_SCHEDULED";
397
421
  payload: MeetingPayload;
@@ -0,0 +1 @@
1
+ export * from './types';
@@ -0,0 +1,65 @@
1
+ import { Autonomy } from '../playbook/types';
2
+ /**
3
+ * An **agent**: a digital colleague. A profile plus an identity plus a mandate.
4
+ *
5
+ * **Why this is its own record, next to the user row and the profile.** Those two already
6
+ * existed and pointed at each other: `User.profileId` said which brain, `Playbook.agentId` said
7
+ * which agent, and nothing related them — `agentId: Alice` running on Bob's brain was a valid
8
+ * configuration, and did happen (see `agent/brain.ts` in the ai app). One record ends that.
9
+ *
10
+ * **`id` is the user id.** Not a `userId` column beside a generated key: an agent *is* that user
11
+ * (that is what gives it a real identity — fail-closed authorization, its own name on the
12
+ * timeline, team membership, its own budget), so a second identifier would only be a second thing
13
+ * that can disagree. Every `agentId` in this codebase is therefore also this row's id.
14
+ *
15
+ * **What is deliberately not here.** The name lives on the user row, because that is what every
16
+ * timeline, mention list and assignee picker already reads; duplicating it would let two places
17
+ * disagree about what an agent is called.
18
+ */
19
+ export interface Agent {
20
+ /** The agent's user id. See above: there is no second identifier. */
21
+ id: string;
22
+ organizationId: string;
23
+ /** The brain: the `AIProfile` this agent thinks with. */
24
+ profileId: string;
25
+ /**
26
+ * What this agent is for, in one line.
27
+ *
28
+ * **On the agent, not on the profile.** Another agent reads this line to decide whether to call
29
+ * this one in ({@link Agent.delegateTo}), so it has to describe *this colleague* — and two
30
+ * agents sharing the Support profile are two different colleagues. It used to be
31
+ * `AIProfile.description`, which gave them the same blurb.
32
+ */
33
+ description?: string;
34
+ /**
35
+ * Per-tool confirmation policy: which of this agent's tools count as a write.
36
+ *
37
+ * Moved off the profile because it is a mandate, not expertise: the same Support brain may be
38
+ * trusted to send in one agent's hands and only to propose in another's. Under autonomy
39
+ * "suggest" write tools are held for approval; reads run freely. A tool absent from this list
40
+ * defaults to "write" (safe).
41
+ */
42
+ toolPolicy?: {
43
+ name: string;
44
+ policy: "read" | "write";
45
+ }[];
46
+ /**
47
+ * Does this colleague act, or propose and wait for a human?
48
+ *
49
+ * Absent = `"suggest"`, fail-closed. On the agent because it is the plainest mandate there is,
50
+ * and because it has to be settable for work that has no playbook: an assignment someone typed
51
+ * took its autonomy from the shared "Assist" playbook, which is gone. A triggered assignment
52
+ * still follows its playbook — that is where an organisation configured that scenario.
53
+ */
54
+ autonomy?: Autonomy;
55
+ /**
56
+ * Which colleagues this agent may call in for a sub-question (agent ids).
57
+ *
58
+ * **Opt-in**: absent and `[]` both mean none. Who you may wake is a permission, and a default
59
+ * of "everyone" would make every new agent reachable by every other one the moment it exists.
60
+ *
61
+ * Moved off the profile for the reason the whole record exists: this is a relation *between
62
+ * agents*, and storing it on a shared brain gave two agents one delegate list.
63
+ */
64
+ delegateTo?: string[];
65
+ }
@@ -1,5 +1,6 @@
1
1
  import { AIAttachment, RejectedAIAttachment } from '../../platform/ai-attachments';
2
2
  import { ToolTraceEntry } from '../../platform/ai-tools';
3
+ import { UserType } from '../user/types';
3
4
  export interface AIMessageInput {
4
5
  text: string;
5
6
  type: "text";
@@ -48,6 +49,37 @@ export interface AIMessage {
48
49
  output: AIMessageOutput;
49
50
  accountId?: string;
50
51
  model?: string;
52
+ /**
53
+ * The brain that produced this turn. `accountId` + `model` said *what* answered and never
54
+ * *who*, so "which profile wrote this" was unanswerable after the fact — and a profile people
55
+ * edit is exactly the thing you want to look back at when an answer was wrong.
56
+ *
57
+ * Absent on a row nobody's brain produced: a human writing in an assignment thread.
58
+ */
59
+ profileId?: string;
60
+ /** The agent whose turn this was, when it was an agent's. Absent for the assistant. */
61
+ agentId?: string;
62
+ /**
63
+ * Who put this row in the thread, when it was somebody rather than the assistant.
64
+ *
65
+ * **The shape `getUserProfile` returns**, deliberately: `type` is the existing answer to
66
+ * "human or agent" ({@link UserType}) and inventing a second author vocabulary for one screen
67
+ * is how this codebase ended up asking "is this a message?" in six places.
68
+ *
69
+ * Why it has to be stored: an answer from a colleague-agent arrives as `sender: "user"` — it
70
+ * *is* the user turn of the next model call — so after a reload it read exactly like an
71
+ * instruction from the person who gave the assignment. The difference between material to
72
+ * weigh and an order to follow lived only in the wake note, which is not persisted.
73
+ *
74
+ * `type` is required here even though it is optional on a user record: every writer of this
75
+ * field knows which of the two it is, and "unknown" is precisely the answer that would make
76
+ * the field pointless.
77
+ */
78
+ author?: {
79
+ id: string;
80
+ name: string;
81
+ type: UserType;
82
+ };
51
83
  externalId?: string;
52
84
  /** Tools the assistant invoked while producing this message (final, persisted trace). */
53
85
  toolTrace?: ToolTraceEntry[];
@@ -27,32 +27,24 @@ export interface AIProfile<T extends Record<string, any> = Record<string, any>>
27
27
  systemPrompt?: string;
28
28
  contextIds?: string[];
29
29
  predefinedPrompts?: PredefinedPrompt[];
30
- /** Namespaced tool names this profile may use (default: none enabled). */
31
- enabledTools?: string[];
32
30
  /**
33
- * Per-tool confirmation policy, stored as a LIST (not a keyed map). It sits on existing profile
34
- * rows with a form on top, and a migration to a map would only yield a nicer shape;
35
- * `enabledTools` next to it is a list too.
36
- *
37
- * Under autonomy "suggest", write tools are held for approval; reads run freely. A tool absent
38
- * from this list defaults to "write" (safe).
39
- *
40
- * **Where this does and does not apply.** Enforced in `playbook/ai/gate.ts`, and thereby on
41
- * every autonomous run (workflow or procedure). The interactive assistant does **nothing** with
42
- * it: there is no confirmation step there. So a `write` marking you expect to enforce in a chat
43
- * conversation does nothing.
31
+ * Namespaced tool names this profile may use (default: none enabled).
44
32
  *
45
- * That is an open gap and not a design choice: a confirmation step in the chat is UI plus an
46
- * extra round, and should be a decision rather than a field that silently does nothing.
33
+ * Expertise, not mandate: *what this brain knows how to do*. Whether a given agent is trusted
34
+ * to actually use a write tool is `Agent.toolPolicy`, and which colleagues it may call in is
35
+ * `Agent.delegateTo` — both moved off this record, because two agents can share one brain and
36
+ * still not be trusted with the same things.
47
37
  */
48
- toolPolicy?: {
49
- name: string;
50
- policy: "read" | "write";
51
- }[];
38
+ enabledTools?: string[];
52
39
  /**
53
- * Whether the acting user's PERSONAL MCP tools (their own per-user connections)
54
- * are available on this profile, on top of the admin-curated `enabledTools`.
55
- * "off" (default) = strictly the curated set; "allow" = also include personal.
40
+ * Whether the acting user's PERSONAL MCP tools (their own per-user connections) are available
41
+ * on top of the admin-curated `enabledTools`. "off" (default) = strictly the curated set.
42
+ *
43
+ * **Stays on the profile while `toolPolicy` and `delegateTo` moved to `Agent`**, because this
44
+ * one is only ever true of the interactive assistant: there a person is typing and the tools
45
+ * are their own. An autonomous run has no watching user, so borrowing someone's token is never
46
+ * the intent — `resolveToolset` passes `SYSTEM_TOOL_POLICY` and this field is dead on that
47
+ * path. Putting it on the agent would have made it a setting that cannot do anything.
56
48
  */
57
49
  personalTools?: "off" | "allow";
58
50
  }
@@ -49,16 +49,25 @@ export interface Assignment {
49
49
  organizationId: string;
50
50
  /** The agent doing this: the `userId` of a user with `type: "agent"`. */
51
51
  agentId: string;
52
- /** The definition this assignment came from (a playbook with a procedure). */
53
- jobId: string;
54
52
  /**
55
- * The version it started on purely for the audit.
53
+ * The playbook this assignment came from, when a trigger put it down. **Absent when someone
54
+ * typed it**, which is the common case in Assist.
55
+ *
56
+ * It used to be required, so a typed request had to invent a definition to be allowed to exist:
57
+ * one org-wide "Assist" playbook with an empty procedure, created on first use and rewritten on
58
+ * every start and every delegation. A shared mutable row behind a foreign key — and a turn read
59
+ * its agent off it, so one delegation could hand every other running assignment to a different
60
+ * agent. The row is gone; what a typed assignment needs, it now carries itself.
61
+ */
62
+ jobId?: string;
63
+ /**
64
+ * The version it started on — purely for the audit. Absent with {@link Assignment.jobId}.
56
65
  *
57
66
  * Every **turn** pins its own procedure (`run.decider`), so improved prose arrives on the next
58
67
  * turn without a running turn switching instructions mid-flight. Pinning forever would mean a
59
68
  * correction never arrives; not pinning would mean a turn changes halfway.
60
69
  */
61
- jobVersion: number;
70
+ jobVersion?: number;
62
71
  /** What this assignment is about. Absent = work without a resource (a rollup, a chore). */
63
72
  subjectKind?: string;
64
73
  subjectId?: string;
@@ -79,6 +88,29 @@ export interface Assignment {
79
88
  * triggered one may not. The list needs to tell them apart, so it is stored.
80
89
  */
81
90
  origin?: "trigger" | "user";
91
+ /**
92
+ * Allowed tools for this assignment, narrowing-only against the agent's brain — the same rule
93
+ * `Procedure.tools` carries. Absent = whatever the brain allows.
94
+ *
95
+ * `POST assignment/start` has always accepted and validated this, and then dropped it on the
96
+ * floor: there was nowhere to put it, because the bounds lived on the shared playbook that
97
+ * every assignment in the organisation had to share.
98
+ */
99
+ tools?: string[];
100
+ /**
101
+ * The assignment that delegated this one, when an agent asked a colleague-agent a sub-question.
102
+ * The return address: `applyTurn` reports back here the moment this assignment goes terminal.
103
+ */
104
+ delegatedFrom?: string;
105
+ /**
106
+ * The agent ids from the root of the delegation chain down to and including this assignment's
107
+ * own agent.
108
+ *
109
+ * **One field, two gates.** Its length is the depth and `includes()` is the cycle check — see
110
+ * {@link delegationRefusal}. A separate `depth` counter beside a chain would be a second source
111
+ * of truth for the same fact.
112
+ */
113
+ delegationChain?: string[];
82
114
  status: AssignmentStatus;
83
115
  /**
84
116
  * What the **assignment** is waiting on between two turns.
@@ -223,3 +255,26 @@ export declare function decideAssignmentState(control: AssignmentControl | undef
223
255
  turns: number;
224
256
  reason?: string;
225
257
  };
258
+ /**
259
+ * How many **hops** one delegation chain may make. A → B is one hop, A → B → C is two.
260
+ *
261
+ * Two, not more: "ask a colleague who asks their colleague" is the last chain that still has an
262
+ * owner anyone can name. Every extra level multiplies the turn ceiling by twelve for work nobody
263
+ * asked for.
264
+ */
265
+ export declare const MAX_DELEGATION_DEPTH = 2;
266
+ /**
267
+ * Why this agent may **not** delegate to that one — or `null` when it may.
268
+ *
269
+ * Pure, so the rule is testable without a Bridge, and the same shape as `decideAssignmentState`
270
+ * beside it.
271
+ *
272
+ * **A reason, not a boolean.** The string goes back to the model as the tool's error, so it can
273
+ * pick another route instead of retrying the same refused call. `toolsOutsideBrain` in the ai app
274
+ * does the same for the same reason.
275
+ *
276
+ * @param chain The calling assignment's {@link Assignment.delegationChain}; absent = root.
277
+ * @param allowed The calling agent's `delegateTo`. `undefined` and `[]` both mean **none**:
278
+ * delegation is opt-in, unlike `enabledTools` where absent means "no filter".
279
+ */
280
+ export declare function delegationRefusal(chain: string[] | undefined, targetAgentId: string, callerAgentId: string, allowed: string[] | undefined): string | null;
@@ -355,6 +355,17 @@ export type WaitingOn = {
355
355
  } | {
356
356
  on: "task";
357
357
  taskId: string;
358
+ }
359
+ /**
360
+ * Another agent is working on a sub-question; its terminal status is the signal.
361
+ *
362
+ * Deliberately quiet: `wakesAssignment` rule 5 only lets a plain internal note through while
363
+ * waiting on a `"reply"`, so an assignment parked on an agent is woken by that agent's answer
364
+ * or by an explicit mention — not by every colleague passing by.
365
+ */
366
+ | {
367
+ on: "agent";
368
+ assignmentId: string;
358
369
  };
359
370
  /** Terminal states: a run does not leave these on its own. */
360
371
  export declare const TERMINAL_RUN_STATUSES: readonly ["done", "escalated", "failed", "stopped"];
@@ -428,8 +439,13 @@ export type StartedBy =
428
439
  export interface PlaybookRun {
429
440
  id: string;
430
441
  organizationId: string;
431
- playbookId: string;
432
- playbookVersion: number;
442
+ /**
443
+ * The definition this run is of. **Absent on a turn of a typed assignment**: that work has no
444
+ * playbook, and inventing one so this column could stay required is exactly what the shared
445
+ * "Assist" row was.
446
+ */
447
+ playbookId?: string;
448
+ playbookVersion?: number;
433
449
  /**
434
450
  * The assignment this is a **turn** of, if any. Absent = a standalone execution (an
435
451
  * automation that fires and is done). Present = one move in work that outlives this run,
@@ -9,6 +9,10 @@
9
9
  * audit loses it.
10
10
  *
11
11
  * Absent means `"user"`: existing rows are humans, and the mirror is not backfilled.
12
+ *
13
+ * This flag is all the user record knows about being an agent. The brain and the mandate live on
14
+ * `Agent` in the ai app, keyed by this user's id — the ai configuration used to sit here as
15
+ * `profileId`, which made user management carry agent behaviour and gave one fact two homes.
12
16
  */
13
17
  export type UserType = "user" | "agent";
14
18
  /**
@@ -27,15 +31,6 @@ export interface UserProfile {
27
31
  displayName?: string;
28
32
  avatarUrl?: string;
29
33
  type?: UserType;
30
- /**
31
- * An agent's brain, when this is an agent — see {@link User.profileId}.
32
- *
33
- * Belongs on this compact record and not only on the full one: "is this an agent" is already
34
- * here (`type`), and "with which brain" is the immediate follow-up of every caller handing it
35
- * work. Without this field the ai app had to use the admin route — the whole record plus an RPC
36
- * to `system.rbac` — to learn one id.
37
- */
38
- profileId?: string;
39
34
  }
40
35
  /** Is this user an agent? An absent type = human. */
41
36
  export declare function isAgentUser(user: {
@@ -53,8 +48,6 @@ export interface User {
53
48
  * not own it. Everything that needs to know "is this a human" reads this field.
54
49
  */
55
50
  type?: UserType;
56
- /** An agent's brain: the `AIProfile` it works with. Only set when `type === "agent"`. */
57
- profileId?: string;
58
51
  /** Mirrored from system.users. */
59
52
  firstName?: string;
60
53
  lastName?: string;