@opencxh/domain 1.254.0 → 1.256.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,55 @@
1
+ /**
2
+ * An **agent**: a digital colleague. A profile plus an identity plus a mandate.
3
+ *
4
+ * **Why this is its own record, next to the user row and the profile.** Those two already
5
+ * existed and pointed at each other: `User.profileId` said which brain, `Playbook.agentId` said
6
+ * which agent, and nothing related them — `agentId: Alice` running on Bob's brain was a valid
7
+ * configuration, and did happen (see `agent/brain.ts` in the ai app). One record ends that.
8
+ *
9
+ * **`id` is the user id.** Not a `userId` column beside a generated key: an agent *is* that user
10
+ * (that is what gives it a real identity — fail-closed authorization, its own name on the
11
+ * timeline, team membership, its own budget), so a second identifier would only be a second thing
12
+ * that can disagree. Every `agentId` in this codebase is therefore also this row's id.
13
+ *
14
+ * **What is deliberately not here.** The name lives on the user row, because that is what every
15
+ * timeline, mention list and assignee picker already reads; duplicating it would let two places
16
+ * disagree about what an agent is called.
17
+ */
18
+ export interface Agent {
19
+ /** The agent's user id. See above: there is no second identifier. */
20
+ id: string;
21
+ organizationId: string;
22
+ /** The brain: the `AIProfile` this agent thinks with. */
23
+ profileId: string;
24
+ /**
25
+ * What this agent is for, in one line.
26
+ *
27
+ * **On the agent, not on the profile.** Another agent reads this line to decide whether to call
28
+ * this one in ({@link Agent.delegateTo}), so it has to describe *this colleague* — and two
29
+ * agents sharing the Support profile are two different colleagues. It used to be
30
+ * `AIProfile.description`, which gave them the same blurb.
31
+ */
32
+ description?: string;
33
+ /**
34
+ * Per-tool confirmation policy: which of this agent's tools count as a write.
35
+ *
36
+ * Moved off the profile because it is a mandate, not expertise: the same Support brain may be
37
+ * trusted to send in one agent's hands and only to propose in another's. Under autonomy
38
+ * "suggest" write tools are held for approval; reads run freely. A tool absent from this list
39
+ * defaults to "write" (safe).
40
+ */
41
+ toolPolicy?: {
42
+ name: string;
43
+ policy: "read" | "write";
44
+ }[];
45
+ /**
46
+ * Which colleagues this agent may call in for a sub-question (agent ids).
47
+ *
48
+ * **Opt-in**: absent and `[]` both mean none. Who you may wake is a permission, and a default
49
+ * of "everyone" would make every new agent reachable by every other one the moment it exists.
50
+ *
51
+ * Moved off the profile for the reason the whole record exists: this is a relation *between
52
+ * agents*, and storing it on a shared brain gave two agents one delegate list.
53
+ */
54
+ delegateTo?: string[];
55
+ }
@@ -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
  }
@@ -79,6 +79,20 @@ export interface Assignment {
79
79
  * triggered one may not. The list needs to tell them apart, so it is stored.
80
80
  */
81
81
  origin?: "trigger" | "user";
82
+ /**
83
+ * The assignment that delegated this one, when an agent asked a colleague-agent a sub-question.
84
+ * The return address: `applyTurn` reports back here the moment this assignment goes terminal.
85
+ */
86
+ delegatedFrom?: string;
87
+ /**
88
+ * The agent ids from the root of the delegation chain down to and including this assignment's
89
+ * own agent.
90
+ *
91
+ * **One field, two gates.** Its length is the depth and `includes()` is the cycle check — see
92
+ * {@link delegationRefusal}. A separate `depth` counter beside a chain would be a second source
93
+ * of truth for the same fact.
94
+ */
95
+ delegationChain?: string[];
82
96
  status: AssignmentStatus;
83
97
  /**
84
98
  * What the **assignment** is waiting on between two turns.
@@ -223,3 +237,26 @@ export declare function decideAssignmentState(control: AssignmentControl | undef
223
237
  turns: number;
224
238
  reason?: string;
225
239
  };
240
+ /**
241
+ * How many **hops** one delegation chain may make. A → B is one hop, A → B → C is two.
242
+ *
243
+ * Two, not more: "ask a colleague who asks their colleague" is the last chain that still has an
244
+ * owner anyone can name. Every extra level multiplies the turn ceiling by twelve for work nobody
245
+ * asked for.
246
+ */
247
+ export declare const MAX_DELEGATION_DEPTH = 2;
248
+ /**
249
+ * Why this agent may **not** delegate to that one — or `null` when it may.
250
+ *
251
+ * Pure, so the rule is testable without a Bridge, and the same shape as `decideAssignmentState`
252
+ * beside it.
253
+ *
254
+ * **A reason, not a boolean.** The string goes back to the model as the tool's error, so it can
255
+ * pick another route instead of retrying the same refused call. `toolsOutsideBrain` in the ai app
256
+ * does the same for the same reason.
257
+ *
258
+ * @param chain The calling assignment's {@link Assignment.delegationChain}; absent = root.
259
+ * @param allowed The calling agent's `delegateTo`. `undefined` and `[]` both mean **none**:
260
+ * delegation is opt-in, unlike `enabledTools` where absent means "no filter".
261
+ */
262
+ export declare function delegationRefusal(chain: string[] | undefined, targetAgentId: string, callerAgentId: string, allowed: string[] | undefined): string | null;
@@ -45,12 +45,15 @@ export declare const TRIGGER_VARS: readonly [{
45
45
  }, {
46
46
  readonly name: "fromStatus";
47
47
  readonly type: "string";
48
+ readonly onlyFor: readonly ["INTERACTION_STATUS_CHANGED"];
48
49
  }, {
49
50
  readonly name: "toStatus";
50
51
  readonly type: "string";
52
+ readonly onlyFor: readonly ["INTERACTION_STATUS_CHANGED"];
51
53
  }, {
52
54
  readonly name: "toCategory";
53
55
  readonly type: "string";
56
+ readonly onlyFor: readonly ["WORK_ITEM_STATUS_CHANGED"];
54
57
  }, {
55
58
  readonly name: "contactId";
56
59
  readonly type: "string";
@@ -70,6 +73,14 @@ export declare const TRIGGER_VARS: readonly [{
70
73
  readonly name: "assigneeUserId";
71
74
  readonly type: "string";
72
75
  }];
76
+ /**
77
+ * Does this trigger fill that variable? A trigger listing no activity types matches
78
+ * everything, which is also what the engine does with it.
79
+ */
80
+ export declare function triggerFillsVar(variable: {
81
+ name: string;
82
+ onlyFor?: readonly string[];
83
+ }, activityTypes: readonly string[] | undefined): boolean;
73
84
  /**
74
85
  * What a classify step **yields** per mode — the contract `runClassify` honours.
75
86
  *
@@ -0,0 +1 @@
1
+ export {};
@@ -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"];
@@ -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;