@ziggs-ai/api-client 0.9.11 → 0.10.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.
@@ -0,0 +1,8 @@
1
+ import type { CapabilityDefinition } from './types.js';
2
+ export declare const agreementCommissionCapability: CapabilityDefinition;
3
+ export declare const agreementBidCapability: CapabilityDefinition;
4
+ export declare const agreementBrokerCapability: CapabilityDefinition;
5
+ export declare const agreementQuestCapability: CapabilityDefinition;
6
+ export declare const agreementOfferCapability: CapabilityDefinition;
7
+ export declare const agreementHandoffCapability: CapabilityDefinition;
8
+ export declare const AGREEMENT_VERB_CAPABILITIES: CapabilityDefinition[];
@@ -0,0 +1,305 @@
1
+ import { getAgreement, proposeBroadcast, proposeDirectTo, } from '../http/AgreementClient.js';
2
+ import { publishOffer } from '../http/MarketplaceClient.js';
3
+ import { fullCreds } from './types.js';
4
+ import { nextCall } from './nextCall.js';
5
+ /**
6
+ * One verb per shape of agreement, instead of one verb and a dispatch table.
7
+ *
8
+ * `agreement_propose` computed which of nine shapes you meant from a
9
+ * combination of `proposedTo`, `providerId`, `parentAgreementId` and
10
+ * `engagementKind`, then spent 420 words of description teaching a caller to
11
+ * reconstruct that same table in its head so it could pick the params that
12
+ * produced the shape it already knew it wanted. Roughly a tenth of the whole
13
+ * tool surface was one tool, and most of that tenth explained `providerId`
14
+ * permutations.
15
+ *
16
+ * The direction of work is in the verb name now, so `providerId` survives on
17
+ * exactly one verb — `agreement_broker` — where naming a third-party provider
18
+ * is the entire point rather than a puzzle. Every shape the old dispatch could
19
+ * reach still has a verb: nothing was dropped to make the list shorter.
20
+ *
21
+ * The count goes up, from one tool to six. That is the trade on purpose: the
22
+ * cost was never the number of tools, it was the number of decisions per call.
23
+ */
24
+ /** Terms every commercial agreement carries, whoever works and whoever pays. */
25
+ const TERMS_PARAMS = {
26
+ description: {
27
+ type: 'string',
28
+ required: true,
29
+ description: 'What the work is, in the words the counterparty will read.',
30
+ },
31
+ price: {
32
+ type: 'number',
33
+ description: 'Amount in CENTS — 500 means $5.00, and ϟ5.00 in the UI. Optional; recording a price does not itself move money. Convert BOTH ways or you are off by 100x: a human who says "ϟ2" means 200, and quoting 500 back as "ϟ500" is the same mistake inverted.',
34
+ },
35
+ billing: {
36
+ type: 'string',
37
+ enum: ['total', 'per_task'],
38
+ description: 'How price reads. total (default) is one price for the whole engagement, settled at fulfilment; per_task is a rate settled as each task completes (standing agreements only, and the default for a hire).',
39
+ },
40
+ engagementKind: {
41
+ type: 'string',
42
+ enum: ['service', 'hire'],
43
+ description: 'service (default) is one deliverable; hire is an ongoing engagement you keep spawning tasks under.',
44
+ },
45
+ lifecycle: {
46
+ type: 'string',
47
+ description: 'open (default) is standing: no expiry, unlimited tasks. Set this only when the engagement should end on its own.',
48
+ },
49
+ expiresAt: {
50
+ type: 'string',
51
+ description: 'ISO timestamp after which this can no longer be acted on.',
52
+ },
53
+ maxExecutions: {
54
+ type: 'number',
55
+ description: 'End the engagement after this many tasks.',
56
+ },
57
+ };
58
+ /** Pull the shared terms off a validated arg bag. */
59
+ function termsFrom(args) {
60
+ return {
61
+ description: args['description'],
62
+ ...(args['price'] === undefined ? {} : { price: args['price'] }),
63
+ ...(args['billing'] === undefined
64
+ ? {}
65
+ : { billing: args['billing'] }),
66
+ ...(args['lifecycle'] === undefined
67
+ ? {}
68
+ : { lifecycle: args['lifecycle'] }),
69
+ ...(args['expiresAt'] === undefined
70
+ ? {}
71
+ : { expiresAt: args['expiresAt'] }),
72
+ ...(args['maxExecutions'] === undefined
73
+ ? {}
74
+ : { maxExecutions: args['maxExecutions'] }),
75
+ };
76
+ }
77
+ function engagementKindFrom(args) {
78
+ return args['engagementKind'] ?? 'service';
79
+ }
80
+ /** The audience a broadcast reaches. */
81
+ const AUDIENCE_PARAM = {
82
+ type: 'string',
83
+ enum: ['everyone', 'org'],
84
+ required: true,
85
+ description: 'everyone is fully public; org is your active organisation only.',
86
+ };
87
+ const COUNTERPARTY_PARAM = {
88
+ type: 'string',
89
+ required: true,
90
+ description: 'Agent or user id of the counterparty.',
91
+ };
92
+ const CHAT_PARAM = {
93
+ type: 'string',
94
+ required: true,
95
+ description: 'The room this is proposed in — the counterparty reads it there.',
96
+ };
97
+ /** Marketplace follow-up: a published listing is claimed, never countered. */
98
+ function publishedNext(env) {
99
+ return [
100
+ nextCall(env, 'marketplace_view', undefined, 'see it listed alongside everything else on offer'),
101
+ ];
102
+ }
103
+ /* ─────────────────────────── direct, to one counterparty ─────────────────── */
104
+ export const agreementCommissionCapability = {
105
+ key: 'agreement_commission',
106
+ names: { sdk: 'agreement_commission', mcp: 'ziggs_agreement_commission' },
107
+ title: 'Commission a counterparty',
108
+ descriptions: {
109
+ sdk: 'Ask a named counterparty to do work your side pays for. They provide, you pay. For work you would do for them instead, use agreement_bid; to reach whoever is available rather than someone named, use agreement_quest.',
110
+ mcp: 'Ask a named counterparty to do work your side pays for: they provide, you pay. Prefer claiming their listing first if they have one (ziggs_marketplace_view then ziggs_agreement_claim) — listings are take-it-or-leave-it and most published agents refuse direct proposals. Use this for bespoke terms, renegotiation, or a counterparty with no listing. For work you would do for them, use ziggs_agreement_bid; to reach whoever is available, use ziggs_agreement_quest.',
111
+ },
112
+ annotation: 'write',
113
+ params: {
114
+ counterparty: COUNTERPARTY_PARAM,
115
+ chatId: CHAT_PARAM,
116
+ ...TERMS_PARAMS,
117
+ },
118
+ needsAgentId: true,
119
+ handler: async (args, env) => {
120
+ const counterparty = args['counterparty'];
121
+ const agreement = await proposeDirectTo({
122
+ ...termsFrom(args),
123
+ proposedTo: counterparty,
124
+ chatId: args['chatId'],
125
+ // They provide. The payer is derived server-side as the other side.
126
+ providerId: counterparty,
127
+ engagementKind: engagementKindFrom(args),
128
+ }, fullCreds(env));
129
+ return { agreement };
130
+ },
131
+ sdkOptions: { isAgreementCreation: true },
132
+ };
133
+ export const agreementBidCapability = {
134
+ key: 'agreement_bid',
135
+ names: { sdk: 'agreement_bid', mcp: 'ziggs_agreement_bid' },
136
+ title: 'Offer to work for a counterparty',
137
+ descriptions: {
138
+ sdk: 'Offer to do work for a named counterparty, who pays. You provide. To publish the same offer to whoever wants it rather than one named party, use agreement_offer.',
139
+ mcp: 'Offer to do work for a named counterparty, who pays: you provide. This is the direct form of a listing — to publish the same offer to whoever wants it, use ziggs_agreement_offer instead. To ask someone else to do work you pay for, use ziggs_agreement_commission.',
140
+ },
141
+ annotation: 'write',
142
+ params: {
143
+ counterparty: COUNTERPARTY_PARAM,
144
+ chatId: CHAT_PARAM,
145
+ ...TERMS_PARAMS,
146
+ },
147
+ needsAgentId: true,
148
+ handler: async (args, env) => {
149
+ const creds = fullCreds(env);
150
+ const agreement = await proposeDirectTo({
151
+ ...termsFrom(args),
152
+ proposedTo: args['counterparty'],
153
+ chatId: args['chatId'],
154
+ // You provide, so the counterparty pays.
155
+ providerId: creds.agentId,
156
+ engagementKind: engagementKindFrom(args),
157
+ }, creds);
158
+ return { agreement };
159
+ },
160
+ sdkOptions: { isAgreementCreation: true },
161
+ };
162
+ export const agreementBrokerCapability = {
163
+ key: 'agreement_broker',
164
+ names: { sdk: 'agreement_broker', mcp: 'ziggs_agreement_broker' },
165
+ title: 'Broker work between two other parties',
166
+ descriptions: {
167
+ sdk: 'Introduce a provider to a customer: the provider does the work, the counterparty pays, and neither side is you. The provider must already have a matching active offer.',
168
+ mcp: 'Introduce a provider to a customer: the named provider does the work, the counterparty pays, and you are neither. The provider needs a matching active offer for this to stand. This is the one verb where naming a provider is the point — every other agreement verb infers it from the direction of work.',
169
+ },
170
+ annotation: 'write',
171
+ params: {
172
+ counterparty: {
173
+ ...COUNTERPARTY_PARAM,
174
+ description: 'Who the work is done for, and who pays.',
175
+ },
176
+ provider: {
177
+ type: 'string',
178
+ required: true,
179
+ description: 'Agent id of the party who does the work.',
180
+ },
181
+ chatId: CHAT_PARAM,
182
+ ...TERMS_PARAMS,
183
+ },
184
+ needsAgentId: true,
185
+ handler: async (args, env) => {
186
+ const agreement = await proposeDirectTo({
187
+ ...termsFrom(args),
188
+ proposedTo: args['counterparty'],
189
+ chatId: args['chatId'],
190
+ providerId: args['provider'],
191
+ engagementKind: engagementKindFrom(args),
192
+ }, fullCreds(env));
193
+ return { agreement };
194
+ },
195
+ sdkOptions: { isAgreementCreation: true },
196
+ };
197
+ /* ──────────────────────────────── broadcast ──────────────────────────────── */
198
+ export const agreementQuestCapability = {
199
+ key: 'agreement_quest',
200
+ names: { sdk: 'agreement_quest', mcp: 'ziggs_agreement_quest' },
201
+ title: 'Post a quest',
202
+ descriptions: {
203
+ sdk: 'Post work you want done and will pay for, for whoever claims it. Whoever claims does the work. To offer work you would do instead, use agreement_offer.',
204
+ mcp: 'Post work you want done and will pay for: whoever claims it does the work. Use this when nothing already listed fits, rather than proposing to named counterparties one at a time. To offer work you would do, use ziggs_agreement_offer. Claimable via ziggs_agreement_claim; visible in ziggs_marketplace_view.',
205
+ },
206
+ annotation: 'write',
207
+ params: { audience: AUDIENCE_PARAM, ...TERMS_PARAMS },
208
+ needsAgentId: true,
209
+ handler: async (args, env) => {
210
+ const agreement = await proposeBroadcast({
211
+ ...termsFrom(args),
212
+ chatId: '',
213
+ audience: args['audience'],
214
+ engagementKind: engagementKindFrom(args),
215
+ }, fullCreds(env));
216
+ return { agreement, readPlan: publishedNext(env) };
217
+ },
218
+ sdkOptions: { isAgreementCreation: true },
219
+ };
220
+ export const agreementOfferCapability = {
221
+ key: 'agreement_offer',
222
+ names: { sdk: 'agreement_offer', mcp: 'ziggs_agreement_offer' },
223
+ title: 'Publish a standing offer',
224
+ descriptions: {
225
+ sdk: 'Publish work you will do, for whoever claims it and pays. You provide. This is your listing: it stands until revoked or exhausted. To offer one named counterparty instead, use agreement_bid.',
226
+ mcp: 'Publish work you will do, for whoever claims it and pays: you provide. This is your listing — it stands until revoked or exhausted, and claimers take it as posted rather than countering it. To offer one named counterparty instead, use ziggs_agreement_bid. To ask for work rather than supply it, use ziggs_agreement_quest.',
227
+ },
228
+ annotation: 'write',
229
+ params: { audience: AUDIENCE_PARAM, ...TERMS_PARAMS },
230
+ needsAgentId: true,
231
+ handler: async (args, env) => {
232
+ const terms = termsFrom(args);
233
+ const agreement = await publishOffer({
234
+ description: terms.description ?? '',
235
+ price: terms.price,
236
+ lifecycle: terms.lifecycle,
237
+ expiresAt: terms.expiresAt,
238
+ maxExecutions: terms.maxExecutions,
239
+ billing: terms.billing,
240
+ engagementKind: engagementKindFrom(args),
241
+ audience: args['audience'],
242
+ }, fullCreds(env));
243
+ return { agreement, readPlan: publishedNext(env) };
244
+ },
245
+ sdkOptions: { isAgreementCreation: true },
246
+ };
247
+ /* ──────────────────────────────── hand-off ───────────────────────────────── */
248
+ export const agreementHandoffCapability = {
249
+ key: 'agreement_handoff',
250
+ names: { sdk: 'agreement_handoff', mcp: 'ziggs_agreement_handoff' },
251
+ title: 'Hand off an agent you hired',
252
+ descriptions: {
253
+ sdk: 'Share an agent you have hired: the provider stays pinned, and whoever claims or approves becomes the customer the work is done for. Omit `to` to make it claimable by anyone. Handing off someone else\'s agent leaves that provider\'s approval pending — it must accept once before the hand-off can be claimed.',
254
+ mcp: 'Share an agent you have hired: the provider stays pinned, and whoever claims or approves is the CUSTOMER the work is done for, never the worker. Omit `to` to publish it claimable; name `to` and that party approves directly. On a priced hand-off the claimer is also the payer; omit price (or 0) and nobody is billed for their tasks. Handing off someone else\'s agent leaves that provider\'s approval pending — it must accept once before the hand-off can be claimed. The provider is read from the parent hire, so you do not pass it.',
255
+ },
256
+ annotation: 'write',
257
+ params: {
258
+ parentAgreementId: {
259
+ type: 'string',
260
+ required: true,
261
+ description: 'The ACTIVE hire you are handing off.',
262
+ },
263
+ to: {
264
+ type: 'string',
265
+ description: 'Who receives it. Omit to publish it claimable by anyone; name a party and they approve directly.',
266
+ },
267
+ ...TERMS_PARAMS,
268
+ description: {
269
+ type: 'string',
270
+ description: 'What the recipient is getting. Defaults to the parent hire.',
271
+ },
272
+ },
273
+ needsAgentId: true,
274
+ handler: async (args, env) => {
275
+ const creds = fullCreds(env);
276
+ const parentAgreementId = args['parentAgreementId'];
277
+ // Read the provider off the parent instead of asking for it. The caller
278
+ // would have had to look it up to pass it, and a lookup whose answer is
279
+ // unique is not a decision worth handing to the caller.
280
+ const parent = await getAgreement(parentAgreementId, creds);
281
+ const providerId = parent?.parties?.providerAgent;
282
+ if (!providerId) {
283
+ throw new Error(`Cannot hand off ${parentAgreementId}: it names no provider, so there is no hire to share. Check the id with agreement_get.`);
284
+ }
285
+ const terms = {
286
+ ...termsFrom(args),
287
+ description: args['description'] ?? parent.description ?? '',
288
+ parentAgreementId,
289
+ };
290
+ const to = args['to'];
291
+ const agreement = to
292
+ ? await proposeDirectTo({ ...terms, proposedTo: to, chatId: '', providerId, engagementKind: 'hire' }, creds)
293
+ : await proposeBroadcast({ ...terms, chatId: '', audience: 'everyone', providerId, engagementKind: 'hire' }, creds);
294
+ return { agreement, ...(to ? {} : { readPlan: publishedNext(env) }) };
295
+ },
296
+ sdkOptions: { isAgreementCreation: true },
297
+ };
298
+ export const AGREEMENT_VERB_CAPABILITIES = [
299
+ agreementCommissionCapability,
300
+ agreementBidCapability,
301
+ agreementBrokerCapability,
302
+ agreementQuestCapability,
303
+ agreementOfferCapability,
304
+ agreementHandoffCapability,
305
+ ];
@@ -9,6 +9,7 @@ import { fullCreds } from './types.js';
9
9
  export const agreementClaimCapability = {
10
10
  key: 'agreement_claim',
11
11
  names: { sdk: 'agreement_claim', mcp: 'ziggs_agreement_claim' },
12
+ title: 'Claim a posted agreement',
12
13
  descriptions: {
13
14
  sdk: 'Claim an open broadcast agreement by id — the DEFAULT way to engage: terms are already posted, consent is the claim, activation is instant, no negotiation turns. Claims a quest (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 and the agreement activates. A hand-off is claimable only after its provider has accepted (409 until then). Find quests/offers with marketplace_view; listings are take-it-or-leave-it — never counter one. Direct proposals are approved with agreement_respond instead, not claimed.',
14
15
  mcp: 'Claim an open broadcast agreement by id — the DEFAULT way to engage: terms are already posted, consent is the claim, activation is instant, no negotiation turns. Claims a quest (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 and the agreement activates. A hand-off is claimable only after its provider has accepted (409 until then). Find quests/offers with ziggs_marketplace_view; listings are take-it-or-leave-it — never counter one. Direct proposals are approved with ziggs_agreement_respond instead, not claimed. You cannot claim your own broadcast.',
@@ -20,6 +20,7 @@ function reportingHint(env, contentType, taskId) {
20
20
  export const recordArtifactCapability = {
21
21
  key: 'artifact_record',
22
22
  names: { sdk: 'artifact_record', mcp: 'ziggs_artifact_record' },
23
+ title: 'Record an artifact',
23
24
  descriptions: {
24
25
  sdk: 'Write an artifact. Scope is optional: pass agreementId or chatId to record it there, ' +
25
26
  'or pass no scope at all to record it as yours alone and attach it somewhere later. ' +
@@ -111,6 +112,7 @@ export const recordArtifactCapability = {
111
112
  export const listArtifactsCapability = {
112
113
  key: 'artifact_list',
113
114
  names: { sdk: 'artifact_list', mcp: 'ziggs_artifact_list' },
115
+ title: 'List artifacts you wrote',
114
116
  descriptions: {
115
117
  sdk: 'List artifacts you authored, in any scope or none — including free-standing ones no scope read can reach. Forward-delta with `after`.',
116
118
  mcp: 'List artifacts YOU authored, across every scope and none. Use this to find something you ' +
@@ -148,6 +150,7 @@ export const listArtifactsCapability = {
148
150
  export const shareArtifactCapability = {
149
151
  key: 'artifact_share',
150
152
  names: { sdk: 'artifact_share', mcp: 'ziggs_artifact_share' },
153
+ title: 'Share an artifact',
151
154
  descriptions: {
152
155
  sdk: 'Share an artifact you authored with another agent — just that artifact, no chat or agreement. An agent already connected to you (same owner, same org, or an active link) gets it immediately; anyone else is offered to your owner to approve.',
153
156
  mcp: 'Share ONE artifact you authored with another agent, without sharing any chat or agreement ' +
@@ -214,6 +217,7 @@ export const shareArtifactCapability = {
214
217
  export const attachArtifactCapability = {
215
218
  key: 'artifact_attach',
216
219
  names: { sdk: 'artifact_attach', mcp: 'ziggs_artifact_attach' },
220
+ title: 'Attach an artifact to a chat or task',
217
221
  descriptions: {
218
222
  sdk: 'Attach an artifact you can read to a chat or a task. Everyone in that chat / party to that task can read it from then on. Use artifact_share to give it to one specific agent instead.',
219
223
  mcp: 'Attach an existing artifact to a chat or a task — the way a free-standing artifact (one you ' +
@@ -276,6 +280,7 @@ export const attachArtifactCapability = {
276
280
  export const uploadArtifactUrlCapability = {
277
281
  key: 'artifact_upload_url',
278
282
  names: { sdk: 'artifact_upload_url', mcp: 'ziggs_artifact_upload_url' },
283
+ title: 'Start a file upload',
279
284
  descriptions: {
280
285
  sdk: 'Start a file artifact upload. Returns a short-lived uploadUrl. PUT the exact ' +
281
286
  'byteSize bytes to that URL (Content-Type = mime), then call artifact_complete_file ' +
@@ -372,6 +377,7 @@ export const uploadArtifactUrlCapability = {
372
377
  export const completeArtifactFileCapability = {
373
378
  key: 'artifact_complete_file',
374
379
  names: { sdk: 'artifact_complete_file', mcp: 'ziggs_artifact_complete_file' },
380
+ title: 'Finish a file upload',
375
381
  descriptions: {
376
382
  sdk: 'Finish a file upload after the S3 PUT. Pass artifactId + sha256 hex of the bytes ' +
377
383
  'you uploaded. Sets extractionStatus=pending and enqueues extract. Poll artifact_list ' +
@@ -410,6 +416,7 @@ export const completeArtifactFileCapability = {
410
416
  export const downloadArtifactCapability = {
411
417
  key: 'artifact_download',
412
418
  names: { sdk: 'artifact_download', mcp: 'ziggs_artifact_download' },
419
+ title: 'Download a file artifact',
413
420
  descriptions: {
414
421
  sdk: 'Get a short-lived (60s) presigned download URL for a file artifact you can read. ' +
415
422
  'Returns downloadUrl, expiresAt, filename — fetch the URL yourself; do not expect ' +
@@ -435,6 +442,7 @@ export const downloadArtifactCapability = {
435
442
  export const reextractArtifactCapability = {
436
443
  key: 'artifact_reextract',
437
444
  names: { sdk: 'artifact_reextract', mcp: 'ziggs_artifact_reextract' },
445
+ title: 'Re-extract text from a file',
438
446
  descriptions: {
439
447
  sdk: 'Re-queue text extraction for a file artifact you authored when status is ok or failed ' +
440
448
  '(pending → conflict). On success a new extracted-text row supersedes the previous one.',
@@ -11,9 +11,10 @@ import { fullCreds } from './types.js';
11
11
  export const openConversationCapability = {
12
12
  key: 'chat_open',
13
13
  names: { sdk: 'chat_open', mcp: 'ziggs_chat_open' },
14
+ title: 'Open a chat',
14
15
  descriptions: {
15
- sdk: 'Open or reuse a chat with a user or agent participant. Calling it again for the same participant returns the SAME chat, so it is how you find the conversation you already have with someone — pass newChat only when this really is a separate subject. To reach an agent in ANOTHER org, establish a link first — agreement_propose with engagementKind "link" (if you have its agent id) or link_create_invite (if you do not), approved/claimed — otherwise this fails with AGENT_NOT_PUBLISHED. To list chats you can already read, use grant_list scopeKind=chat.',
16
- mcp: 'Open or reuse a chat with a user or agent participant. Calling it again for the same participant returns the SAME chat; pass newChat only when this really is a separate subject. To reach an agent in ANOTHER org, an unpublished agent must establish a link first — ziggs_agreement_propose with engagementKind "link" (if you have its agent id) or ziggs_link_create_invite (if you do not), approved/claimed — otherwise this fails with AGENT_NOT_PUBLISHED.',
16
+ sdk: 'Open or reuse a chat with a user or agent participant. Calling it again for the same participant returns the SAME chat, so it is how you find the conversation you already have with someone — pass newChat only when this really is a separate subject. To reach an agent in ANOTHER org, establish a link first — link_propose (if you have its agent id) or link_create_invite (if you do not), approved/claimed — otherwise this fails with AGENT_NOT_PUBLISHED. To list chats you can already read, use grant_list scopeKind=chat.',
17
+ mcp: 'Open or reuse a chat with a user or agent participant. Calling it again for the same participant returns the SAME chat; pass newChat only when this really is a separate subject. To reach an agent in ANOTHER org, an unpublished agent must establish a link first — ziggs_link_propose (if you have its agent id) or ziggs_link_create_invite (if you do not), approved/claimed — otherwise this fails with AGENT_NOT_PUBLISHED.',
17
18
  },
18
19
  annotation: 'write',
19
20
  params: {
@@ -9,11 +9,15 @@ function client(env) {
9
9
  export const connectionProxyCapability = {
10
10
  key: 'connection_proxy',
11
11
  names: { sdk: 'connection_proxy', mcp: 'ziggs_connection_proxy' },
12
+ title: 'Use a stored connection',
12
13
  descriptions: {
13
- sdk: "Use a stored connection (a third-party credential, e.g. the owner's GitHub/Jira) without ever seeing the credential. Calls the backend connections proxy with a grant the owner issued to this agent: the proxy enforces the grant, decrypts the token server-side, makes the upstream provider call, and returns the result (token-leak guarded on both sides). Provide connectionId, grantId, the provider action (e.g. repo:read), and an optional action-specific payload. Don't know connectionId/grantId yet? Use grant_list (scopeKind=connection) or connection_list_grants first.",
14
- mcp: "Use a stored connection (a third-party credential, e.g. the owner's GitHub/Jira — NOT an agent-to-agent Link, see ziggs_link_list for that) without ever seeing the credential. " +
15
- 'Calls the backend connections proxy with a grant the owner issued to this agent: the proxy enforces the grant, decrypts the token server-side, makes the upstream provider call, and returns the result (token-leak guarded on both sides). ' +
16
- 'Provide connectionId, grantId, the provider action (e.g. repo:read), and an optional action-specific payload. ' +
14
+ sdk: "Use a named-connector stored connection (e.g. the owner's GitHub/Jira) without ever seeing the credential. " +
15
+ 'Calls the backend connections proxy with a grant the owner issued to this agent. ' +
16
+ 'Not for remote MCP servers (provider "mcp") — those use mcp_tool_call / mcp_tools_list; proxy refuses them with "Unknown provider: mcp". ' +
17
+ "Don't know connectionId/grantId yet? Use grant_list (scopeKind=connection) or connection_list_grants first.",
18
+ mcp: "Use a named-connector stored connection (e.g. the owner's GitHub/Jira — NOT an agent-to-agent Link, see ziggs_link_list) without ever seeing the credential. " +
19
+ 'Calls the backend connections proxy with a grant the owner issued to this agent. ' +
20
+ 'Not for remote MCP servers (provider "mcp") — those use ziggs_mcp_tool_call / ziggs_mcp_tools_list; proxy refuses them with "Unknown provider: mcp". ' +
17
21
  "Don't know connectionId/grantId yet? Call ziggs_connection_list first.",
18
22
  },
19
23
  annotation: 'write',
@@ -52,11 +56,14 @@ export const connectionProxyCapability = {
52
56
  export const requestConnectionCapability = {
53
57
  key: 'connection_request',
54
58
  names: { sdk: 'connection_request', mcp: 'ziggs_connection_request' },
59
+ title: 'Ask your principal for a connection',
55
60
  descriptions: {
56
- sdk: 'Ask your principal (the human) to connect a remote MCP server and grant you the listed tools. Opens a connection-consent agreement as an approvable card in the chat you pass — the human approves it there like any other agreement. On approval the server is connected (browser OAuth if needed) and you are granted the tools; use them via connection_proxy.',
61
+ sdk: 'Ask your principal (the human) to connect a remote MCP server and grant you the listed tools. ' +
62
+ 'Opens a connection-consent agreement as an approvable card in the chat you pass — the human approves it there like any other agreement. ' +
63
+ 'On approval the server is connected (browser OAuth if needed) and you are granted the tools; call them with mcp_tool_call / mcp_tools_list (not connection_proxy).',
57
64
  mcp: 'Ask your principal (the human) to connect a remote MCP server and grant you the listed tools. ' +
58
65
  'Opens a connection-consent agreement as an approvable card in the chat you pass — the human approves it there like any other agreement (there is no MCP tool to approve it, so tell them to approve it in the chat). ' +
59
- 'On approval the server is connected (browser OAuth if needed) and you are granted the tools; the result shows up in ziggs_connection_list for use with ziggs_connection_proxy.',
66
+ 'On approval the server is connected (browser OAuth if needed) and you are granted the tools; the result shows up in ziggs_connection_list for use with ziggs_mcp_tools_list / ziggs_mcp_tool_call (not ziggs_connection_proxy).',
60
67
  },
61
68
  annotation: 'write',
62
69
  params: {
@@ -96,9 +103,9 @@ export const requestConnectionCapability = {
96
103
  ...result,
97
104
  note: env.surface === 'mcp'
98
105
  ? 'A connection-consent card is now in the chat awaiting your principal. Tell the human now (pull-only MCP has no push) — they approve it right in the chat. ' +
99
- 'Once approved, the connection + grant appear in ziggs_connection_list for ziggs_connection_proxy.'
106
+ 'Once approved, the connection + grant appear in ziggs_connection_list for ziggs_mcp_tools_list / ziggs_mcp_tool_call.'
100
107
  : 'A connection-consent card is now in the chat awaiting your principal — they approve it right there. ' +
101
- 'Once approved, the connection + grant appear in grant_list (scopeKind=connection) / connection_list_grants for connection_proxy.',
108
+ 'Once approved, the connection + grant appear in grant_list (scopeKind=connection) / connection_list_grants for mcp_tools_list / mcp_tool_call.',
102
109
  };
103
110
  }
104
111
  catch (e) {
@@ -60,6 +60,7 @@ const ARTIFACT_VIA_NOTE = 'via=artifact:<id> is a point read of one named artifa
60
60
  export const contextReadCapability = {
61
61
  key: 'context_read',
62
62
  names: { sdk: 'context_read', mcp: 'ziggs_context_read' },
63
+ title: 'Read context you hold',
63
64
  descriptions: {
64
65
  sdk: `Read the contents of a scope you already hold a grant for: ${CONTEXT_READ_TYPES.join(' | ')}. Each type reads through its own entries — ${VIA_BY_TYPE}. Use grant_list first to see which scopes your grants cover, then read through any of them. ${ARTIFACT_VIA_NOTE}. Cursored; all access is grant-fenced server-side.`,
65
66
  mcp: `Read the contents of a scope you already hold: ${CONTEXT_READ_TYPES.join(' | ')} (the type param). Each type accepts its own via entries — ${VIA_BY_TYPE} — and any other pairing is refused. ${ARTIFACT_VIA_NOTE} (e.g. an artifact someone shared with you; ziggs_grant_list scopeKind=artifact shows those). Forward-delta with after+direction=forward; cursor pagination; contextGrantId pins a grant. The response carries a \`readPlan\` with the next page and/or forward-delta call pre-filled (after=this page's latestSequence), so you can keep reading without rebuilding args. This is the single read path for all four types — to discover which scopes exist, use the listers: ziggs_chat_list, ziggs_task_list, ziggs_agreement_list, ziggs_grant_list, ziggs_link_list.`,
@@ -129,6 +130,7 @@ export const contextReadCapability = {
129
130
  export const contextExpandReachCapability = {
130
131
  key: 'context_expand_reach',
131
132
  names: { sdk: 'context_expand_reach', mcp: 'ziggs_context_expand_reach' },
133
+ title: 'See what a grant reaches',
132
134
  descriptions: {
133
135
  sdk: 'Expand a grant you hold into the chat/agreement ids inside its scope, so you can read through it. grant_list tells you that you hold e.g. org:acme or agreement:x; this returns the actual { chats, agreements } (ids + labels only, no content) that scope covers — feed an id to context_read (via=chat:<id> / agreement:<id>). Org scope is capped: truncatedChats/truncatedAgreements say how many were left off. An artifact-scope grant returns empty by design — the grant IS one artifact, so read it directly with context_read via=artifact:<the scope id>. Holder-only, grant-fenced.',
134
136
  mcp: 'Expand a grant you hold into the chat/agreement ids inside its scope, so you can actually read through it. ziggs_grant_list tells you that you hold e.g. org:acme or agreement:x; this returns the { chats, agreements } (ids + labels only, never content) that scope covers — feed an id to ziggs_context_read (via=chat:<id> / agreement:<id>). Org scope is capped: truncatedChats/truncatedAgreements say how many were left off. An artifact-scope grant returns empty by design — that grant IS a single artifact, so do not read the empty map as a dead grant: read the artifact with ziggs_context_read via=artifact:<the scope id>. Holder-only, grant-fenced.',
@@ -153,6 +155,7 @@ export const contextExpandReachCapability = {
153
155
  export const contextDiscoverGrantableCapability = {
154
156
  key: 'context_discover_grantable',
155
157
  names: { sdk: 'context_discover_grantable', mcp: 'ziggs_context_discover_grantable' },
158
+ title: 'Find context you cannot read yet',
156
159
  descriptions: {
157
160
  sdk: 'See what context EXISTS in orgs you actively work in that you CANNOT read yet — the inverse of grant_list. Covers chats, agreements, and connections (type is one of "chat" | "agreement" | "connection"; connection labels are the provider name only). Returns labels only ({ type, label, scopeRef, orgId } per item), never content, member names, tokens, or money. Bounded to orgs you hold an active WORK agreement in (hire/service; a link to a peer org does not open that org), and excludes anything you already hold a grant for. Use it to notice you may be missing context, then either ask your human to grant a scopeRef, or (if you hold a broader grant) context_delegate using that scopeRef. Pair with grant_list (what you hold) and context_expand_reach (what a held grant covers).',
158
161
  mcp: 'See what context EXISTS in orgs you actively work in that you CANNOT read yet — so you can ask for it instead of failing blind. Covers chats, agreements, and connections (type is "chat" | "agreement" | "connection"; connection labels are the provider name only). Returns labels only: { type, label, scopeRef, orgId } per item, never content, member names, tokens, or money. Bounded to orgs you hold an active WORK agreement in (hire/service; a link to a peer org does not open that org). To act on one, ask your human to grant it, or (if you hold a broader grant of your own) delegate via ziggs_context_delegate using the scopeRef. Use ziggs_grant_list for what you already hold; this is what you lack.',
@@ -169,6 +172,7 @@ export const contextDiscoverGrantableCapability = {
169
172
  export const contextDelegateCapability = {
170
173
  key: 'context_delegate',
171
174
  names: { sdk: 'context_delegate', mcp: 'ziggs_context_delegate' },
175
+ title: 'Pass on a narrower grant',
172
176
  descriptions: {
173
177
  sdk: 'Delegate a narrower child context grant (tighter scope, shorter expiry, or from-now). Holder-only. Delegating a grant whose original owner is another party opens an approval request rather than minting immediately (status: pending_approval).',
174
178
  mcp: 'Delegate a narrower child grant from one you hold (POST /context/grants/:id/delegate). Delegation only narrows scope/expiry/temporal — never broadens. If the grant\'s original owner is a different party, this does NOT grant — it opens a request that owner must approve, and returns { status: "pending_approval", agreementId }; surface that to the human and do not treat it as done.',
@@ -3,9 +3,10 @@ import { fullCreds } from './types.js';
3
3
  export const agentSearchCapability = {
4
4
  key: 'agent_search',
5
5
  names: { sdk: 'agent_search', mcp: 'ziggs_agent_search' },
6
+ title: 'Search for agents',
6
7
  descriptions: {
7
8
  sdk: 'Search for agents by capability, name, or description. Returns ranked results with relevance scores. A keyword/natural-language query searches the published store AND, scoped to your authority, your own org-mates and any delegate you have an active link with. Passing an EXACT agent id resolves that one agent even if unpublished/private. Each row carries `doors` — its engagement doors: doors.listingAgreementId is a live listing to CLAIM (agreement_claim — the default, one hop from here) and doors.acceptsProposals says whether a direct proposal would even be accepted (most published agents are claim-only). Use returned agentId in grant/issue tools — do not guess ids.',
8
- mcp: 'Find agents (AgentSearchClient). A keyword/natural-language query searches the published store AND, scoped to your authority, your own org-mates and any delegate you have an active link with — so you can find a teammate or another user\'s delegate by name and ziggs_chat_open with it directly, even if it is unpublished/offline and has never been in a chat with you. Passing an EXACT agent id resolves that one agent even if unpublished/private — use this for a delegate someone shared an id for, then propose a link (ziggs_agreement_propose engagementKind="link") if not yet linked. Each row carries `doors` — its engagement doors: doors.listingAgreementId is a live listing to CLAIM (ziggs_agreement_claim — the default, one hop from here) and doors.acceptsProposals says whether a direct proposal would even be accepted (most published agents are claim-only). Each result carries a per-row `reachability` field derived from HOW you can reach it — `published` (store directory), `same-org`, `linked`, or `managed`; it is not a blanket "published" label. If an exact-id lookup matches an unpublished agent you cannot reach, the row is `reachability: "restricted"` and returns the id only with no name/profile. Use returned agentId in grant/issue tools — do not guess ids.',
9
+ mcp: 'Find agents (AgentSearchClient). A keyword/natural-language query searches the published store AND, scoped to your authority, your own org-mates and any delegate you have an active link with — so you can find a teammate or another user\'s delegate by name and ziggs_chat_open with it directly, even if it is unpublished/offline and has never been in a chat with you. Passing an EXACT agent id resolves that one agent even if unpublished/private — use this for a delegate someone shared an id for, then propose a link (ziggs_link_propose) if not yet linked. Each row carries `doors` — its engagement doors: doors.listingAgreementId is a live listing to CLAIM (ziggs_agreement_claim — the default, one hop from here) and doors.acceptsProposals says whether a direct proposal would even be accepted (most published agents are claim-only). Each result carries a per-row `reachability` field derived from HOW you can reach it — `published` (store directory), `same-org`, `linked`, or `managed`; it is not a blanket "published" label. If an exact-id lookup matches an unpublished agent you cannot reach, the row is `reachability: "restricted"` and returns the id only with no name/profile. Use returned agentId in grant/issue tools — do not guess ids.',
9
10
  },
10
11
  annotation: 'read-only',
11
12
  params: {
@@ -49,6 +50,7 @@ export const agentSearchCapability = {
49
50
  export const agentGetCapability = {
50
51
  key: 'agent_get',
51
52
  names: { sdk: 'agent_get', mcp: 'ziggs_agent_get' },
53
+ title: 'Read an agent profile',
52
54
  descriptions: {
53
55
  sdk: 'Fetch the full profile of a specific agent by ID — name, description, tags, capabilities, reachability, reliability, and `doors` (engagement doors: doors.listingAgreementId to CLAIM — the default engagement — and doors.acceptsProposals for whether a direct proposal is even accepted). Use to confirm capabilities and terms before engaging, when you already hold the agent id. An id you cannot reach returns reachability "restricted" (id only, no profile). To find an agent by keyword instead, use agent_search.',
54
56
  mcp: 'Fetch the full profile of ONE agent by its exact id (GET /agents/:id) — name, description, tags, capabilities, reachability, reliability, and `doors` (engagement doors: doors.listingAgreementId to CLAIM via ziggs_agreement_claim — the default engagement — and doors.acceptsProposals for whether a direct proposal is even accepted). Use to confirm a candidate before engaging, when you already hold the agent id (from ziggs_agent_search, a grant, or an agreement party). Grant-scoped: an id you cannot reach returns reachability "restricted" (id only, no profile). To find an agent by keyword instead, use ziggs_agent_search.',
@@ -37,6 +37,7 @@ function parseScopeKinds(raw) {
37
37
  export const listGrantsCapability = {
38
38
  key: 'grant_list',
39
39
  names: { sdk: 'grant_list', mcp: 'ziggs_grant_list' },
40
+ title: 'List grants you hold',
40
41
  descriptions: {
41
42
  sdk: 'List grants this agent holds — or, with role=issuer, grants this agent caused (author-shares and delegate children). Context (chat/agreement/org/artifact), connection, and wallet — canonical grants (grantId, scope, caveats, expiresAt, health; no content or credentials). Default role=holder answers "what grants do I HOLD?", which is narrower than "what can I reach?": authorship, chat membership, agreement party, and org membership leave no grant row — an empty holder list means "no grants", never "no access". role=issuer answers "who did I share with / what did I mint?" after artifact_share. Filter by scopeKind, scopeId, and health (defaults to active). Rails you lack the operator-key read scope for are named in unreadableRails. Cursor-paginated.',
42
43
  mcp: 'List grants this delegate holds — or, with role=issuer, grants this delegate caused (author-shares and delegate children). Context (chat/agreement/org/artifact), connection, and wallet — canonical grants (grantId, scope, caveats, expiresAt, health; no content or credentials). Default role=holder answers "what grants do I HOLD?", which is narrower than "what can I reach?": authorship, chat membership, agreement party, and org membership leave no grant row — an empty holder list means "no grants", never "no access". After ziggs_artifact_share, pass role=issuer (and usually scopeKind=["artifact"]) to recover grantIds and revoke with ziggs_context_revoke_grant. Filter by scopeKind, scopeId, and health (defaults to active). Rails you lack the operator-key read scope for are named in unreadableRails. Cursor-paginated.',
@@ -1,6 +1,8 @@
1
1
  export { type CapabilitySurface, type CapabilityAnnotation, type CapabilityParam, type CapabilityEnv, type CapabilityDefinition, fullCreds, rethrowWithContext, } from './types.js';
2
+ export { nextCall, peerAgentId, type NextCall } from './nextCall.js';
3
+ export { AGREEMENT_VERB_CAPABILITIES, agreementCommissionCapability, agreementBidCapability, agreementBrokerCapability, agreementQuestCapability, agreementOfferCapability, agreementHandoffCapability, } from './agreementVerbs.js';
2
4
  export { PAYMENT_CAPABILITIES, paymentBalanceCapability, paymentTransferCapability, paymentWaitForApprovalCapability, paymentHoldCapability, paymentReleaseCapability, paymentResolveWalletCapability, paymentIssueGrantCapability, paymentAttenuateGrantCapability, paymentRevokeGrantCapability, } from './payments.js';
3
- export { LINK_CAPABILITIES, createLinkInviteCapability, listLinksCapability, linkIsReachOnly, } from './links.js';
5
+ export { LINK_CAPABILITIES, createLinkInviteCapability, listLinksCapability, proposeLinkCapability, linkIsReachOnly, } from './links.js';
4
6
  export { AGREEMENT_CAPABILITIES, agreementClaimCapability } from './agreements.js';
5
7
  export { AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION } from './proposeProviderId.js';
6
8
  export { MARKETPLACE_CAPABILITIES, marketplaceViewCapability } from './marketplace.js';
@@ -1,6 +1,8 @@
1
1
  export { fullCreds, rethrowWithContext, } from './types.js';
2
+ export { nextCall, peerAgentId } from './nextCall.js';
3
+ export { AGREEMENT_VERB_CAPABILITIES, agreementCommissionCapability, agreementBidCapability, agreementBrokerCapability, agreementQuestCapability, agreementOfferCapability, agreementHandoffCapability, } from './agreementVerbs.js';
2
4
  export { PAYMENT_CAPABILITIES, paymentBalanceCapability, paymentTransferCapability, paymentWaitForApprovalCapability, paymentHoldCapability, paymentReleaseCapability, paymentResolveWalletCapability, paymentIssueGrantCapability, paymentAttenuateGrantCapability, paymentRevokeGrantCapability, } from './payments.js';
3
- export { LINK_CAPABILITIES, createLinkInviteCapability, listLinksCapability, linkIsReachOnly, } from './links.js';
5
+ export { LINK_CAPABILITIES, createLinkInviteCapability, listLinksCapability, proposeLinkCapability, linkIsReachOnly, } from './links.js';
4
6
  export { AGREEMENT_CAPABILITIES, agreementClaimCapability } from './agreements.js';
5
7
  export { AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION } from './proposeProviderId.js';
6
8
  export { MARKETPLACE_CAPABILITIES, marketplaceViewCapability } from './marketplace.js';
@@ -12,4 +12,14 @@ import { type CapabilityDefinition, type CapabilityEnv } from './types.js';
12
12
  export declare function linkIsReachOnly(env: CapabilityEnv): string;
13
13
  export declare const createLinkInviteCapability: CapabilityDefinition;
14
14
  export declare const listLinksCapability: CapabilityDefinition;
15
+ /**
16
+ * A link proposed to an agent you can name.
17
+ *
18
+ * This used to ride the propose grammar as `engagementKind: "link"`, which put
19
+ * bilateral trust on the same verb as commercial terms it has none of: no
20
+ * price, no chat, no work. It belongs here, beside the invite and the list, so
21
+ * links have one rail. `link_create_invite` is the same act when you do NOT
22
+ * have an agent id and need a shareable page instead.
23
+ */
24
+ export declare const proposeLinkCapability: CapabilityDefinition;
15
25
  export declare const LINK_CAPABILITIES: CapabilityDefinition[];
@@ -1,4 +1,5 @@
1
1
  import { createAgreement, listAgreements } from '../http/AgreementClient.js';
2
+ import { nextCall, peerAgentId } from './nextCall.js';
2
3
  import { fullCreds } from './types.js';
3
4
  const DEFAULT_WEB_URL = 'https://ziggsai.com';
4
5
  function webAppOrigin(env) {
@@ -43,14 +44,15 @@ const LINK_STATUSES = ['active', 'open', 'cancelled', 'all'];
43
44
  const MAX_LINK_INVITE_CLAIMS = 25;
44
45
  ///1022 — the link rail shrank to two tools. Links are agreements, so
45
46
  // the agreement verbs carry the rest: request a direct link with
46
- // agreement_propose (engagementKind 'link', proposedTo = the agent id), claim
47
+ // link_propose (counterparty = the agent id), claim
47
48
  // an invite with agreement_claim, end a link with agreement_revoke.
48
49
  export const createLinkInviteCapability = {
49
50
  key: 'link_create_invite',
50
51
  names: { sdk: 'link_create_invite', mcp: 'ziggs_link_create_invite' },
52
+ title: 'Create a link invite',
51
53
  descriptions: {
52
- sdk: "Create a shareable OPEN link invite (bilateral agent-to-agent trust) when you do NOT have the counterparty's agent id (e.g. connecting across orgs). Creates an open link agreement proposed to everyone and returns shareUrl — one public page that is the entire invite, for a person or for their assistant. Set maxClaims to let several people claim the same link — each gets their own separate connection. When you DO have the agent id, propose the link directly instead: agreement_propose with engagementKind \"link\" and proposedTo = that id.",
53
- mcp: 'Create a shareable OPEN link invite (bilateral agent-to-agent trust, NOT a third-party service connection — see ziggs_connection_list for that) when you do NOT have the counterparty\'s agent id (e.g. connecting across orgs). Creates an open link agreement (POST /agreements {engagementKind:"link"}, proposedTo:"everyone") and returns shareUrl: one public page that is the entire invite — the recipient accepts from it with no account, and their assistant can read the connect instructions off the same URL. Give the human that link and nothing else. Set maxClaims to share ONE link with several people; each claimer gets their own separate connection, not a group. Revoke via ziggs_agreement_revoke to disable. When you DO have the agent id, propose the link directly instead: ziggs_agreement_propose with engagementKind "link" and proposedTo = that id.',
54
+ sdk: "Create a shareable OPEN link invite (bilateral agent-to-agent trust) when you do NOT have the counterparty's agent id (e.g. connecting across orgs). Creates an open link agreement proposed to everyone and returns shareUrl — one public page that is the entire invite, for a person or for their assistant. Set maxClaims to let several people claim the same link — each gets their own separate connection. When you DO have the agent id, propose the link directly instead: link_propose with counterparty = that id.",
55
+ mcp: 'Create a shareable OPEN link invite (bilateral agent-to-agent trust, NOT a third-party service connection — see ziggs_connection_list for that) when you do NOT have the counterparty\'s agent id (e.g. connecting across orgs). Creates an open link agreement (POST /agreements {engagementKind:"link"}, proposedTo:"everyone") and returns shareUrl: one public page that is the entire invite — the recipient accepts from it with no account, and their assistant can read the connect instructions off the same URL. Give the human that link and nothing else. Set maxClaims to share ONE link with several people; each claimer gets their own separate connection, not a group. Revoke via ziggs_agreement_revoke to disable. When you DO have the agent id, propose the link directly instead: ziggs_link_propose with counterparty = that id.',
54
56
  },
55
57
  annotation: 'write',
56
58
  params: {
@@ -93,9 +95,10 @@ export const createLinkInviteCapability = {
93
95
  export const listLinksCapability = {
94
96
  key: 'link_list',
95
97
  names: { sdk: 'link_list', mcp: 'ziggs_link_list' },
98
+ title: 'List your links',
96
99
  descriptions: {
97
- sdk: 'List link agreements for this agent — bilateral agent-to-agent trust relationships (GET /agreements?engagementKind=link). Any agent can link with any other agent. Defaults to ACTIVE links only; pass status to see pending proposals ("open") or revoked ones ("cancelled"). Each item is a link summary: agreementId, status, proposalStatus, parties. Request a new link with agreement_propose (engagementKind "link"), or link_create_invite when you lack the agent id; end one with agreement_revoke.',
98
- mcp: 'List link agreements for this agent — bilateral agent-to-agent trust relationships, NOT third-party service connections (see ziggs_connection_list for those) (GET /agreements?engagementKind=link). Any agent can link with any other agent. Defaults to ACTIVE links only; pass status to see pending proposals ("open") or revoked ones ("cancelled"). Each item is a link summary: agreementId, status, proposalStatus, parties.creatorAgent (requester), parties.providerAgent (target), parties.proposedTo (target owner). Approve pending links via ziggs_agreement_respond; request a new one with ziggs_agreement_propose (engagementKind "link"), or ziggs_link_create_invite when you lack the agent id; end one with ziggs_agreement_revoke.',
100
+ sdk: 'List link agreements for this agent — bilateral agent-to-agent trust relationships (GET /agreements?engagementKind=link). Any agent can link with any other agent. Defaults to ACTIVE links only; pass status to see pending proposals ("open") or revoked ones ("cancelled"). Each item is a link summary: agreementId, status, proposalStatus, parties. Request a new link with link_propose, or link_create_invite when you lack the agent id; end one with agreement_revoke.',
101
+ mcp: 'List link agreements for this agent — bilateral agent-to-agent trust relationships, NOT third-party service connections (see ziggs_connection_list for those) (GET /agreements?engagementKind=link). Any agent can link with any other agent. Defaults to ACTIVE links only; pass status to see pending proposals ("open") or revoked ones ("cancelled"). Each item is a link summary: agreementId, status, proposalStatus, parties.creatorAgent (requester), parties.providerAgent (target), parties.proposedTo (target owner). Approve pending links via ziggs_agreement_respond; request a new one with ziggs_link_propose, or ziggs_link_create_invite when you lack the agent id; end one with ziggs_agreement_revoke.',
99
102
  },
100
103
  annotation: 'read-only',
101
104
  params: {
@@ -115,17 +118,87 @@ export const listLinksCapability = {
115
118
  engagementKind: 'link',
116
119
  ...(status === 'all' ? {} : { status }),
117
120
  }, fullCreds(env));
118
- const hasActive = links.some((a) => a.status === 'active');
121
+ const active = links.filter((a) => a.status === 'active');
122
+ // A link grants reach and nothing else, so the move after seeing one is
123
+ // always the same: open a room with that peer, or share context explicitly.
124
+ // Both were named in prose, and the peer id had to be dug out of
125
+ // `parties` by whoever read it — while being right here.
126
+ const readPlan = [];
127
+ for (const link of active.slice(0, 3)) {
128
+ const peer = peerAgentId(link.parties, env.creds.agentId);
129
+ if (!peer)
130
+ continue;
131
+ readPlan.push(nextCall(env, 'chat_open', { participantId: peer }, 'open a room with this linked peer — a link alone carries no context'));
132
+ }
133
+ if (active.length) {
134
+ readPlan.push(nextCall(env, 'context_issue_grant', undefined, 'share context that already exists: a link does not share any on its own'));
135
+ }
119
136
  return {
120
137
  count: links.length,
121
138
  status,
122
139
  links,
123
- ...(hasActive ? { nextSteps: linkIsReachOnly(env) } : {}),
140
+ ...(readPlan.length ? { readPlan } : {}),
141
+ ...(active.length ? { nextSteps: linkIsReachOnly(env) } : {}),
124
142
  };
125
143
  },
126
144
  sdkOptions: { isGenericFallback: true },
127
145
  };
146
+ /**
147
+ * A link proposed to an agent you can name.
148
+ *
149
+ * This used to ride the propose grammar as `engagementKind: "link"`, which put
150
+ * bilateral trust on the same verb as commercial terms it has none of: no
151
+ * price, no chat, no work. It belongs here, beside the invite and the list, so
152
+ * links have one rail. `link_create_invite` is the same act when you do NOT
153
+ * have an agent id and need a shareable page instead.
154
+ */
155
+ export const proposeLinkCapability = {
156
+ key: 'link_propose',
157
+ names: { sdk: 'link_propose', mcp: 'ziggs_link_propose' },
158
+ title: 'Propose a link to an agent',
159
+ descriptions: {
160
+ sdk: "Propose bilateral trust to an agent you can name — no chat, no money, no work. Approval is routed to that agent's owner, because a person decides who their delegate trusts. When you do NOT have the agent id, use link_create_invite for a shareable page instead.",
161
+ mcp: "Propose bilateral trust to an agent you can name (NOT a third-party service connection — see ziggs_connection_list for that). No chat, no money, no work: a link is reach, and reach only. The server routes approval to that agent's owner human, because a person decides who their delegate trusts, so the id in the response may differ from the one you passed; `note` says so when it does. Approve via ziggs_agreement_respond. When you do NOT have the agent id, use ziggs_link_create_invite for a shareable page instead.",
162
+ },
163
+ annotation: 'write',
164
+ params: {
165
+ counterparty: {
166
+ type: 'string',
167
+ required: true,
168
+ description: 'Agent id to link with.',
169
+ },
170
+ message: {
171
+ type: 'string',
172
+ description: 'Optional note shown to whoever approves it.',
173
+ },
174
+ },
175
+ needsAgentId: true,
176
+ handler: async (args, env) => {
177
+ const counterparty = args['counterparty'];
178
+ const { agreement } = await createAgreement({
179
+ engagementKind: 'link',
180
+ providerId: counterparty,
181
+ ...(args['message'] ? { description: args['message'] } : {}),
182
+ }, fullCreds(env));
183
+ // The owner rewrite is surfaced rather than left to be noticed: an agent
184
+ // that passed one id and reads back another otherwise concludes it was
185
+ // ignored.
186
+ const routedTo = agreement?.parties?.proposedTo;
187
+ const note = routedTo && routedTo !== counterparty
188
+ ? `Approval routed to the target agent's owner (${routedTo}): a person decides who their delegate trusts. You passed ${counterparty}.`
189
+ : undefined;
190
+ return {
191
+ agreement,
192
+ ...(note ? { note } : {}),
193
+ readPlan: [
194
+ nextCall(env, 'link_list', { status: 'open' }, 'check whether it has been approved yet'),
195
+ ],
196
+ };
197
+ },
198
+ sdkOptions: { isAgreementCreation: true },
199
+ };
128
200
  export const LINK_CAPABILITIES = [
201
+ proposeLinkCapability,
129
202
  createLinkInviteCapability,
130
203
  listLinksCapability,
131
204
  ];
@@ -1,8 +1,9 @@
1
1
  import { pullOffers, pullQuests } from '../http/MarketplaceClient.js';
2
2
  import { fullCreds } from './types.js';
3
+ import { nextCall } from './nextCall.js';
3
4
  const VIEW_KINDS = ['all', 'quests', 'offers'];
4
5
  function publishHint(env) {
5
- const propose = env.surface === 'mcp' ? 'ziggs_agreement_propose' : 'agreement_propose';
6
+ const propose = env.surface === 'mcp' ? 'ziggs_agreement_quest' : 'agreement_quest';
6
7
  const claim = env.surface === 'mcp' ? 'ziggs_agreement_claim' : 'agreement_claim';
7
8
  return (`Claim any row with ${claim} (agreementId). Publish your own with ${propose}: ` +
8
9
  `proposedTo "everyone" or "org" with no providerId broadcasts a quest (claimer works, you pay); ` +
@@ -16,9 +17,10 @@ function publishHint(env) {
16
17
  export const marketplaceViewCapability = {
17
18
  key: 'marketplace_view',
18
19
  names: { sdk: 'marketplace_view', mcp: 'ziggs_marketplace_view' },
20
+ title: 'Browse the marketplace',
19
21
  descriptions: {
20
22
  sdk: 'Browse the open marketplace — where every engagement STARTS (posted-first: reuse an active agreement, else claim a listing here, before ever proposing). Quests are work buyers broadcast (you would do the work); standing offers are services sellers broadcast (you would buy). Returns public rows plus org-scoped rows from your org, filtered server-side. Claim a row with agreement_claim — listings are take-it-or-leave-it, never counter one; publish your own via agreement_propose with proposedTo "everyone"/"org" (providerId = your id for an offer, omitted for a quest).',
21
- mcp: 'Browse the open marketplace — where every engagement STARTS (posted-first: reuse an active agreement, else claim a listing here, before ever proposing). Quests are work buyers broadcast (you would do the work); standing offers are services sellers broadcast (you would buy). Returns public rows plus org-scoped rows from your org, filtered server-side. Claim a row with ziggs_agreement_claim — listings are take-it-or-leave-it, never counter one; publish your own via ziggs_agreement_propose with proposedTo "everyone"/"org" (providerId = your id for an offer, omitted for a quest).',
23
+ mcp: 'Browse the open marketplace — where every engagement STARTS (posted-first: reuse an active agreement, else claim a listing here, before ever proposing). Quests are work buyers broadcast (you would do the work); standing offers are services sellers broadcast (you would buy). Returns public rows plus org-scoped rows from your org, filtered server-side. Claim a row with ziggs_agreement_claim — listings are take-it-or-leave-it, never counter one; publish your own with ziggs_agreement_offer (you do the work, the claimer pays) or ziggs_agreement_quest (the claimer does the work, you pay).',
22
24
  },
23
25
  annotation: 'read-only',
24
26
  params: {
@@ -48,6 +50,16 @@ export const marketplaceViewCapability = {
48
50
  return {
49
51
  ...(kind !== 'offers' ? { quests, questCount: quests.length } : {}),
50
52
  ...(kind !== 'quests' ? { offers, offerCount: offers.length } : {}),
53
+ // Claiming is the move after browsing, and the id is in the row the
54
+ // caller just received. The prose hint stays for the publish side, which
55
+ // is a choice rather than a call.
56
+ readPlan: [
57
+ ...(quests.length || offers.length
58
+ ? [
59
+ nextCall(env, 'agreement_claim', undefined, 'claim a listing from this view by passing its agreementId — listings are take-it-or-leave-it, never countered'),
60
+ ]
61
+ : []),
62
+ ],
51
63
  nextSteps: publishHint(env),
52
64
  };
53
65
  },
@@ -0,0 +1,44 @@
1
+ import type { CapabilityEnv } from './types.js';
2
+ /**
3
+ * One runnable next step: a tool name, pre-filled arguments, and why.
4
+ *
5
+ * The inbox and the context read already answer this way, and it is the part of
6
+ * those tools that works best: a caller runs the entries verbatim instead of
7
+ * parsing a paragraph for a tool name and then digging the ids out of the
8
+ * response body it just received.
9
+ *
10
+ * Everywhere else said the same thing in prose, under two other field names, so
11
+ * an agent had to read English to find out that "open a chat with the peer"
12
+ * meant `chat_open` with a `participantId` it had to locate itself. Prose is
13
+ * still right when the next move is not a call at all (a human has to decide
14
+ * something); it is wrong when the call and its arguments are both already
15
+ * known here.
16
+ */
17
+ export interface NextCall {
18
+ /** Registered tool name on the calling surface. */
19
+ tool: string;
20
+ /** Arguments, filled in from what the response already carries. */
21
+ args?: Record<string, unknown>;
22
+ /** One clause: what running this achieves. */
23
+ why: string;
24
+ }
25
+ /**
26
+ * Build a next step naming the tool as the calling surface registers it.
27
+ *
28
+ * Takes the capability key, not a tool name, because the two surfaces spell the
29
+ * same capability differently (`chat_open` and `ziggs_chat_open`). Every one of
30
+ * the 31 capability definitions follows that one rule, so the key is enough and
31
+ * a caller cannot accidentally emit a name the surface does not have.
32
+ */
33
+ export declare function nextCall(env: CapabilityEnv, capabilityKey: string, args: Record<string, unknown> | undefined, why: string): NextCall;
34
+ /**
35
+ * The other party in a two-party agreement, from the perspective of `selfId`.
36
+ *
37
+ * Returns null rather than guessing when the row does not identify one, because
38
+ * a pre-filled call naming the wrong counterparty is worse than no pre-filled
39
+ * call: the caller would run it, and it would do something they did not ask for.
40
+ */
41
+ export declare function peerAgentId(parties: {
42
+ creatorAgent?: string | null;
43
+ providerAgent?: string | null;
44
+ } | undefined, selfId: string | undefined): string | null;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Build a next step naming the tool as the calling surface registers it.
3
+ *
4
+ * Takes the capability key, not a tool name, because the two surfaces spell the
5
+ * same capability differently (`chat_open` and `ziggs_chat_open`). Every one of
6
+ * the 31 capability definitions follows that one rule, so the key is enough and
7
+ * a caller cannot accidentally emit a name the surface does not have.
8
+ */
9
+ export function nextCall(env, capabilityKey, args, why) {
10
+ return {
11
+ tool: env.surface === 'mcp' ? `ziggs_${capabilityKey}` : capabilityKey,
12
+ ...(args && Object.keys(args).length ? { args } : {}),
13
+ why,
14
+ };
15
+ }
16
+ /**
17
+ * The other party in a two-party agreement, from the perspective of `selfId`.
18
+ *
19
+ * Returns null rather than guessing when the row does not identify one, because
20
+ * a pre-filled call naming the wrong counterparty is worse than no pre-filled
21
+ * call: the caller would run it, and it would do something they did not ask for.
22
+ */
23
+ export function peerAgentId(parties, selfId) {
24
+ if (!parties || !selfId)
25
+ return null;
26
+ const { creatorAgent, providerAgent } = parties;
27
+ if (creatorAgent && creatorAgent !== selfId)
28
+ return creatorAgent;
29
+ if (providerAgent && providerAgent !== selfId)
30
+ return providerAgent;
31
+ return null;
32
+ }
@@ -32,6 +32,7 @@ const grantCaveatParams = {
32
32
  export const paymentBalanceCapability = {
33
33
  key: 'payment_balance',
34
34
  names: { sdk: 'payment_balance', mcp: 'ziggs_payment_balance' },
35
+ title: 'Check your balance',
35
36
  descriptions: {
36
37
  sdk: "Check the caller's current wallet balance and available balance (total minus active holds). Use before a transfer to confirm sufficient funds.",
37
38
  mcp: "Check the caller's current wallet balance and available balance (total minus active holds). Use before a transfer to confirm sufficient funds.",
@@ -50,6 +51,7 @@ export const paymentBalanceCapability = {
50
51
  export const paymentResolveWalletCapability = {
51
52
  key: 'payment_resolve_wallet',
52
53
  names: { sdk: 'payment_resolve_wallet', mcp: 'ziggs_payment_resolve_wallet' },
54
+ title: 'Look up a wallet',
53
55
  descriptions: {
54
56
  sdk: 'Look up a walletId by userId or agentId. Use before a transfer when you only know the recipient by their platform ID.',
55
57
  mcp: 'Look up a walletId by userId or agentId. Use before a transfer when you only know the recipient by their platform ID.',
@@ -82,6 +84,7 @@ export const paymentResolveWalletCapability = {
82
84
  export const paymentTransferCapability = {
83
85
  key: 'payment_transfer',
84
86
  names: { sdk: 'payment_transfer', mcp: 'ziggs_payment_transfer' },
87
+ title: 'Transfer funds',
85
88
  descriptions: {
86
89
  sdk: 'Transfer funds to another wallet. Amounts are integer cents. Transfers above the wallet owner\'s policy pause with status "approval_required" — wait inline with payment_wait_for_approval when you expect a quick decision.',
87
90
  mcp: 'Transfer funds to another wallet. Amounts are integer cents. As a delegate you spend under a payment grant the wallet owner issued (paymentGrantId — find yours via ziggs_grant_list scopeKind=wallet). Transfers above the owner\'s policy pause with status "approval_required": the human approves on the wallet page (it also shows in ziggs_pending_decisions) — you can wait inline with ziggs_payment_wait_for_approval, and you must NEVER approve your own transfer.',
@@ -169,6 +172,7 @@ export const paymentTransferCapability = {
169
172
  export const paymentWaitForApprovalCapability = {
170
173
  key: 'payment_wait_for_approval',
171
174
  names: { sdk: 'payment_wait_for_approval', mcp: 'ziggs_payment_wait_for_approval' },
175
+ title: 'Wait on a payment approval',
172
176
  descriptions: {
173
177
  sdk: 'Poll a paused transfer (status "approval_required") until the human decides or the timeout passes. Returns executed | rejected | expired | timeout | gone.',
174
178
  mcp: 'Poll a paused transfer (status "approval_required") until the human decides or the timeout passes. Returns executed | rejected | expired | timeout | gone. Use for quick decisions (≤2 min); for longer waits, stop and check again next session.',
@@ -207,6 +211,7 @@ export const paymentWaitForApprovalCapability = {
207
211
  export const paymentHoldCapability = {
208
212
  key: 'payment_hold',
209
213
  names: { sdk: 'payment_hold', mcp: 'ziggs_payment_hold' },
214
+ title: 'Hold funds in escrow',
210
215
  descriptions: {
211
216
  sdk: 'Pre-authorize (escrow) funds without moving them. Use to reserve payment at agreement formation; release with payment_release once work is complete or to refund if work is cancelled.',
212
217
  mcp: "Pre-authorize (escrow) funds without moving them. Use to reserve payment at agreement formation; release with ziggs_payment_release once work is complete, or refund if it's cancelled.",
@@ -246,6 +251,7 @@ export const paymentHoldCapability = {
246
251
  export const paymentReleaseCapability = {
247
252
  key: 'payment_release',
248
253
  names: { sdk: 'payment_release', mcp: 'ziggs_payment_release' },
254
+ title: 'Settle or refund a hold',
249
255
  descriptions: {
250
256
  sdk: "Settle or refund an escrow hold. Use action='complete' to transfer held funds to toWalletId (work done), or action='refund' to return funds to the sender (work cancelled).",
251
257
  mcp: "Settle or refund an escrow hold. action='complete' transfers held funds to toWalletId (work done); action='refund' returns funds to the sender (work cancelled).",
@@ -296,6 +302,7 @@ export const paymentReleaseCapability = {
296
302
  export const paymentIssueGrantCapability = {
297
303
  key: 'payment_issue_grant',
298
304
  names: { sdk: 'payment_issue_grant', mcp: 'ziggs_payment_issue_grant' },
305
+ title: 'Issue a spend grant',
299
306
  descriptions: {
300
307
  sdk: "Issue a payment grant delegating bounded spend from the operator's wallet to an agent holder. Caveats bound what the holder can do (max_amount, daily_budget, allowed_recipients, expiry). The holder spends by passing the grantId as paymentGrantId on transfers.",
301
308
  mcp: "Issue a payment grant delegating bounded spend from the operator's wallet to an agent holder. Caveats bound what the holder can do (max_amount, daily_budget, allowed_recipients, expiry). The holder spends by passing the grantId as paymentGrantId on transfers.",
@@ -329,6 +336,7 @@ export const paymentIssueGrantCapability = {
329
336
  export const paymentAttenuateGrantCapability = {
330
337
  key: 'payment_attenuate_grant',
331
338
  names: { sdk: 'payment_attenuate_grant', mcp: 'ziggs_payment_attenuate_grant' },
339
+ title: 'Pass on a tighter spend grant',
332
340
  descriptions: {
333
341
  sdk: 'Re-delegate a payment grant you hold to another agent with TIGHTER caveats (narrowing only — the child can never exceed the parent). Use to pass a bounded spend slice to a sub-agent.',
334
342
  mcp: 'Re-delegate a payment grant you hold to another agent with TIGHTER caveats (narrowing only — the child can never exceed the parent). Use to pass a bounded spend slice to a sub-agent.',
@@ -372,6 +380,7 @@ export const paymentAttenuateGrantCapability = {
372
380
  export const paymentRevokeGrantCapability = {
373
381
  key: 'payment_revoke_grant',
374
382
  names: { sdk: 'payment_revoke_grant', mcp: 'ziggs_payment_revoke_grant' },
383
+ title: 'Revoke a spend grant',
375
384
  descriptions: {
376
385
  sdk: 'Revoke a payment grant (and its attenuated children). The holder can no longer spend under it.',
377
386
  mcp: 'Revoke a payment grant (and its attenuated children). The holder can no longer spend under it.',
@@ -59,6 +59,15 @@ export interface CapabilityDefinition {
59
59
  sdk: string;
60
60
  mcp: string;
61
61
  };
62
+ /**
63
+ * Human-readable label a host shows instead of the raw `rail_verb` name.
64
+ * Required: a tool with no title displays as its wire name, and the
65
+ * connector directory rejects the surface for it. One title for both
66
+ * surfaces — the wire name differs per surface, the action it performs
67
+ * does not. Phrase it as that action, and keep it product copy: this is
68
+ * what a person reads in the tool picker.
69
+ */
70
+ title: string;
62
71
  /**
63
72
  * Tool description per surface. Kept side by side deliberately: wording may
64
73
  * reference surface-local tool names and MCP delegate-protocol guidance, but
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/api-client",
3
- "version": "0.9.11",
3
+ "version": "0.10.0",
4
4
  "description": "HTTP and WebSocket client for the Ziggs backend API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",