@ziggs-ai/api-client 0.25.0 → 0.26.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.
@@ -51,20 +51,22 @@ export const openConversationCapability = {
51
51
  names: { sdk: 'chat_open', mcp: 'ziggs_chat_open' },
52
52
  title: 'Start or reuse a conversation',
53
53
  descriptions: {
54
- sdk: 'Open or reuse a chat with a user or agent participant. Reuse is for this acting agent and that participant; it does not find another assistant’s or the human owner’s conversation, so it is how you find the conversation you already have with someone — pass newChat only when this really is a separate subject. This is also how you reach the HUMAN who hired you when the work needs an answer only they have: pass their user id (context_snapshot lists it under users; on your agreement they are the payer/creator party), then chat_send in the chat it returns with no receiverId. Opening a room is issuing the other side access to it, so it needs something that authorizes that: they publish a listing, you share an org, you hold a live agreement with them (a link counts, so link_propose is how you get one with a peer in another org), or they claimed your invite. Someone else\'s agent is not opened directly, inside your org or outside it: open the chat with the person it answers for (their user id); which of their agents answers is theirs to decide, and an agent of theirs already in a room with you can be written to there. With none of those the call is refused and the refusal names the levers. When the person you cannot reach is somebody YOUR OWN PERSON already knows — their teammate, their partner, their customer — none of those levers is the right one: ask the person you work for to open a room with them and add you, in plain words in your conversation with them, then stop until you are in it. Claiming a listing or sending an invite is for a stranger you are doing business with, and using it on somebody your person could introduce you to in two clicks costs them a negotiation instead. To list chats you can already read, use grant_list scopeKind=chat.',
55
- mcp: 'Open or reuse a chat with a user or agent participant. Reuse is for this acting agent and that participant; it does not find another assistant’s or the human owner’s conversation; pass newChat only when this really is a separate subject. Opening a room is issuing the other side access to it, so it needs something that authorizes that: they publish a listing, you share an org, you hold a live agreement with them (a link counts, so ziggs_link_propose is how you get one with a peer in another org), or they claimed your invite. Someone else\'s agent is not opened directly, inside your org or outside it: open the chat with the person it answers for (their user id); which of their agents answers is theirs to decide, and an agent of theirs already in a room with you can be written to there. With none of those the call is refused and names the levers. If you already have the intended chatId, use ziggs_open and ziggs_access_request for missing access; opening another room does not recover its history. When the person you cannot reach is somebody your own person already knows — their teammate, their partner, their customer — ask that person to open a room with them and add you, rather than reaching for a listing or an invite: those are for strangers you are doing business with.',
54
+ sdk: 'Open a conversation or explicitly invite people to a room. participantId opens/reuses the acting agent’s pair conversation with one person or agent; newChat opens a separate subject. participantIds creates a separate group with the acting caller and those people; include a stable idempotencyKey for retries. chatId plus participantIds invites people into that exact room, requiring room access and admission authority. Every person needs a valid contact basis: shared organization membership, a link, or an applicable agreement. Each participant keeps their own agent and work relationships; opening a group selects no service agreement. Group creation never expands an existing private DM. Invited teammates read from admission onward. participantIds takes people only: another agent joins a room when a person adds it in the web app, where its approval rules apply, and your own missing seat is asked for with access_request. Reach another person through their user id, not an unpublished agent of theirs. If you cannot reach someone your person knows, ask your person to introduce you in a room. Opening does not send a message; chat_send must name its intended receiverId to guarantee a target. To list accessible rooms, use grant_list scopeKind=chat.',
55
+ mcp: 'Open a conversation or explicitly invite people to a room. participantId opens/reuses the acting agent’s pair conversation with one person or agent; newChat opens a separate subject. participantIds creates a separate group with the acting caller and those people; include a stable idempotencyKey for retries. chatId plus participantIds invites people into that exact room, requiring room access and admission authority. Every person needs a valid contact basis: shared organization membership, a link, or an applicable agreement. Each participant keeps their own agent and work relationships; opening a group selects no service agreement. Group creation never expands an existing private DM. Invited teammates read from admission onward. participantIds takes people only: another agent joins a room when a person adds it in the web app, where its approval rules apply, and your own missing seat is asked for with ziggs_access_request. Reach another person through their user id, not an unpublished agent of theirs. For missing access to a known room, use ziggs_access_request; opening another room does not recover its history. Opening does not send a message; ziggs_chat_send must name its intended receiverId to guarantee a target.',
56
56
  },
57
57
  annotation: 'write',
58
58
  params: {
59
59
  participantId: {
60
60
  type: 'string',
61
- required: true,
62
61
  description: 'Known user or agent id from search, a directory, a chat, or an agreement. For a teammate or a linked person, their user id, not an agent of theirs. No existing chat is required; do not guess ids.',
63
62
  },
63
+ participantIds: { type: 'array', items: { type: 'string' }, description: '1–20 known user ids. Creates a separate group, or invites these people to chatId. Use instead of participantId. Each invitation is authorized separately.' },
64
+ chatId: { type: 'string', description: 'Existing room to invite participantIds into. Never creates another room. Omit creation options and agreementId.' },
65
+ idempotencyKey: { type: 'string', description: 'Required for group creation: a unique key for this conversation, reused unchanged on retries. A different participant set needs a new key.' },
64
66
  createIfMissing: { type: 'boolean', description: 'Set false to reuse only an existing room for this acting agent and participant. A missing room is refused without creating a replacement.' },
65
67
  newChat: {
66
68
  type: 'boolean',
67
- description: 'Open a separate chat even though one is already open with this participant. ' +
69
+ description: 'Open a separate chat even though one is already open with the named participants. ' +
68
70
  'For when the conversation is genuinely its own subject and would confuse an ' +
69
71
  'existing thread. The separate chat does not become the main one, so a later ' +
70
72
  'call without this still returns the original. Leave unset to continue where ' +
@@ -78,15 +80,31 @@ export const openConversationCapability = {
78
80
  },
79
81
  needsAgentId: true,
80
82
  handler: async (args, env) => {
81
- if (!args['participantId'])
82
- throw new Error('participantId is required');
83
- const { chatId, reused } = await openConversation(args['participantId'], fullCreds(env), {
84
- newChat: args['newChat'] === true,
83
+ if (!args.participantId && !Array.isArray(args.participantIds)) {
84
+ throw new Error('participantId is required, or use participantIds to invite people');
85
+ }
86
+ if (args.participantIds !== undefined) {
87
+ if (args.participantId !== undefined || !Array.isArray(args.participantIds) ||
88
+ args.participantIds.length < 1 || args.participantIds.length > 20 ||
89
+ args.participantIds.some(id => typeof id !== 'string' || !id.trim())) {
90
+ throw new Error('Use participantIds containing 1–20 people, without participantId');
91
+ }
92
+ if (!args.chatId && (typeof args.idempotencyKey !== 'string' || !args.idempotencyKey.trim())) {
93
+ throw new Error('Group creation requires an idempotencyKey; reuse it when retrying');
94
+ }
95
+ }
96
+ else if (args.chatId || args.idempotencyKey) {
97
+ throw new Error('chatId and idempotencyKey require participantIds');
98
+ }
99
+ const { chatId, reused } = await openConversation({
100
+ ...(typeof args.participantId === 'string' ? { participantId: args.participantId } : {}),
101
+ ...(Array.isArray(args.participantIds) ? { participantIds: args.participantIds } : {}),
102
+ ...(typeof args.chatId === 'string' ? { chatId: args.chatId } : {}),
103
+ ...(typeof args.idempotencyKey === 'string' ? { idempotencyKey: args.idempotencyKey } : {}),
104
+ ...(args.newChat === true ? { newChat: true } : {}),
85
105
  ...(typeof args.createIfMissing === 'boolean' ? { createIfMissing: args.createIfMissing } : {}),
86
- ...(typeof args['agreementId'] === 'string' && args['agreementId']
87
- ? { agreementId: args['agreementId'] }
88
- : {}),
89
- });
106
+ ...(typeof args.agreementId === 'string' && args.agreementId ? { agreementId: args.agreementId } : {}),
107
+ }, fullCreds(env));
90
108
  const lister = env.surface === 'mcp' ? 'ziggs_grant_list' : 'grant_list';
91
109
  const grantNote = `Use ${lister} scopeKind=chat to inspect your room grants. Opening a conversation does not send a message or confirm that anyone has read it.`;
92
110
  // word the note from the outcome instead of covering both cases.
@@ -94,11 +112,13 @@ export const openConversationCapability = {
94
112
  // withheld it — worse than silence, because the agent cannot even tell
95
113
  // there is something to look up. A backend too old to report it keeps the
96
114
  // old both-cases wording, and `reused` is simply absent from the result.
97
- const outcomeNote = reused === true
98
- ? 'Reused the conversation you already had with this participant, so it may already hold history — read it before you speak.'
99
- : reused === false
100
- ? 'Created a new conversation with this participant, so there is no history to catch up on.'
101
- : 'Conversation is open (or reused).';
115
+ const outcomeNote = args.chatId
116
+ ? 'The named people can participate in this room. Their history access remains bounded by their grants.'
117
+ : reused === true
118
+ ? 'Reused the conversation you already had with the named participants, so it may already hold history — read it before you speak.'
119
+ : reused === false
120
+ ? 'Created a new conversation with the named participants, so there is no history to catch up on.'
121
+ : 'Conversation is open (or reused).';
102
122
  return {
103
123
  chatId,
104
124
  actingAgentId: fullCreds(env).agentId,
@@ -216,6 +216,11 @@ export function nextCall(env, capabilityKey, args, why, opts) {
216
216
  for (const [key, value] of Object.entries(args ?? {})) {
217
217
  if (!isFilled(value))
218
218
  continue;
219
+ if (key === 'participantIds' && Array.isArray(value) && value.some(isPersonaFace)) {
220
+ missing.push(key);
221
+ strippedFaces.push(key);
222
+ continue;
223
+ }
219
224
  if (ADDRESS_FIELDS.includes(key) &&
220
225
  isPersonaFace(value)) {
221
226
  missing.push(key);
@@ -225,11 +230,20 @@ export function nextCall(env, capabilityKey, args, why, opts) {
225
230
  filled[key] = value;
226
231
  }
227
232
  for (const name of requiredArgNames(capabilityKey, opts?.definition)) {
233
+ if (capabilityKey === 'chat_open' && name === 'participantId' && Array.isArray(filled.participantIds) && filled.participantIds.length > 0)
234
+ continue;
228
235
  if (!isFilled(filled[name])) {
229
236
  if (!missing.includes(name))
230
237
  missing.push(name);
231
238
  }
232
239
  }
240
+ if (capabilityKey === 'chat_open' && (!opts?.definition || 'participantIds' in opts.definition.params)) {
241
+ if (!filled.participantId && !(Array.isArray(filled.participantIds) && filled.participantIds.length > 0) && !missing.includes('participantId')) {
242
+ missing.push('participantId');
243
+ }
244
+ if (Array.isArray(filled.participantIds) && !filled.chatId && !filled.idempotencyKey)
245
+ missing.push('idempotencyKey');
246
+ }
233
247
  const hold = opts?.hold;
234
248
  const ready = missing.length === 0 && !hold;
235
249
  const outcome = opts?.outcome ??
@@ -13,7 +13,16 @@ import { type Creds } from '../types.js';
13
13
  * with without a second call.
14
14
  */
15
15
  export type ChatSummary = ChatReadDto;
16
- export declare function openConversation(participantId: string, creds: Creds, { newChat, agreementId, createIfMissing }?: {
16
+ export interface OpenConversationInput {
17
+ participantId?: string;
18
+ participantIds?: string[];
19
+ chatId?: string;
20
+ idempotencyKey?: string;
21
+ newChat?: boolean;
22
+ agreementId?: string;
23
+ createIfMissing?: boolean;
24
+ }
25
+ export declare function openConversation(participant: string | OpenConversationInput, creds: Creds, { newChat, agreementId, createIfMissing }?: {
17
26
  newChat?: boolean;
18
27
  agreementId?: string;
19
28
  createIfMissing?: boolean;
@@ -34,9 +43,10 @@ export interface SendChatMessageInput {
34
43
  */
35
44
  to?: string;
36
45
  /**
37
- * Recipient id. Optional: when omitted, the backend infers the
38
- * receiver when Jev is confident. Otherwise it stores room context without
39
- * a specific wake. Explicit addressing is required for a guaranteed target.
46
+ * Recipient id. Optional: when omitted, the one other participant in the
47
+ * room is the receiver; with several, the backend infers one when Jev is
48
+ * confident. Otherwise it stores room context without a specific wake.
49
+ * Explicit addressing is required for a guaranteed target.
40
50
  */
41
51
  receiverId?: string;
42
52
  text: string;
@@ -118,7 +128,7 @@ export declare class ChatClient {
118
128
  * @param agentId Required for fleet keys (sent as X-Agent-Id). Omit for agent-scoped keys.
119
129
  */
120
130
  constructor(operatorKey: string, agentId?: string);
121
- open(participantId: string, opts?: {
131
+ open(participant: string | OpenConversationInput, opts?: {
122
132
  newChat?: boolean;
123
133
  agreementId?: string;
124
134
  createIfMissing?: boolean;
@@ -10,21 +10,20 @@ function assertCreds(creds, op) {
10
10
  if (!creds?.agentId)
11
11
  throw new Error(`agentId is required for ${op}`);
12
12
  }
13
- export async function openConversation(participantId, creds, { newChat = false, agreementId, createIfMissing } = {}) {
14
- if (!participantId)
15
- throw new Error('participantId is required for openConversation');
13
+ export async function openConversation(participant, creds, { newChat = false, agreementId, createIfMissing } = {}) {
14
+ const input = typeof participant === 'string'
15
+ ? { participantId: participant, ...(newChat ? { newChat } : {}), ...(agreementId ? { agreementId } : {}), ...(createIfMissing !== undefined ? { createIfMissing } : {}) }
16
+ : participant;
17
+ if (!input || (!input.participantId && !input.participantIds?.length)) {
18
+ throw new Error('participantId is required, or use participantIds to invite people');
19
+ }
16
20
  assertCreds(creds, 'open conversation');
17
21
  const res = await fetch(`${getBackendUrl()}/chats`, {
18
22
  method: 'POST',
19
23
  headers: buildHeaders(creds),
20
24
  // Only sent when asked for: an older backend ignores the field, so a
21
25
  // caller that never wants a separate room behaves identically either way.
22
- body: JSON.stringify({
23
- participantId,
24
- ...(newChat ? { newChat: true } : {}),
25
- ...(createIfMissing !== undefined ? { createIfMissing } : {}),
26
- ...(agreementId ? { agreementId } : {}),
27
- }),
26
+ body: JSON.stringify(input),
28
27
  });
29
28
  if (!res.ok) {
30
29
  const body = await res.text().catch(() => '');
@@ -166,7 +165,7 @@ export class ChatClient {
166
165
  // standalone functions still assert it per call.
167
166
  this.creds = { operatorKey, agentId };
168
167
  }
169
- open(participantId, opts) { return openConversation(participantId, this.creds, opts); }
168
+ open(participant, opts) { return openConversation(participant, this.creds, opts); }
170
169
  addMember(input) { return addChatMember(input, this.creds); }
171
170
  sendMessage(input) { return sendChatMessage(input, this.creds); }
172
171
  listMine() { return listMyChats(this.creds); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/api-client",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "HTTP and WebSocket client for the Ziggs backend API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",