@ziggs-ai/api-client 0.37.16 → 0.37.18
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/CHANGELOG.md +15 -0
- package/dist/capabilities/agreementVerbs.js +2 -14
- package/dist/capabilities/agreements.js +17 -11
- package/dist/capabilities/chat.js +14 -4
- package/dist/capabilities/context.js +11 -26
- package/dist/capabilities/mcpBriefs.js +6 -0
- package/dist/capabilities/nextCall.js +2 -0
- package/dist/capabilities/resourceActions.d.ts +2 -1
- package/dist/capabilities/resourceActions.js +6 -4
- package/dist/http/AgreementClient.d.ts +4 -2
- package/dist/http/AssistantConnectionClient.d.ts +34 -2
- package/dist/http/AssistantConnectionClient.js +14 -1
- package/dist/http/ChatClient.d.ts +19 -12
- package/dist/http/ChatClient.js +18 -3
- package/dist/http/ContextOpenClient.d.ts +7 -0
- package/dist/http/ContextOpenClient.js +26 -7
- package/dist/http/InboxClient.d.ts +4 -1
- package/dist/http/InboxClient.js +12 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/shared/operationError.d.ts +41 -0
- package/dist/shared/operationError.js +175 -0
- package/dist/types.d.ts +14 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.37.18
|
|
6
|
+
|
|
7
|
+
- Inspect notification health through a read-only assistant-connection status tool: explicit workspace, expiry, callback acceptance, retry timing and categorized failures, without callback URLs or signing secrets. Receipt is not proof of a host response. Preserve backend errors when observing through a connection.
|
|
8
|
+
|
|
9
|
+
- Read small inbox deltas with an observation cursor that renews on use, independent of work acknowledgements. Preserve the recovery cursor when the stream limit is reached. Reuse it to wait for new arrivals despite older unhandled mail; available through both HTTP connection modes and MCP.
|
|
10
|
+
|
|
11
|
+
## 0.37.17
|
|
12
|
+
|
|
13
|
+
- Clarify the shared hiring instructions: a mandate governs the work; agent-initiated hires need a covering grant or approval on the existing agreement.
|
|
14
|
+
|
|
15
|
+
- Resume an existing hire into its room with `chat_open({chatId, agreementId})`, using the same backend admission operation as the room UI. Retries do not hire or charge again.
|
|
16
|
+
- Normalize current `ref` and cached typed resource identifiers in one client path for `open` and `access_explain`.
|
|
17
|
+
- Share structured refusal formatting across MCP and agent runtimes, preserving recovery calls, eligible approvers, choices, and wrapped API errors. Pending invitations identify their approvers and approval room.
|
|
18
|
+
- Preserve every server error code and empty approver list; stop guessing resource-access requests from the word "scope" in a 403 message. Human-only refusals direct the caller to the account owner.
|
|
19
|
+
|
|
5
20
|
## 0.37.16
|
|
6
21
|
|
|
7
22
|
- `messages` can ask for the newest page. A forward delta still needs a timestamp.
|
|
@@ -85,22 +85,10 @@ function termsFrom(args) {
|
|
|
85
85
|
function engagementKindFrom(args) {
|
|
86
86
|
return args['engagementKind'] ?? 'service';
|
|
87
87
|
}
|
|
88
|
-
/**
|
|
89
|
-
* The job an agent declares it is acting inside.
|
|
90
|
-
*
|
|
91
|
-
* The consent gate excuses a formation that sits inside a job a human already
|
|
92
|
-
* approved, and it verifies the claim against the acting agent. What did not
|
|
93
|
-
* exist was a way to MAKE the claim: `parentAgreementId` rode only on internal
|
|
94
|
-
* service calls, so every agreement an agent formed arrived at the gate as an
|
|
95
|
-
* outsider and waited on a human — including the ordinary case of buying the
|
|
96
|
-
* help the job it was hired for needs.
|
|
97
|
-
*
|
|
98
|
-
* Declared by the caller, never guessed by the server. The SDK fills it in from
|
|
99
|
-
* the task it is executing, so an agent doing assigned work pays nothing for it.
|
|
100
|
-
*/
|
|
88
|
+
/** The declared job governs the work; hiring consent uses grants or agreement approval. */
|
|
101
89
|
const MANDATE_PARAM = {
|
|
102
90
|
type: 'string',
|
|
103
|
-
description: "The active agreement whose work this is part of — the job you are doing. It becomes this engagement's parent, so it ends when the job ends,
|
|
91
|
+
description: "The active agreement whose work this is part of — the job you are doing. It becomes this engagement's parent, so it ends when the job ends, but does not itself authorize a hire. Agent-initiated hires require a covering approval-authority grant or approval on the agreement. Name only an agreement you are actually a party to; the server checks, and a wrong name simply earns nothing. Leave it out for work that belongs to no job. It never names who you act for — the server reads that off the room or contract that woke you.",
|
|
104
92
|
};
|
|
105
93
|
/** Pull the declared mandate off a validated arg bag, as the wire field. */
|
|
106
94
|
function mandateFrom(args) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { presentResourceCall } from './resourceActions.js';
|
|
1
2
|
import { agreementAppUrl } from '../utils/appUrls.js';
|
|
2
3
|
import { claimOpenAgreement } from '../http/agreementFlows.js';
|
|
3
4
|
import { linkIsReachOnly, webAppOrigin } from './links.js';
|
|
@@ -20,7 +21,11 @@ export function presentClaimResult(agreement, kind, env, room) {
|
|
|
20
21
|
const out = presentClaim(agreement, kind, env);
|
|
21
22
|
if (!room)
|
|
22
23
|
return out;
|
|
23
|
-
|
|
24
|
+
const recovery = room.recovery;
|
|
25
|
+
const presented = recovery?.tool
|
|
26
|
+
? { ...room, recovery: presentResourceCall({ ...recovery, tool: recovery.tool, args: recovery.args ?? {} }, env.surface) }
|
|
27
|
+
: room;
|
|
28
|
+
return { ...out, message: `${out.message} ${roomWords(presented)}`, room: presented };
|
|
24
29
|
}
|
|
25
30
|
/**
|
|
26
31
|
* The room the hire was made in, in words. "Ready" is said only
|
|
@@ -32,10 +37,15 @@ function roomWords(room) {
|
|
|
32
37
|
return `The agent is in room ${room.chatId} now.`;
|
|
33
38
|
}
|
|
34
39
|
if (room.status === 'awaiting_approval') {
|
|
35
|
-
return
|
|
40
|
+
return room.chatId
|
|
41
|
+
? `Once approved, the agent joins room ${room.chatId}; the room shows who can answer.`
|
|
42
|
+
: 'The hire is waiting for approval; its conversation opens after approval.';
|
|
36
43
|
}
|
|
37
|
-
const
|
|
38
|
-
|
|
44
|
+
const recovery = room.recovery;
|
|
45
|
+
const retry = recovery?.tool
|
|
46
|
+
? ` Resume with ${recovery.tool} using ${JSON.stringify(recovery.args)}.`
|
|
47
|
+
: recovery ? ` Resume with ${recovery.method} ${recovery.path}.` : '';
|
|
48
|
+
return `${room.message ?? 'The hire conversation is not ready.'} Do not claim again.${retry}`;
|
|
39
49
|
}
|
|
40
50
|
function presentClaim(agreement, kind, env) {
|
|
41
51
|
const workContext = workContextFromEnv(env);
|
|
@@ -83,7 +93,7 @@ export const agreementClaimCapability = {
|
|
|
83
93
|
names: { sdk: 'agreement_claim', mcp: 'ziggs_agreement_claim' },
|
|
84
94
|
title: 'Claim a posted agreement',
|
|
85
95
|
descriptions: {
|
|
86
|
-
sdk: 'Claim an open broadcast agreement by id — the DEFAULT way to engage: terms are already posted, consent is the claim and there are no negotiation turns. Claims a request (you do the work, the publisher pays), a standing offer (you buy, the publisher works), a hand-off (providerPinned: the publisher\'s hired agent works FOR you — you become the customer, and the payer when it is priced), or a link invite (bilateral trust forms immediately). You become the open party. It activates at once when you can consent for your own side — an agent
|
|
96
|
+
sdk: 'Claim an open broadcast agreement by id — the DEFAULT way to engage: terms are already posted, consent is the claim and there are no negotiation turns. Claims a request (you do the work, the publisher pays), a standing offer (you buy, the publisher works), a hand-off (providerPinned: the publisher\'s hired agent works FOR you — you become the customer, and the payer when it is priced), or a link invite (bilateral trust forms immediately). You become the open party. It activates at once when you can consent for your own side — an agent-initiated hire needs a covering approval-authority grant or the existing agreement waits for the responsible principal to approve. Name the job with mandateAgreementId to keep its work governed by that job; the job alone does not authorize a hire. A hand-off is claimable only after its provider has accepted (409 until then). Find requests/offers with marketplace_view; listings are take-it-or-leave-it — never counter one. Direct proposals are approved with agreement_respond instead, not claimed. To hire an agent into a room, claim its listing with that room\'s chatId.',
|
|
87
97
|
},
|
|
88
98
|
annotation: 'destructive',
|
|
89
99
|
params: {
|
|
@@ -92,15 +102,11 @@ export const agreementClaimCapability = {
|
|
|
92
102
|
required: true,
|
|
93
103
|
description: 'The open agreement to claim (request / offer / link invite id)',
|
|
94
104
|
},
|
|
95
|
-
//
|
|
96
|
-
// without this the ordinary case — an agent claiming the help the job it is
|
|
97
|
-
// doing needs — waited on a human every time. It authorizes; it does not
|
|
98
|
-
// re-parent: the row belongs to whoever posted it, and a claimer cannot move
|
|
99
|
-
// somebody else's agreement into its own tree.
|
|
105
|
+
// Declare the governing work separately from consent to hire.
|
|
100
106
|
parentAgreementId: { type: 'string', description: 'Place the new service-listing claim under this governing relationship or work agreement. Requires separate consent to the posted terms.' },
|
|
101
107
|
mandateAgreementId: {
|
|
102
108
|
type: 'string',
|
|
103
|
-
description: 'The active agreement whose work this claim is part of — the job you are doing.
|
|
109
|
+
description: 'The active agreement whose work this claim is part of — the job you are doing. The server checks you are a party to the job. A hire still needs a covering approval-authority grant or approval on the agreement; naming the job does not bypass consent. Leave it out when the claim belongs to no job.',
|
|
104
110
|
},
|
|
105
111
|
chatId: {
|
|
106
112
|
type: 'string',
|
|
@@ -61,9 +61,9 @@ export function presentSendResult(result, env) {
|
|
|
61
61
|
export const openConversationCapability = {
|
|
62
62
|
key: 'chat_open',
|
|
63
63
|
names: { sdk: 'chat_open', mcp: 'ziggs_chat_open' },
|
|
64
|
-
title: '
|
|
64
|
+
title: 'Open, invite to, or resume a conversation',
|
|
65
65
|
descriptions: {
|
|
66
|
-
sdk: 'Open a conversation or explicitly invite people to a room. participantId opens/reuses your person’s conversation with one person or agent, the same room each of their agents finds; newChat opens a separate subject, which can take a name. 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 may name agents, when creating a group or inviting into one: each must be reachable (your own, one of your company, published, or already engaged). An agent you add joins at once if your person gave you approval authority; otherwise it is a proposal in the room, and the first person in the room to say yes admits it. The result lists each id as added, already-here or pending. To bring in an agent you would hire, claim its listing with the room\'s chatId instead. 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 with scopeKind ["chat"].',
|
|
66
|
+
sdk: 'Open a conversation or explicitly invite people to a room. To resume admission of an existing hire, pass only chatId and agreementId; this uses the same recovery as the room UI and never hires or charges again. participantId opens/reuses your person’s conversation with one person or agent, the same room each of their agents finds; newChat opens a separate subject, which can take a name. 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 may name agents, when creating a group or inviting into one: each must be reachable (your own, one of your company, published, or already engaged). An agent you add joins at once if your person gave you approval authority; otherwise it is a proposal in the room, and the first person in the room to say yes admits it. The result lists each id as added, already-here or pending. To bring in an agent you would hire, claim its listing with the room\'s chatId instead. 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 with scopeKind ["chat"].',
|
|
67
67
|
},
|
|
68
68
|
annotation: 'write',
|
|
69
69
|
params: {
|
|
@@ -72,7 +72,7 @@ export const openConversationCapability = {
|
|
|
72
72
|
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.',
|
|
73
73
|
},
|
|
74
74
|
participantIds: { type: 'array', items: { type: 'string' }, description: '1–20 known ids. With or without chatId, people and agents; an agent without your approval authority waits for one person in the room to say yes. Use instead of participantId. Each invitation is authorized separately.' },
|
|
75
|
-
chatId: { type: 'string', description: 'Existing room
|
|
75
|
+
chatId: { type: 'string', description: 'Existing room. With participantIds: invite into it. With agreementId only: resume the approved hire admission without another hire or charge.' },
|
|
76
76
|
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.' },
|
|
77
77
|
name: { type: 'string', description: 'Name for a new group or a newChat room, 1–80 characters. Pair rooms and team channels take no name. A retry of the same idempotencyKey keeps the first name. Rename later is PATCH /chats/:chatId by someone who can admit people.' },
|
|
78
78
|
createIfMissing: { type: 'boolean', description: 'Set false to reuse only an existing room with this participant. A missing room is refused without creating a replacement.' },
|
|
@@ -86,12 +86,22 @@ export const openConversationCapability = {
|
|
|
86
86
|
},
|
|
87
87
|
agreementId: {
|
|
88
88
|
type: 'string',
|
|
89
|
-
description: '
|
|
89
|
+
description: 'With chatId: resume that existing hire’s room admission. Otherwise, the hire this open is the working room for. On a store-claim kickoff, pass the ' +
|
|
90
90
|
'claimed agreement id so the payer is stamped from-start in that room.',
|
|
91
91
|
},
|
|
92
92
|
},
|
|
93
93
|
needsAgentId: true,
|
|
94
94
|
handler: async (args, env) => {
|
|
95
|
+
if (args.chatId && args.agreementId) {
|
|
96
|
+
const result = await openConversation(args, fullCreds(env));
|
|
97
|
+
return {
|
|
98
|
+
...result,
|
|
99
|
+
appUrl: chatAppUrl(webAppOrigin(env), result.chatId),
|
|
100
|
+
message: result.status === 'ready'
|
|
101
|
+
? 'The hired agent is admitted under this agreement. No new hire or charge was made.'
|
|
102
|
+
: `Admission is still incomplete${result.reason ? `: ${result.reason}` : ''}. Retry this same call; do not claim again.`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
95
105
|
if (!args.participantId && !Array.isArray(args.participantIds)) {
|
|
96
106
|
throw new Error('participantId is required, or use participantIds to invite people');
|
|
97
107
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { isContextAccessRequest } from '../shared/contextAccessRequest.js';
|
|
2
|
-
import { looksLikeHandle, parseRef } from '../handles.js';
|
|
3
2
|
import { presentResourceActions } from './resourceActions.js';
|
|
4
3
|
import { ContextReadClient, CONTEXT_READ_TYPES, CONTEXT_READ_VIA, parseVia, viaHint, } from '../http/ContextReadClient.js';
|
|
5
4
|
import { ContextOpenClient } from '../http/ContextOpenClient.js';
|
|
@@ -259,29 +258,15 @@ export const contextDelegateCapability = {
|
|
|
259
258
|
const OPEN_REF_PARAMS = {
|
|
260
259
|
ref: {
|
|
261
260
|
type: 'string',
|
|
262
|
-
required: true,
|
|
263
261
|
description: 'What to open: a handle such as T7K2QXD (T task, A agreement, C chat, F artifact) in any case, a Ziggs web URL holding one, or an id from a prior response.',
|
|
264
262
|
},
|
|
263
|
+
// Cached connector schemas still send these. Both surfaces normalize them
|
|
264
|
+
// here; the server remains the only authority on the resource and access.
|
|
265
|
+
...Object.fromEntries(['artifactId', 'chatId', 'taskId', 'agreementId'].map(key => [key, {
|
|
266
|
+
type: 'string',
|
|
267
|
+
description: 'Use instead of ref; exactly one reference.',
|
|
268
|
+
}])),
|
|
265
269
|
};
|
|
266
|
-
function openBodyFromArgs(args) {
|
|
267
|
-
// A mistyped handle is refused here, before any call: its check letter
|
|
268
|
-
// fails. Any other id goes to the server, which knows older shapes.
|
|
269
|
-
const raw = typeof args.ref === 'string' ? args.ref.trim() : '';
|
|
270
|
-
const invalid = (message) => Object.assign(new Error(message), { code: 'INVALID_INPUT' });
|
|
271
|
-
if (!raw)
|
|
272
|
-
throw invalid('ref is required: a handle such as T7K2QXD, a Ziggs URL, or an id from a prior response');
|
|
273
|
-
const ref = parseRef(raw);
|
|
274
|
-
if (!ref && looksLikeHandle(raw))
|
|
275
|
-
throw invalid(`${raw} is not a handle: its check letter does not match. Check it for a typo`);
|
|
276
|
-
const body = { ref: ref?.id ?? raw };
|
|
277
|
-
if (typeof args['cursor'] === 'string')
|
|
278
|
-
body.cursor = args['cursor'];
|
|
279
|
-
if (typeof args['after'] === 'string')
|
|
280
|
-
body.after = args['after'];
|
|
281
|
-
if (typeof args['limit'] === 'number')
|
|
282
|
-
body.limit = args['limit'];
|
|
283
|
-
return body;
|
|
284
|
-
}
|
|
285
270
|
function presentOpenResult(result, env) {
|
|
286
271
|
// Resource-routed OAuth calls let the backend select the assistant. Their
|
|
287
272
|
// local credentials have no agent id, so only the response can name it.
|
|
@@ -364,7 +349,7 @@ export const openCapability = {
|
|
|
364
349
|
handler: async (args, env) => {
|
|
365
350
|
const creds = fullCreds(env);
|
|
366
351
|
const client = new ContextOpenClient(creds.operatorKey, creds.agentId, env.baseUrl, creds.laneId);
|
|
367
|
-
return presentOpenResult(await client.open(
|
|
352
|
+
return presentOpenResult(await client.open(args), env);
|
|
368
353
|
},
|
|
369
354
|
};
|
|
370
355
|
export const accessExplainCapability = {
|
|
@@ -380,7 +365,7 @@ export const accessExplainCapability = {
|
|
|
380
365
|
handler: async (args, env) => {
|
|
381
366
|
const creds = fullCreds(env);
|
|
382
367
|
const client = new ContextOpenClient(creds.operatorKey, creds.agentId, env.baseUrl, creds.laneId);
|
|
383
|
-
const result = await client.explain(
|
|
368
|
+
const result = await client.explain(args);
|
|
384
369
|
return presentOpenResult(result, env);
|
|
385
370
|
},
|
|
386
371
|
};
|
|
@@ -418,7 +403,7 @@ export const accessRequestCapability = {
|
|
|
418
403
|
title: 'Request access for yourself or named readers',
|
|
419
404
|
annotation: 'destructive', needsAgentId: true,
|
|
420
405
|
descriptions: {
|
|
421
|
-
sdk: 'Request optional bounded access to one artifact, chat, agreement context or task. Creates only a pending approval; never grants access or approves.
|
|
406
|
+
sdk: 'Request optional bounded access to one artifact, chat, agreement context or task. Creates only a pending approval; never grants access or approves. Choose recipientId when several approvers are offered; with one, it is selected automatically: your work brief offers them for this wake — workBrief.actingFor.customer.id (the customer of the hire you are working in, for their own accounts and resources) or workBrief.actingFor.operator.id (your operator, for tools you bring). Anyone else is refused, and the refusal lists who you can ask. Artifact requests also need chatId, the conversation they approve in. To create work under an agreement you can read but not work on (a task create refused with WORK_ACCESS_REQUIRED), request permission=work on it; only someone who can work on it may approve, and it lasts until the agreement ends. Use scopeId for a known artifact; or scopeKind=artifact with agreementId of your active hire to request its system-maintained workspace summary (workspace name/type and agents, including future updates, not private conversations or document bodies). Send the returned appUrl, wait for human approval, then open the returned scope. To arrange reading of one artifact for others, name them in holders (agents by id, people or workspaces by id or the participant reference this room shows you); include yourself only if you need to read it too. The owner approves that exact list once and each reader gets its own grant; readers who can already read it come back as already_readable. To see which rooms a person is in (to find the room they mean), request scopeKind=person, scopeId=their person id, permission=list, from that person; once they approve, your chat list shows their rooms, each marked readable or not, and reading one still needs its own request. Hiring alone grants no source access. Revocation and expiry are checked by the system.',
|
|
422
407
|
},
|
|
423
408
|
params: {
|
|
424
409
|
scopeKind: { type: 'string', required: true, enum: ['artifact', 'chat', 'agreement', 'task', 'person'] },
|
|
@@ -432,7 +417,7 @@ export const accessRequestCapability = {
|
|
|
432
417
|
// Not required here: the server requires it, and a request without it
|
|
433
418
|
// comes back refused with the people this agent may ask. Required in the
|
|
434
419
|
// schema, a host refused the call before that list could arrive.
|
|
435
|
-
recipientId: { type: 'string', description: 'Who to ask: workBrief.actingFor.customer.id (the customer of this hire) or workBrief.actingFor.operator.id (your operator).
|
|
420
|
+
recipientId: { type: 'string', description: 'Who to ask: workBrief.actingFor.customer.id (the customer of this hire) or workBrief.actingFor.operator.id (your operator). Omit when only one approver is available; otherwise the response lists choices.' },
|
|
436
421
|
holders: {
|
|
437
422
|
type: 'array',
|
|
438
423
|
description: 'Artifact only, read only: up to 20 readers, each { kind: "agent" | "user" | "org", id }. Leave yourself out to arrange it for others. Omit to ask for yourself.',
|
|
@@ -448,7 +433,7 @@ export const accessRequestCapability = {
|
|
|
448
433
|
idempotencyKey: { type: 'string', description: 'Reuse for the same logical request' },
|
|
449
434
|
},
|
|
450
435
|
handler: async (args, env) => {
|
|
451
|
-
// recipientId is
|
|
436
|
+
// recipientId is resolved by the server: it picks the sole eligible recipient
|
|
452
437
|
// and lists who this agent may ask, which nothing on this side knows.
|
|
453
438
|
for (const key of ['scopeKind', 'reason']) {
|
|
454
439
|
if (typeof args[key] !== 'string' || !String(args[key]).trim())
|
|
@@ -13,6 +13,12 @@ const BRIEFS = {
|
|
|
13
13
|
does: 'Sets this assistant up in each workspace its owner has enabled.',
|
|
14
14
|
changes: 'may create hires, grants and assistant identities; a retry reuses them',
|
|
15
15
|
},
|
|
16
|
+
ziggs_notification_status: {
|
|
17
|
+
does: 'Reads this connection’s notification state, expiry and failures in one workspace. Callback receipt is not a host response; null means unknown.',
|
|
18
|
+
},
|
|
19
|
+
ziggs_inbox_changes: {
|
|
20
|
+
does: 'Reads 25 new inbox references per page; reuse the observation cursor to wait. No claim or ack.',
|
|
21
|
+
},
|
|
16
22
|
ziggs_inbox_peek: {
|
|
17
23
|
does: 'Says whether mail arrived since your last ack (count is 1 or 0, not a total) and names who you represent. Does not return deliveries, claim the inbox or ack.',
|
|
18
24
|
next: 'ziggs_inbox to read it',
|
|
@@ -63,6 +63,8 @@ const HTTP_BINDINGS = {
|
|
|
63
63
|
open: { method: 'POST', path: '/context/open' },
|
|
64
64
|
access_explain: { method: 'POST', path: '/context/access/explain' },
|
|
65
65
|
inbox_peek: { method: 'GET', path: '/inbox/peek' },
|
|
66
|
+
inbox_changes: { method: 'GET', path: '/inbox/changes' },
|
|
67
|
+
notification_status: { method: 'GET', path: '/oauth/events/status' },
|
|
66
68
|
task_get: { method: 'GET', path: '/tasks/:taskId' },
|
|
67
69
|
task_list: { method: 'GET', path: '/tasks' },
|
|
68
70
|
link_list: { method: 'GET', path: '/agreements?engagementKind=link' },
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import type { ResourceAction } from '../http/ContextOpenClient.js';
|
|
1
|
+
import type { ResourceAction, ResourceActionCall } from '../http/ContextOpenClient.js';
|
|
2
|
+
export declare function presentResourceCall<T extends ResourceActionCall>(value: T, surface: 'sdk' | 'mcp'): T;
|
|
2
3
|
/** Bind canonical tool names without changing the backend's decision or scope. */
|
|
3
4
|
export declare function presentResourceActions(actions: ResourceAction[], surface: 'sdk' | 'mcp'): ResourceAction[];
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
export function presentResourceCall(value, surface) {
|
|
2
|
+
const name = value.tool.replace(/^ziggs_/, '');
|
|
3
|
+
return { ...value, tool: surface === 'mcp' ? `ziggs_${name}` : name };
|
|
4
|
+
}
|
|
1
5
|
/** Bind canonical tool names without changing the backend's decision or scope. */
|
|
2
6
|
export function presentResourceActions(actions, surface) {
|
|
3
|
-
const toolName = (name) => surface === 'mcp' && !name.startsWith('ziggs_') ? `ziggs_${name}` : name;
|
|
4
|
-
const call = (value) => ({ ...value, tool: toolName(value.tool) });
|
|
5
7
|
return actions.map(action => ({ ...action,
|
|
6
|
-
...(action.tool ? { tool:
|
|
7
|
-
...(action.recovery ? { recovery:
|
|
8
|
+
...(action.tool ? { tool: presentResourceCall({ tool: action.tool, args: action.args ?? {} }, surface).tool } : {}),
|
|
9
|
+
...(action.recovery ? { recovery: presentResourceCall(action.recovery, surface) } : {}),
|
|
8
10
|
}));
|
|
9
11
|
}
|
|
@@ -351,12 +351,14 @@ export interface ClaimOptions {
|
|
|
351
351
|
* resumes it without hiring or charging again.
|
|
352
352
|
*/
|
|
353
353
|
export interface ClaimRoom {
|
|
354
|
-
chatId
|
|
355
|
-
status: 'awaiting_approval' | 'ready' | 'admission_failed';
|
|
354
|
+
chatId?: string;
|
|
355
|
+
status: 'awaiting_approval' | 'ready' | 'admission_failed' | 'preparation_failed' | 'unavailable';
|
|
356
356
|
message?: string;
|
|
357
357
|
recovery?: {
|
|
358
358
|
method: string;
|
|
359
359
|
path: string;
|
|
360
|
+
tool?: string;
|
|
361
|
+
args?: Record<string, unknown>;
|
|
360
362
|
};
|
|
361
363
|
}
|
|
362
364
|
export declare function claimAgreement(agreementId: string, creds: Creds, opts?: ClaimOptions): Promise<{
|
|
@@ -1,9 +1,38 @@
|
|
|
1
|
-
import { type Creds } from '../types.js';
|
|
1
|
+
import { type Creds, type InboxChangesOptions, type InboxChangesResult } from '../types.js';
|
|
2
2
|
export interface AssistantWorkspace {
|
|
3
3
|
orgId: string;
|
|
4
4
|
name: string;
|
|
5
5
|
agentId: string;
|
|
6
6
|
}
|
|
7
|
+
export interface McpEventStatus {
|
|
8
|
+
asOf: string;
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
orgId: string;
|
|
11
|
+
event: 'ziggs.inbox.updated';
|
|
12
|
+
receiptMeaning: string;
|
|
13
|
+
truncated: boolean;
|
|
14
|
+
subscriptions: Array<{
|
|
15
|
+
id: string;
|
|
16
|
+
state: 'active' | 'expired' | 'stopped';
|
|
17
|
+
expiresAt: string;
|
|
18
|
+
lastVerifiedAt: string | null;
|
|
19
|
+
lastCheckedAt: string | null;
|
|
20
|
+
lastWorkerErrorAt: string | null;
|
|
21
|
+
lastAcceptedAt: string | null;
|
|
22
|
+
lastAttempt: {
|
|
23
|
+
at: string;
|
|
24
|
+
httpStatus: number | null;
|
|
25
|
+
outcome: 'accepted' | 'retrying' | 'failed' | null;
|
|
26
|
+
} | null;
|
|
27
|
+
lastFailure: {
|
|
28
|
+
at: string;
|
|
29
|
+
reason: 'network' | 'http' | 'attempts_exhausted' | null;
|
|
30
|
+
httpStatus: number | null;
|
|
31
|
+
} | null;
|
|
32
|
+
nextAttemptAt: string | null;
|
|
33
|
+
stoppedReason: 'unsubscribed' | 'access_revoked' | 'connection_changed' | 'receiver_gone' | null;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
7
36
|
/**
|
|
8
37
|
* The reads a connection makes about itself: which workspaces it works in,
|
|
9
38
|
* and its one inbox across all of them. It never remembers a
|
|
@@ -14,7 +43,7 @@ export declare class AssistantConnectionClient {
|
|
|
14
43
|
private readonly creds;
|
|
15
44
|
private readonly baseUrl;
|
|
16
45
|
constructor(creds: Creds, baseUrl?: string);
|
|
17
|
-
request<T>(path: string, body?: unknown): Promise<T>;
|
|
46
|
+
request<T>(path: string, body?: unknown, signal?: AbortSignal): Promise<T>;
|
|
18
47
|
/** Read existing enabled assistant hires. Provisioning is an explicit sync action. */
|
|
19
48
|
workspaces(): Promise<AssistantWorkspace[]>;
|
|
20
49
|
/** Which of the connection's assistants is on an existing thing. */
|
|
@@ -23,6 +52,9 @@ export declare class AssistantConnectionClient {
|
|
|
23
52
|
choices: AssistantWorkspace[];
|
|
24
53
|
}>;
|
|
25
54
|
inbox(waitSeconds?: number, snapshot?: boolean): Promise<Record<string, unknown>>;
|
|
55
|
+
getChanges(options?: InboxChangesOptions): Promise<InboxChangesResult>;
|
|
56
|
+
/** Receipt diagnostics only: this does not subscribe, renew or wake a host. */
|
|
57
|
+
eventStatus(orgId: string, signal?: AbortSignal): Promise<McpEventStatus>;
|
|
26
58
|
ack(upTo: string, handledResourceIds?: string[]): Promise<unknown>;
|
|
27
59
|
park(agreementId: string): Promise<{
|
|
28
60
|
agreementId: string;
|
|
@@ -15,9 +15,10 @@ export class AssistantConnectionClient {
|
|
|
15
15
|
this.creds = creds;
|
|
16
16
|
this.baseUrl = baseUrl;
|
|
17
17
|
}
|
|
18
|
-
async request(path, body) {
|
|
18
|
+
async request(path, body, signal) {
|
|
19
19
|
const response = await fetch(`${this.baseUrl}/oauth/${path}`, {
|
|
20
20
|
method: body === undefined ? 'GET' : 'POST',
|
|
21
|
+
signal,
|
|
21
22
|
headers: {
|
|
22
23
|
Authorization: `Bearer ${this.creds.operatorKey}`,
|
|
23
24
|
'Content-Type': 'application/json',
|
|
@@ -47,6 +48,18 @@ export class AssistantConnectionClient {
|
|
|
47
48
|
inbox(waitSeconds = 0, snapshot = false) {
|
|
48
49
|
return this.request(`inbox${snapshot ? "/snapshot" : ""}?wait=${waitSeconds}`);
|
|
49
50
|
}
|
|
51
|
+
getChanges(options = {}) {
|
|
52
|
+
const query = new URLSearchParams();
|
|
53
|
+
if (options.cursor !== undefined)
|
|
54
|
+
query.set('cursor', options.cursor);
|
|
55
|
+
if (options.waitSeconds !== undefined)
|
|
56
|
+
query.set('wait', String(options.waitSeconds));
|
|
57
|
+
return this.request(`inbox/changes?${query}`, undefined, options.signal);
|
|
58
|
+
}
|
|
59
|
+
/** Receipt diagnostics only: this does not subscribe, renew or wake a host. */
|
|
60
|
+
eventStatus(orgId, signal) {
|
|
61
|
+
return this.request(`events/status?${new URLSearchParams({ orgId })}`, undefined, signal);
|
|
62
|
+
}
|
|
50
63
|
ack(upTo, handledResourceIds = []) {
|
|
51
64
|
return this.request('inbox/ack', { upTo, handledResourceIds });
|
|
52
65
|
}
|
|
@@ -21,6 +21,14 @@ export interface ChatOpenInvite {
|
|
|
21
21
|
/** pending: an agent's joining waits for the people in the room to approve it. */
|
|
22
22
|
status: 'added' | 'already-here' | 'pending';
|
|
23
23
|
admissionId?: string;
|
|
24
|
+
approvers?: Array<{
|
|
25
|
+
id: string;
|
|
26
|
+
name: string | null;
|
|
27
|
+
}>;
|
|
28
|
+
approveIn?: {
|
|
29
|
+
kind: 'chat';
|
|
30
|
+
chatId: string;
|
|
31
|
+
};
|
|
24
32
|
}
|
|
25
33
|
export interface OpenConversationInput {
|
|
26
34
|
participantId?: string;
|
|
@@ -33,16 +41,20 @@ export interface OpenConversationInput {
|
|
|
33
41
|
agreementId?: string;
|
|
34
42
|
createIfMissing?: boolean;
|
|
35
43
|
}
|
|
36
|
-
export
|
|
37
|
-
newChat?: boolean;
|
|
38
|
-
agreementId?: string;
|
|
39
|
-
createIfMissing?: boolean;
|
|
40
|
-
}): Promise<{
|
|
44
|
+
export interface OpenConversationResult {
|
|
41
45
|
chatId: string;
|
|
42
46
|
reused?: boolean;
|
|
43
47
|
invited?: ChatOpenInvite[];
|
|
44
48
|
name?: string;
|
|
45
|
-
|
|
49
|
+
agreementId?: string;
|
|
50
|
+
status?: 'ready' | 'admission_failed';
|
|
51
|
+
reason?: string;
|
|
52
|
+
}
|
|
53
|
+
export declare function openConversation(participant: string | OpenConversationInput, creds: Creds, { newChat, agreementId, createIfMissing }?: {
|
|
54
|
+
newChat?: boolean;
|
|
55
|
+
agreementId?: string;
|
|
56
|
+
createIfMissing?: boolean;
|
|
57
|
+
}): Promise<OpenConversationResult>;
|
|
46
58
|
export interface SendChatMessageInput {
|
|
47
59
|
/**
|
|
48
60
|
* Chat to send in. Required unless `to` names a person you already
|
|
@@ -168,12 +180,7 @@ export declare class ChatClient {
|
|
|
168
180
|
newChat?: boolean;
|
|
169
181
|
agreementId?: string;
|
|
170
182
|
createIfMissing?: boolean;
|
|
171
|
-
}): Promise<
|
|
172
|
-
chatId: string;
|
|
173
|
-
reused?: boolean;
|
|
174
|
-
invited?: ChatOpenInvite[];
|
|
175
|
-
name?: string;
|
|
176
|
-
}>;
|
|
183
|
+
}): Promise<OpenConversationResult>;
|
|
177
184
|
addMember(input: AddChatMemberInput): Promise<AddChatMemberResult>;
|
|
178
185
|
sendMessage(input: SendChatMessageInput): Promise<SendChatMessageResult>;
|
|
179
186
|
listMine(): Promise<ChatReadDto[]>;
|
package/dist/http/ChatClient.js
CHANGED
|
@@ -15,16 +15,23 @@ export async function openConversation(participant, creds, { newChat = false, ag
|
|
|
15
15
|
const input = typeof participant === 'string'
|
|
16
16
|
? { participantId: participant, ...(newChat ? { newChat } : {}), ...(agreementId ? { agreementId } : {}), ...(createIfMissing !== undefined ? { createIfMissing } : {}) }
|
|
17
17
|
: participant;
|
|
18
|
-
|
|
18
|
+
const resumeHire = !!input?.chatId && !!input.agreementId;
|
|
19
|
+
if (resumeHire && (input.participantId || input.participantIds || input.newChat !== undefined || input.name !== undefined || input.idempotencyKey !== undefined || input.createIfMissing !== undefined)) {
|
|
20
|
+
throw new Error('To resume hire admission, pass only chatId and agreementId');
|
|
21
|
+
}
|
|
22
|
+
if (!input || (!resumeHire && !input.participantId && !input.participantIds?.length)) {
|
|
19
23
|
throw new Error('participantId is required, or use participantIds to invite people');
|
|
20
24
|
}
|
|
21
25
|
assertCreds(creds, 'open conversation');
|
|
22
|
-
const
|
|
26
|
+
const route = resumeHire
|
|
27
|
+
? `/chats/${encodeURIComponent(input.chatId)}/hires/${encodeURIComponent(input.agreementId)}/admission`
|
|
28
|
+
: '/chats';
|
|
29
|
+
const res = await fetch(`${getBackendUrl()}${route}`, {
|
|
23
30
|
method: 'POST',
|
|
24
31
|
headers: buildHeaders(creds),
|
|
25
32
|
// Only sent when asked for: an older backend ignores the field, so a
|
|
26
33
|
// caller that never wants a separate room behaves identically either way.
|
|
27
|
-
body: JSON.stringify(input),
|
|
34
|
+
body: JSON.stringify(resumeHire ? {} : input),
|
|
28
35
|
});
|
|
29
36
|
if (!res.ok) {
|
|
30
37
|
const body = await res.text().catch(() => '');
|
|
@@ -38,6 +45,9 @@ export async function openConversation(participant, creds, { newChat = false, ag
|
|
|
38
45
|
// send it. A backend from before this field cannot be assumed to have created
|
|
39
46
|
// the room — defaulting to false would report a reused conversation as fresh,
|
|
40
47
|
// which is worse than saying nothing.
|
|
48
|
+
if (resumeHire && (data.chatId !== input.chatId || data.agreementId !== input.agreementId || !['ready', 'admission_failed'].includes(String(data.status)))) {
|
|
49
|
+
throw new Error('Invalid hire-admission response: expected the selected chat, hire and admission status');
|
|
50
|
+
}
|
|
41
51
|
const reused = data['reused'];
|
|
42
52
|
const name = data['name'];
|
|
43
53
|
const invited = Array.isArray(data['invited'])
|
|
@@ -45,6 +55,11 @@ export async function openConversation(participant, creds, { newChat = false, ag
|
|
|
45
55
|
: undefined;
|
|
46
56
|
return {
|
|
47
57
|
chatId: data['chatId'],
|
|
58
|
+
...(resumeHire ? {
|
|
59
|
+
agreementId: input.agreementId,
|
|
60
|
+
status: data.status,
|
|
61
|
+
...(typeof data.reason === 'string' ? { reason: data.reason } : {}),
|
|
62
|
+
} : {}),
|
|
48
63
|
...(typeof reused === 'boolean' ? { reused } : {}),
|
|
49
64
|
...(typeof name === 'string' && name ? { name } : {}),
|
|
50
65
|
...(invited ? { invited } : {}),
|
|
@@ -21,6 +21,12 @@ export interface ResourceActionCall {
|
|
|
21
21
|
tool: string;
|
|
22
22
|
args: Record<string, unknown>;
|
|
23
23
|
required?: string[];
|
|
24
|
+
unavailableReason?: string;
|
|
25
|
+
approvers?: Array<{
|
|
26
|
+
id: string;
|
|
27
|
+
name: string | null;
|
|
28
|
+
role: string;
|
|
29
|
+
}>;
|
|
24
30
|
}
|
|
25
31
|
export interface ResourceAction extends Partial<ResourceActionCall> {
|
|
26
32
|
action: string;
|
|
@@ -90,3 +96,4 @@ export declare class ContextOpenClient {
|
|
|
90
96
|
explain(body: ContextOpenBody): Promise<ContextExplainResult>;
|
|
91
97
|
private post;
|
|
92
98
|
}
|
|
99
|
+
export declare function normalizeContextOpenBody(args: Record<string, unknown>): ContextOpenBody;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { looksLikeHandle, parseRef } from '../handles.js';
|
|
1
2
|
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
2
3
|
import { pollSurfaceError } from '../shared/rateLimit.js';
|
|
3
4
|
import { buildOperatorHeaders } from './operatorHeaders.js';
|
|
@@ -31,19 +32,14 @@ export class ContextOpenClient {
|
|
|
31
32
|
return this.post('/context/access/explain', body);
|
|
32
33
|
}
|
|
33
34
|
async post(path, body) {
|
|
34
|
-
const
|
|
35
|
-
if (filled.length !== 1) {
|
|
36
|
-
// A caller's input mistake, named in the caller's words: the shared
|
|
37
|
-
// INVALID_INPUT code, and no internal class name in the sentence.
|
|
38
|
-
throw Object.assign(new Error('Pass ref (a handle such as T7K2QXD, a Ziggs URL, or an id), or exactly one of artifactId, chatId, taskId, agreementId.'), { code: 'INVALID_INPUT' });
|
|
39
|
-
}
|
|
35
|
+
const normalized = normalizeContextOpenBody({ ...body });
|
|
40
36
|
const url = `${this.baseUrl || getBackendUrl()}${path}`;
|
|
41
37
|
const res = await fetch(url, {
|
|
42
38
|
method: 'POST',
|
|
43
39
|
headers: {
|
|
44
40
|
...buildOperatorHeaders(this.operatorKey, this.agentId, { 'Content-Type': 'application/json' }, this.laneId),
|
|
45
41
|
},
|
|
46
|
-
body: JSON.stringify(
|
|
42
|
+
body: JSON.stringify(normalized),
|
|
47
43
|
});
|
|
48
44
|
const text = await res.text().catch(() => '');
|
|
49
45
|
if (!res.ok) {
|
|
@@ -52,3 +48,26 @@ export class ContextOpenClient {
|
|
|
52
48
|
return JSON.parse(text);
|
|
53
49
|
}
|
|
54
50
|
}
|
|
51
|
+
export function normalizeContextOpenBody(args) {
|
|
52
|
+
// A mistyped handle is refused here, before any call: its check letter
|
|
53
|
+
// fails. Any other id goes to the server, which knows older shapes.
|
|
54
|
+
const invalid = (message) => Object.assign(new Error(message), { code: 'INVALID_INPUT' });
|
|
55
|
+
const keys = ['ref', 'artifactId', 'chatId', 'taskId', 'agreementId'].filter(key => args[key] !== undefined);
|
|
56
|
+
if (keys.length !== 1)
|
|
57
|
+
throw invalid('Pass exactly one resource reference: ref, artifactId, chatId, taskId, or agreementId');
|
|
58
|
+
const key = keys[0];
|
|
59
|
+
const raw = typeof args[key] === 'string' ? args[key].trim() : '';
|
|
60
|
+
if (!raw)
|
|
61
|
+
throw invalid(`${key} must be a non-empty resource reference`);
|
|
62
|
+
const ref = parseRef(raw);
|
|
63
|
+
if (!ref && looksLikeHandle(raw))
|
|
64
|
+
throw invalid(`${raw} is not a handle: its check letter does not match. Check it for a typo`);
|
|
65
|
+
const body = { [key]: ref?.id ?? raw };
|
|
66
|
+
if (typeof args['cursor'] === 'string')
|
|
67
|
+
body.cursor = args['cursor'];
|
|
68
|
+
if (typeof args['after'] === 'string')
|
|
69
|
+
body.after = args['after'];
|
|
70
|
+
if (typeof args['limit'] === 'number')
|
|
71
|
+
body.limit = args['limit'];
|
|
72
|
+
return body;
|
|
73
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { InboxAckResult, InboxEnvelope, InboxPeek, InboxReadOptions } from '../types.js';
|
|
1
|
+
import type { InboxAckResult, InboxEnvelope, InboxPeek, InboxReadOptions, InboxChangesOptions, InboxChangesResult } from '../types.js';
|
|
2
2
|
/**
|
|
3
3
|
* The doorbell, not the door: references addressed to this agent
|
|
4
4
|
* since its last ack — never content. Flow: inbox → read → act → ack
|
|
@@ -26,6 +26,9 @@ export declare class InboxClient {
|
|
|
26
26
|
});
|
|
27
27
|
private headers;
|
|
28
28
|
private inboxUrl;
|
|
29
|
+
/** At most 25 retained references per page. Keep cursor for subsequent waits;
|
|
30
|
+
* observation never claims work or acknowledges it. Expired cursors return 410. */
|
|
31
|
+
getChanges(opts?: InboxChangesOptions): Promise<InboxChangesResult>;
|
|
29
32
|
/** Read without acquiring worker ownership or recording receipts. */
|
|
30
33
|
getSnapshot(opts?: InboxReadOptions & {
|
|
31
34
|
signal?: AbortSignal;
|
package/dist/http/InboxClient.js
CHANGED
|
@@ -57,6 +57,18 @@ export class InboxClient {
|
|
|
57
57
|
}
|
|
58
58
|
return url.toString();
|
|
59
59
|
}
|
|
60
|
+
/** At most 25 retained references per page. Keep cursor for subsequent waits;
|
|
61
|
+
* observation never claims work or acknowledges it. Expired cursors return 410. */
|
|
62
|
+
async getChanges(opts = {}) {
|
|
63
|
+
const url = new URL(this.inboxUrl('/inbox/changes', opts));
|
|
64
|
+
if (opts.cursor !== undefined)
|
|
65
|
+
url.searchParams.set('cursor', opts.cursor);
|
|
66
|
+
const res = await fetch(url.toString(), { headers: this.headers(), signal: opts.signal });
|
|
67
|
+
const body = await res.text().catch(() => '');
|
|
68
|
+
if (!res.ok)
|
|
69
|
+
throw pollSurfaceError('InboxClient.getChanges', res, body);
|
|
70
|
+
return JSON.parse(body);
|
|
71
|
+
}
|
|
60
72
|
/** Read without acquiring worker ownership or recording receipts. */
|
|
61
73
|
async getSnapshot(opts = {}) {
|
|
62
74
|
const res = await fetch(this.inboxUrl('/inbox/snapshot', opts), {
|
package/dist/index.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ export type { ContinuationKind, InboxAckDriver, InboxHandling, RepresentedPerson
|
|
|
30
30
|
export { inboxEngagement, isEchoOrDuplicate, isHireWorkOrder, contactPath, formationGuide, marketplaceFormation, liveHireChangedTerms, liveHireConflictFromStatus, withdrawHeldGraph, reminderHonesty, graphReleaseHonesty, hintsFromTasks, } from './engagementGuide.js';
|
|
31
31
|
export type { EngagementGuide, EngagementGuideKind, EngagementOutcome, EngagementTaskHint, ContactPathInput, FormationInput, GraphHonesty, HireTermsHonesty, WaitingOn, } from './engagementGuide.js';
|
|
32
32
|
export { isContextAccessRequest } from './shared/contextAccessRequest.js';
|
|
33
|
-
export { AssistantConnectionClient, type AssistantWorkspace, } from './http/AssistantConnectionClient.js';
|
|
33
|
+
export { AssistantConnectionClient, type AssistantWorkspace, type McpEventStatus, } from './http/AssistantConnectionClient.js';
|
|
34
34
|
export { withAssistantRequest, assistantRequestHeaders } from './http/operatorHeaders.js';
|
|
35
35
|
export { HANDLE_ALPHABET, findHandles, looksLikeHandle, parseHandle, parseRef, type HandleKind } from './handles.js';
|
|
36
|
+
export { describeOperationError, type OperationError } from './shared/operationError.js';
|
package/dist/index.js
CHANGED
|
@@ -31,3 +31,4 @@ export { AssistantConnectionClient, } from './http/AssistantConnectionClient.js'
|
|
|
31
31
|
export { withAssistantRequest, assistantRequestHeaders } from './http/operatorHeaders.js';
|
|
32
32
|
// Short handles: T…, A…, C…, F… (task, agreement, chat, artifact).
|
|
33
33
|
export { HANDLE_ALPHABET, findHandles, looksLikeHandle, parseHandle, parseRef } from './handles.js';
|
|
34
|
+
export { describeOperationError } from './shared/operationError.js';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One refusal shape for MCP, SDK tools, and agent runtimes.
|
|
3
|
+
*
|
|
4
|
+
* classify from the thrown {@link ApiError} fields (`status`,
|
|
5
|
+
* `code`, `body` / `message`). No client-prefix strip, no HTTP-status regex,
|
|
6
|
+
* no re-parsing a JSON body that was concatenated into the message string.
|
|
7
|
+
*/
|
|
8
|
+
export interface OperationError {
|
|
9
|
+
code: string;
|
|
10
|
+
message: string;
|
|
11
|
+
hint?: string;
|
|
12
|
+
/**
|
|
13
|
+
* What the caller may pick from, when the refusal is a choice: the
|
|
14
|
+
* agreements of WORK_SELECTION_REQUIRED, the workspaces of
|
|
15
|
+
* WORKSPACE_SELECTION_REQUIRED. Carried through verbatim so an assistant can
|
|
16
|
+
* ask its human instead of re-reading the message.
|
|
17
|
+
*/
|
|
18
|
+
choices?: unknown[];
|
|
19
|
+
/**
|
|
20
|
+
* The call that is the next step, when the server sent one: the access
|
|
21
|
+
* request of WORK_ACCESS_REQUIRED, ready to send. Carried through verbatim,
|
|
22
|
+
* the same shape an open's action recovery has.
|
|
23
|
+
*/
|
|
24
|
+
recovery?: Record<string, unknown>;
|
|
25
|
+
/** Scoped restart position returned when too many observation streams exist. */
|
|
26
|
+
recoveryCursor?: string;
|
|
27
|
+
/** hidden | unreadable, the same outcome open and access_explain give. */
|
|
28
|
+
outcome?: string;
|
|
29
|
+
/** The resource the outcome is about. */
|
|
30
|
+
ref?: {
|
|
31
|
+
kind: string;
|
|
32
|
+
id: string;
|
|
33
|
+
};
|
|
34
|
+
/** For an unreadable resource: the request to make, as open lists it. */
|
|
35
|
+
actions?: unknown[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Classify a thrown client error (or a plain string this layer raised) into a
|
|
39
|
+
* stable shape on either tool surface.
|
|
40
|
+
*/
|
|
41
|
+
export declare function describeOperationError(input: unknown, surface?: 'sdk' | 'mcp'): OperationError;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { isCoverageRefusal } from './apiError.js';
|
|
2
|
+
import { presentResourceCall } from '../capabilities/resourceActions.js';
|
|
3
|
+
/**
|
|
4
|
+
* The shared refusal vocabulary every read tool answers in (hidden,
|
|
5
|
+
* unreadable, invalid input), so an assistant can branch on the code instead
|
|
6
|
+
* of parsing each tool's sentence.
|
|
7
|
+
*/
|
|
8
|
+
const SHARED_REFUSALS = new Set(['RESOURCE_HIDDEN', 'RESOURCE_UNREADABLE', 'INVALID_INPUT']);
|
|
9
|
+
const SCOPE_DENIED_HINT = 'For missing access to a returned artifact, chat, agreement, or task, use ' +
|
|
10
|
+
'ziggs_access_request with that original resource id and your purpose. It creates ' +
|
|
11
|
+
'a pending access approval agreement; it grants no access. After approval, ' +
|
|
12
|
+
'ziggs_open the same resource using the same agent identity. For earlier chat ' +
|
|
13
|
+
'messages request temporal=from-start; replying also requires permission=reply. ' +
|
|
14
|
+
'Do not issue yourself a grant. A new room does not show this room\'s history.';
|
|
15
|
+
// a human-authority denial is not "try again with more scope": no
|
|
16
|
+
// retry by this caller can ever pass it, because the guard refuses on being an
|
|
17
|
+
// agent at all, before it looks at what was asked. Without a next action the
|
|
18
|
+
// only strategy left is repetition, which is what agents did.
|
|
19
|
+
const HUMAN_AUTHORITY_HINT = 'Ask the account owner to perform this action in their signed-in session. ' +
|
|
20
|
+
'Requesting resource access cannot authorize a human-only operation; retrying as an agent will not change that.';
|
|
21
|
+
/**
|
|
22
|
+
* What covers a conversation, refused. These arrive as 429 or 403,
|
|
23
|
+
* and a 429 read as "rate limited" tells an agent to wait and retry, which can
|
|
24
|
+
* never help: the writer has to hire the agent, or their person has to set up
|
|
25
|
+
* the agreement. The backend's own code and sentence are the answer.
|
|
26
|
+
*/
|
|
27
|
+
const COVERAGE_HINT = 'Retry only after the condition in this refusal changes. Use the returned recovery call when present. ' +
|
|
28
|
+
'For missing resource permission, ziggs_access_request asks your customer or operator and returns who can approve; ' +
|
|
29
|
+
'funding, ended agreements, and task-only terms require the responsible party to change those terms.';
|
|
30
|
+
function codeForStatus(status) {
|
|
31
|
+
if (status === 401)
|
|
32
|
+
return 'NOT_AUTHENTICATED';
|
|
33
|
+
if (status === 403)
|
|
34
|
+
return 'NOT_AUTHORIZED';
|
|
35
|
+
if (status === 404)
|
|
36
|
+
return 'NOT_FOUND';
|
|
37
|
+
if (status === 409)
|
|
38
|
+
return 'CONFLICT';
|
|
39
|
+
if (status === 429)
|
|
40
|
+
return 'RATE_LIMITED';
|
|
41
|
+
if (status >= 500)
|
|
42
|
+
return 'UPSTREAM_ERROR';
|
|
43
|
+
if (status >= 400)
|
|
44
|
+
return 'BAD_REQUEST';
|
|
45
|
+
return 'TOOL_ERROR';
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* An `ApiError` from api-client, recognised by shape rather than `instanceof`:
|
|
49
|
+
* a bundled or duplicated copy of the class would fail an identity check while
|
|
50
|
+
* carrying exactly the fields we need.
|
|
51
|
+
*/
|
|
52
|
+
function thrownFields(err) {
|
|
53
|
+
if (typeof err === 'string') {
|
|
54
|
+
return { message: err, status: null, code: null, choices: null };
|
|
55
|
+
}
|
|
56
|
+
if (typeof err !== 'object' || err === null) {
|
|
57
|
+
return { message: String(err), status: null, code: null, choices: null };
|
|
58
|
+
}
|
|
59
|
+
const o = err;
|
|
60
|
+
const status = typeof o.status === 'number' && o.status >= 100 && o.status < 600
|
|
61
|
+
? o.status
|
|
62
|
+
: null;
|
|
63
|
+
const code = typeof o.code === 'string' && o.code ? o.code : null;
|
|
64
|
+
const message = typeof o.message === 'string' && o.message ? o.message : String(err);
|
|
65
|
+
return { message, status, code, choices: choicesIn(o.body) };
|
|
66
|
+
}
|
|
67
|
+
/** The `recovery` call of a refusal body, when the server sent one. */
|
|
68
|
+
function recoveryIn(body) {
|
|
69
|
+
if (typeof body !== 'string' || !body)
|
|
70
|
+
return null;
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(body);
|
|
73
|
+
const recovery = parsed?.recovery;
|
|
74
|
+
if (!recovery || typeof recovery !== 'object' || Array.isArray(recovery))
|
|
75
|
+
return null;
|
|
76
|
+
return recovery;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** The `choices` list of a refusal body, when the server sent one. */
|
|
83
|
+
function choicesIn(body) {
|
|
84
|
+
if (typeof body !== 'string' || !body)
|
|
85
|
+
return null;
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(body);
|
|
88
|
+
return Array.isArray(parsed?.choices)
|
|
89
|
+
? parsed.choices
|
|
90
|
+
: null;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Classify a thrown client error (or a plain string this layer raised) into a
|
|
98
|
+
* stable shape on either tool surface.
|
|
99
|
+
*/
|
|
100
|
+
export function describeOperationError(input, surface = 'sdk') {
|
|
101
|
+
input = unwrapApiError(input);
|
|
102
|
+
const shape = classify(input);
|
|
103
|
+
const { choices } = thrownFields(input);
|
|
104
|
+
const recovery = typeof input === 'object' && input !== null
|
|
105
|
+
? recoveryIn(input.body)
|
|
106
|
+
: null;
|
|
107
|
+
let recoveryCursor;
|
|
108
|
+
if (shape.code === 'INBOX_OBSERVATION_LIMIT' && input && typeof input === 'object') {
|
|
109
|
+
try {
|
|
110
|
+
const body = JSON.parse(input.body ?? '{}');
|
|
111
|
+
if (typeof body.recoveryCursor === 'string' && /^ibxo1_[0-9a-f-]{36}$/.test(body.recoveryCursor))
|
|
112
|
+
recoveryCursor = body.recoveryCursor;
|
|
113
|
+
}
|
|
114
|
+
catch { /* Malformed recovery metadata does not hide the original refusal. */ }
|
|
115
|
+
}
|
|
116
|
+
const result = {
|
|
117
|
+
...shape,
|
|
118
|
+
...(recoveryCursor ? { recoveryCursor } : {}),
|
|
119
|
+
...(choices ? { choices } : {}),
|
|
120
|
+
...(recovery ? { recovery: typeof recovery.tool === 'string'
|
|
121
|
+
? { ...presentResourceCall(recovery, surface) }
|
|
122
|
+
: recovery } : {}),
|
|
123
|
+
};
|
|
124
|
+
if (recovery)
|
|
125
|
+
delete result.hint;
|
|
126
|
+
if (surface === 'sdk' && result.hint)
|
|
127
|
+
result.hint = result.hint.replace(/\bziggs_(\w+)/g, '$1');
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
function classify(input) {
|
|
131
|
+
const { message, status, code: machineCode } = thrownFields(input);
|
|
132
|
+
if (machineCode && SHARED_REFUSALS.has(machineCode)) {
|
|
133
|
+
return { code: machineCode, message };
|
|
134
|
+
}
|
|
135
|
+
if (machineCode === 'AGENT_LACKS_HUMAN_AUTHORITY') {
|
|
136
|
+
return {
|
|
137
|
+
code: 'AGENT_LACKS_HUMAN_AUTHORITY',
|
|
138
|
+
message,
|
|
139
|
+
hint: HUMAN_AUTHORITY_HINT,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// Can read the agreement, cannot work on it: the server's sentence and its
|
|
143
|
+
// recovery are the answer, and the generic scope hint would send the agent
|
|
144
|
+
// back for the read it already has.
|
|
145
|
+
if (machineCode === 'WORK_ACCESS_REQUIRED') {
|
|
146
|
+
return { code: machineCode, message };
|
|
147
|
+
}
|
|
148
|
+
if (machineCode && isCoverageRefusal(machineCode)) {
|
|
149
|
+
return { code: machineCode, message, hint: COVERAGE_HINT };
|
|
150
|
+
}
|
|
151
|
+
if (machineCode === 'NOT_AUTHORIZED_FOR_SCOPE') {
|
|
152
|
+
return {
|
|
153
|
+
code: 'NOT_AUTHORIZED_FOR_SCOPE',
|
|
154
|
+
message,
|
|
155
|
+
hint: SCOPE_DENIED_HINT,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
// The server owns the decision. A word such as "scope" may mean a token
|
|
159
|
+
// restriction, a resource grant, or a policy boundary; prose cannot tell us
|
|
160
|
+
// which request would authorize it. Preserve every supplied machine code.
|
|
161
|
+
return { code: machineCode ?? (status === null ? 'TOOL_ERROR' : codeForStatus(status)), message };
|
|
162
|
+
}
|
|
163
|
+
/** Tool runners wrap failures with context. Preserve the original API refusal. */
|
|
164
|
+
function unwrapApiError(input) {
|
|
165
|
+
const seen = new Set();
|
|
166
|
+
let current = input;
|
|
167
|
+
while (current && typeof current === 'object' && !seen.has(current)) {
|
|
168
|
+
seen.add(current);
|
|
169
|
+
const row = current;
|
|
170
|
+
if (typeof row.code === 'string' || typeof row.status === 'number' || typeof row.body === 'string')
|
|
171
|
+
return current;
|
|
172
|
+
current = row.cause;
|
|
173
|
+
}
|
|
174
|
+
return input;
|
|
175
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -526,3 +526,17 @@ export interface InboxPeek {
|
|
|
526
526
|
asOf: string;
|
|
527
527
|
count: number;
|
|
528
528
|
}
|
|
529
|
+
/** Private observation position, renewed on read for 24 idle hours; never pass to ack(). */
|
|
530
|
+
export interface InboxChangesOptions extends InboxReadOptions {
|
|
531
|
+
cursor?: string;
|
|
532
|
+
signal?: AbortSignal;
|
|
533
|
+
}
|
|
534
|
+
export interface InboxChangesResult {
|
|
535
|
+
asOf: string;
|
|
536
|
+
cursor: string;
|
|
537
|
+
expiresAt: string;
|
|
538
|
+
hasMore: boolean;
|
|
539
|
+
deliveries: Array<Pick<InboxDeliveryRef, 'eventId' | 'kind' | 'resourceId' | 'chatId' | 'taskId' | 'agreementId' | 'assigneeId' | 'needsHuman' | 'ts'> & {
|
|
540
|
+
orgId?: string | null;
|
|
541
|
+
}>;
|
|
542
|
+
}
|