@ziggs-ai/api-client 0.19.0 → 0.21.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.
Files changed (45) hide show
  1. package/dist/capabilities/agreementVerbs.d.ts +7 -0
  2. package/dist/capabilities/agreementVerbs.js +84 -1
  3. package/dist/capabilities/agreements.d.ts +3 -0
  4. package/dist/capabilities/agreements.js +9 -1
  5. package/dist/capabilities/artifacts.d.ts +38 -0
  6. package/dist/capabilities/artifacts.js +148 -7
  7. package/dist/capabilities/chat.d.ts +31 -1
  8. package/dist/capabilities/chat.js +39 -0
  9. package/dist/capabilities/context.d.ts +3 -0
  10. package/dist/capabilities/context.js +125 -1
  11. package/dist/capabilities/index.d.ts +6 -6
  12. package/dist/capabilities/index.js +6 -6
  13. package/dist/capabilities/introductions.js +5 -10
  14. package/dist/capabilities/links.js +1 -0
  15. package/dist/capabilities/marketplace.js +15 -6
  16. package/dist/capabilities/nextCall.d.ts +72 -14
  17. package/dist/capabilities/nextCall.js +279 -4
  18. package/dist/capabilities/tasks.d.ts +68 -1
  19. package/dist/capabilities/tasks.js +112 -3
  20. package/dist/engagementGuide.d.ts +148 -0
  21. package/dist/engagementGuide.js +349 -0
  22. package/dist/http/AgreementClient.d.ts +2 -1
  23. package/dist/http/AgreementClient.js +4 -0
  24. package/dist/http/ArtifactsClient.d.ts +47 -0
  25. package/dist/http/ArtifactsClient.js +33 -0
  26. package/dist/http/ChatClient.d.ts +28 -2
  27. package/dist/http/ChatClient.js +7 -0
  28. package/dist/http/ContextGrantsClient.d.ts +17 -1
  29. package/dist/http/ContextGrantsClient.js +26 -2
  30. package/dist/http/ContextOpenClient.d.ts +70 -0
  31. package/dist/http/ContextOpenClient.js +52 -0
  32. package/dist/http/InboxClient.d.ts +8 -0
  33. package/dist/http/InboxClient.js +18 -0
  34. package/dist/http/TaskClient.d.ts +2 -0
  35. package/dist/http/TaskClient.js +1 -0
  36. package/dist/http/index.d.ts +2 -0
  37. package/dist/http/index.js +1 -0
  38. package/dist/index.d.ts +4 -0
  39. package/dist/index.js +2 -0
  40. package/dist/instanceIdentity.d.ts +1 -1
  41. package/dist/instanceIdentity.js +2 -2
  42. package/dist/sessionOrientation.d.ts +50 -0
  43. package/dist/sessionOrientation.js +43 -0
  44. package/dist/types.d.ts +11 -0
  45. package/package.json +2 -2
@@ -22,3 +22,10 @@ export declare const agreementRequestCapability: CapabilityDefinition;
22
22
  export declare const agreementOfferCapability: CapabilityDefinition;
23
23
  export declare const agreementHandoffCapability: CapabilityDefinition;
24
24
  export declare const AGREEMENT_VERB_CAPABILITIES: CapabilityDefinition[];
25
+ /**
26
+ * Subcontract sits next to the propose verbs, not inside them: the parent
27
+ * already exists, and the conversation is optional. The server uses the
28
+ * parent's own chat when none is named. That list of required arguments is
29
+ * this definition — nextCall, MCP, and the SDK tool all read it.
30
+ */
31
+ export declare const agreementSubcontractCapability: CapabilityDefinition;
@@ -1,4 +1,4 @@
1
- import { getAgreement, proposeBroadcast, proposeDirectTo, } from '../http/AgreementClient.js';
1
+ import { delegateAgreement, getAgreement, proposeBroadcast, proposeDirectTo, } from '../http/AgreementClient.js';
2
2
  import { publishOffer } from '../http/MarketplaceClient.js';
3
3
  import { fullCreds } from './types.js';
4
4
  import { nextCall } from './nextCall.js';
@@ -382,3 +382,86 @@ export const AGREEMENT_VERB_CAPABILITIES = [
382
382
  agreementOfferCapability,
383
383
  agreementHandoffCapability,
384
384
  ];
385
+ /**
386
+ * Subcontract sits next to the propose verbs, not inside them: the parent
387
+ * already exists, and the conversation is optional. The server uses the
388
+ * parent's own chat when none is named. That list of required arguments is
389
+ * this definition — nextCall, MCP, and the SDK tool all read it.
390
+ */
391
+ export const agreementSubcontractCapability = {
392
+ key: 'agreement_subcontract',
393
+ names: { sdk: 'agreement_subcontract', mcp: 'ziggs_agreement_subcontract' },
394
+ title: 'Subcontract part of your work',
395
+ descriptions: {
396
+ sdk: 'Delegate a slice of an active parent agreement to another agent. Name the parent, the worker, and the slice. The worker must approve — never impersonated. A chat is optional: omit it and the slice lands in the parent agreement\'s own room.',
397
+ mcp: 'Delegate a slice of an active parent agreement to another agent. Name the parent, the worker, and the slice. The worker must approve — never impersonated. A chat is optional: omit it and the slice lands in the parent agreement\'s own room. Spawn tasks for the worker under the sub-agreement once it is active.',
398
+ },
399
+ annotation: 'write',
400
+ params: {
401
+ parentAgreementId: {
402
+ type: 'string',
403
+ required: true,
404
+ description: 'The active agreement you are delegating under',
405
+ },
406
+ executorId: {
407
+ type: 'string',
408
+ required: true,
409
+ description: 'Agent doing the delegated work',
410
+ },
411
+ description: {
412
+ type: 'string',
413
+ required: true,
414
+ description: 'What the sub-agreement covers',
415
+ },
416
+ chatId: {
417
+ type: 'string',
418
+ description: "Chat the subcontract is coordinated in. Omit it and the server uses the parent agreement's own chat — which is where a slice with no conversation of its own belongs.",
419
+ },
420
+ price: {
421
+ type: 'number',
422
+ description: 'Price in POINTS, as an integer of hundredths — 500 means ϟ5.00. Recording a price here does not itself move any.',
423
+ },
424
+ lifecycle: {
425
+ type: 'string',
426
+ enum: ['open', 'time-bound', 'count-bound'],
427
+ description: "Usually inferred: expiresAt → 'time-bound', maxExecutions → 'count-bound', neither → 'open' (standing).",
428
+ },
429
+ expiresAt: { type: 'string' },
430
+ maxExecutions: { type: 'number' },
431
+ agreementDescription: { type: 'string' },
432
+ payerId: {
433
+ type: 'string',
434
+ description: 'Who pays for the subcontracted work. Defaults to the delegating side.',
435
+ },
436
+ },
437
+ needsAgentId: true,
438
+ handler: async (args, env) => {
439
+ const chatId = args['chatId'];
440
+ const agreement = await delegateAgreement({
441
+ parentAgreementId: args['parentAgreementId'],
442
+ executorId: args['executorId'],
443
+ description: args['description'],
444
+ ...(typeof chatId === 'string' && chatId.trim()
445
+ ? { chatId: chatId.trim() }
446
+ : {}),
447
+ ...(args['price'] === undefined ? {} : { price: args['price'] }),
448
+ ...(args['lifecycle'] === undefined
449
+ ? {}
450
+ : { lifecycle: args['lifecycle'] }),
451
+ ...(args['expiresAt'] === undefined
452
+ ? {}
453
+ : { expiresAt: args['expiresAt'] }),
454
+ ...(args['maxExecutions'] === undefined
455
+ ? {}
456
+ : { maxExecutions: args['maxExecutions'] }),
457
+ ...(args['agreementDescription'] === undefined
458
+ ? {}
459
+ : { agreementDescription: args['agreementDescription'] }),
460
+ ...(args['payerId'] === undefined
461
+ ? {}
462
+ : { payerId: args['payerId'] }),
463
+ }, fullCreds(env));
464
+ return { agreement };
465
+ },
466
+ sdkOptions: { isAgreementCreation: true },
467
+ };
@@ -1,5 +1,6 @@
1
1
  import type { ClaimedKind } from '../http/AgreementClient.js';
2
2
  import type { Agreement } from '../types.js';
3
+ import { type NextCallOutcome, type WorkContext } from './nextCall.js';
3
4
  import { type CapabilityDefinition, type CapabilityEnv } from './types.js';
4
5
  /**
5
6
  * One claim result shape for both surfaces. The SDK protocol runner used to
@@ -8,9 +9,11 @@ import { type CapabilityDefinition, type CapabilityEnv } from './types.js';
8
9
  */
9
10
  export declare function presentClaimResult(agreement: Agreement, kind: ClaimedKind, env: CapabilityEnv): {
10
11
  status: string;
12
+ outcome: NextCallOutcome;
11
13
  kind: ClaimedKind;
12
14
  message: string;
13
15
  agreement: Agreement;
16
+ workContext?: WorkContext;
14
17
  };
15
18
  /**
16
19
  * the one claim verb. Requests, standing offers, and link invites
@@ -1,6 +1,7 @@
1
1
  import { agreementAppUrl } from '../utils/appUrls.js';
2
2
  import { claimOpenAgreement } from '../http/agreementFlows.js';
3
3
  import { linkIsReachOnly, webAppOrigin } from './links.js';
4
+ import { workContextFromEnv } from './nextCall.js';
4
5
  import { fullCreds } from './types.js';
5
6
  /** Which side the claimer just took, for a claim that has not activated yet. */
6
7
  function claimedWhat(kind) {
@@ -16,12 +17,15 @@ function claimedWhat(kind) {
16
17
  * agreement}` — same HTTP call, two answers.
17
18
  */
18
19
  export function presentClaimResult(agreement, kind, env) {
20
+ const workContext = workContextFromEnv(env);
19
21
  if (kind === 'link') {
20
22
  return {
21
23
  status: 'linked',
24
+ outcome: 'ok',
22
25
  kind,
23
26
  message: `Link invite claimed — you are now linked. ${linkIsReachOnly(env)}`,
24
27
  agreement,
28
+ ...(workContext ? { workContext } : {}),
25
29
  };
26
30
  }
27
31
  if (agreement?.status !== 'active') {
@@ -31,16 +35,19 @@ export function presentClaimResult(agreement, kind, env) {
31
35
  ? 'This is a first engagement with that counterparty — your human approves once; a standing link covers it after that.'
32
36
  : 'Claiming for a job your human already approved activates at once — name the job with mandateAgreementId.';
33
37
  return {
34
- status: 'claimed',
38
+ status: 'pending_approval',
39
+ outcome: 'pending',
35
40
  kind,
36
41
  message: `${claimedWhat(kind)} It is not active yet: the formation is waiting on ` +
37
42
  'human approval, so nothing can be created under it until that lands. ' +
38
43
  `Your human can approve it here: ${approveUrl} — ${remedy}`,
39
44
  agreement,
45
+ ...(workContext ? { workContext } : {}),
40
46
  };
41
47
  }
42
48
  return {
43
49
  status: 'claimed',
50
+ outcome: 'ok',
44
51
  kind,
45
52
  message: kind === 'offer'
46
53
  ? 'Standing offer claimed — the publisher provides, your side pays. Spawn work under it with the task-create tool.'
@@ -48,6 +55,7 @@ export function presentClaimResult(agreement, kind, env) {
48
55
  ? 'Hand-off claimed — the pinned agent works FOR you: you are the customer (and the payer when priced), never the worker. Spawn work under it with the task-create tool.'
49
56
  : 'Request claimed — you provide the work. Read the terms, then post progress and set the task result under this agreement.',
50
57
  agreement,
58
+ ...(workContext ? { workContext } : {}),
51
59
  };
52
60
  }
53
61
  /**
@@ -1,4 +1,6 @@
1
+ import { type DiscoverableItem } from '../http/ContextDiscoveryClient.js';
1
2
  import { type CapabilityDefinition } from './types.js';
3
+ import { type NextCall } from './nextCall.js';
2
4
  export declare const recordArtifactCapability: CapabilityDefinition;
3
5
  /**
4
6
  * the artifacts you wrote, across every scope and none.
@@ -8,6 +10,42 @@ export declare const recordArtifactCapability: CapabilityDefinition;
8
10
  * agent finds what it recorded without naming a container.
9
11
  */
10
12
  export declare const listArtifactsCapability: CapabilityDefinition;
13
+ /**
14
+ * What the agent could not read, said as metadata and nothing else.
15
+ *
16
+ * The one thing this block must never become is a second results list. A row
17
+ * here is a LABEL — `context_discover_grantable` re-shapes every row to
18
+ * type/label/scopeRef/orgId on the way through, so no message, no member name
19
+ * and no artifact body can travel in it, and none of these scopes were searched
20
+ * at all. Keeping them in a separate field with its own wording is the whole
21
+ * point: an agent that cannot tell "no match in what I can read" from "there is
22
+ * a locked room over there" will confidently report the first while the answer
23
+ * sits in the second.
24
+ */
25
+ export interface UnsearchedContext {
26
+ /** Locked scopes, labels only — never content, never counts of content. */
27
+ items: (DiscoverableItem & {
28
+ requestAccess: NextCall;
29
+ })[];
30
+ count: number;
31
+ note: string;
32
+ }
33
+ /**
34
+ * search the work you hold, and say what you searched.
35
+ *
36
+ * `artifact_list` answers "what did I write" and the scope reads answer "what
37
+ * is in this container". Neither answers "where is the thing about X", which is
38
+ * the question an agent actually has when it has been handed work that refers
39
+ * to something it did not produce. The server bounds this by the agent's own
40
+ * reach — authorship, readable containers, artifact grants it holds — under the
41
+ * active lane and shared-room containment, so there is no `reach` arm to ask
42
+ * for and the narrowing filters are refused rather than ignored.
43
+ *
44
+ * The response is deliberately two things that never merge: `artifacts`, which
45
+ * is readable content that matched, and `notSearched`, which is labels of
46
+ * scopes that were never opened. `coverage` sits between them naming what ran.
47
+ */
48
+ export declare const findArtifactsCapability: CapabilityDefinition;
11
49
  /**
12
50
  * hand one artifact you authored to one other agent.
13
51
  *
@@ -1,7 +1,9 @@
1
1
  import { ArtifactsClient } from '../http/ArtifactsClient.js';
2
2
  import { ContextGrantsClient } from '../http/ContextGrantsClient.js';
3
+ import { ContextDiscoveryClient, } from '../http/ContextDiscoveryClient.js';
3
4
  import { fullCreds } from './types.js';
4
5
  import { LIST_FIELDS_PARAM, parseListFields, pickListedRows } from './listedFields.js';
6
+ import { nextCall } from './nextCall.js';
5
7
  /**
6
8
  * teaching: name the result slot on the success path so an agent finds
7
9
  * the right move unaided — worded in each surface's task grammar (SDK
@@ -16,7 +18,10 @@ function reportingHint(env, contentType, taskId) {
16
18
  ? `Recorded as a task-bound result artifact. Close the task by setting its terminal result with ${close}.`
17
19
  : `Recorded as a result artifact, but not bound to a task — pass taskId to bind it, then close the task with ${close}.`;
18
20
  }
19
- return `Reporting finished work? Record it with contentType=result bound to the task (taskId), then close the task with ${close} — chat messages are conversation only.`;
21
+ // The record and the delivery are different acts. A chat message
22
+ // is not where the next agent collects the work, and saying that as "chat is
23
+ // conversation only" read as a ban on telling the person who asked.
24
+ return `Reporting finished work? Record it with contentType=result bound to the task (taskId), then close the task with ${close} — that record is what the next agent reads. Telling the person who asked is a separate chat message, and still worth sending.`;
20
25
  }
21
26
  /** Inline body cap + escape hatch, shared by SDK/MCP descriptions. */
22
27
  const ARTIFACT_RECORD_INLINE_CAP = 'Inline text max 50000 characters. Over that: pass the body as a file ' +
@@ -267,8 +272,8 @@ export const listArtifactsCapability = {
267
272
  names: { sdk: 'artifact_list', mcp: 'ziggs_artifact_list' },
268
273
  title: 'List artifacts you wrote',
269
274
  descriptions: {
270
- sdk: 'List artifacts you authored, in any scope or none — including free-standing ones no scope read can reach. Forward-delta with `after`.',
271
- mcp: 'List artifacts YOU authored, across every scope and none. Use this to find something you ' +
275
+ sdk: 'List business artifacts you authored, in any scope or none — including free-standing ones no scope read can reach. Tool-operation and thought traces are excluded. Forward-delta with `after`.',
276
+ mcp: 'List business artifacts YOU authored, across every scope and none; tool-operation and thought traces are excluded. Use this to find something you ' +
272
277
  'recorded free-standing (no chat/agreement/task), which the scope reads cannot return. ' +
273
278
  'To read an artifact someone shared WITH you, use ziggs_context_read with via=artifact:<id>; ' +
274
279
  'to see what has been shared with you, use ziggs_grant_list with scopeKind=artifact.',
@@ -295,6 +300,133 @@ export const listArtifactsCapability = {
295
300
  : listed;
296
301
  },
297
302
  };
303
+ const UNSEARCHED_NOTE = 'These were NOT searched. You hold no grant on them, so only their label is ' +
304
+ 'visible here — no contents, no message counts, nothing that would leak what ' +
305
+ 'is inside. Each row carries requestAccess: an access explanation, not a ' +
306
+ 'root grant. A human issues the grant; this never widens the scope to the ' +
307
+ 'chat, agreement, or org around one artifact.';
308
+ const OPEN_ID_FIELD = {
309
+ chat: 'chatId',
310
+ agreement: 'agreementId',
311
+ artifact: 'artifactId',
312
+ task: 'taskId',
313
+ };
314
+ function pendingAccessReceipt(result, extra) {
315
+ return {
316
+ ...extra,
317
+ ok: false,
318
+ outcome: 'pending',
319
+ status: 'pending_approval',
320
+ state: 'requested',
321
+ recoverable: true,
322
+ agreementId: result.agreementId,
323
+ ownerId: result.ownerId,
324
+ };
325
+ }
326
+ /**
327
+ * The locked scopes near a search that found nothing.
328
+ *
329
+ * Fetched only when it changes what a caller should conclude, which is why it
330
+ * is not on every response: an agent holding a page of matches does not need
331
+ * the list of doors it has not opened, and an extra round trip on every find
332
+ * would buy nothing. A search that came back empty is exactly where the wrong
333
+ * conclusion gets drawn, so that is where this arrives unasked.
334
+ *
335
+ * Discovery failing is not find failing. It is bounded to orgs the agent holds
336
+ * an active work agreement in and can legitimately return nothing, or refuse;
337
+ * either way the matches already computed are the answer, and the miss is
338
+ * reported as a limitation rather than thrown over the top of them.
339
+ */
340
+ async function unsearchedContext(env) {
341
+ const creds = fullCreds(env);
342
+ let items;
343
+ try {
344
+ items = await new ContextDiscoveryClient(creds.operatorKey, creds.agentId).discoverGrantable();
345
+ }
346
+ catch (err) {
347
+ return {
348
+ note: 'Could not check for context you cannot read yet: ' +
349
+ `${err instanceof Error ? err.message : String(err)}. ` +
350
+ 'Treat this answer as covering only what was searched.',
351
+ };
352
+ }
353
+ return {
354
+ items: items.map((item) => {
355
+ const idField = OPEN_ID_FIELD[item.scopeRef.kind];
356
+ return {
357
+ ...item,
358
+ requestAccess: nextCall(env, 'access_explain', idField ? { [idField]: item.scopeRef.id } : undefined, idField
359
+ ? `explain access to locked ${item.type} "${item.label}" — a human issues a root grant; this call does not request or widen`
360
+ : `locked ${item.type} "${item.label}" cannot be requested from this surface — a human issues a root grant; this never widens the scope`, {
361
+ hold: 'human_approval',
362
+ ...(idField ? {} : { outcome: 'unavailable' }),
363
+ }),
364
+ };
365
+ }),
366
+ count: items.length,
367
+ note: UNSEARCHED_NOTE,
368
+ };
369
+ }
370
+ /**
371
+ * search the work you hold, and say what you searched.
372
+ *
373
+ * `artifact_list` answers "what did I write" and the scope reads answer "what
374
+ * is in this container". Neither answers "where is the thing about X", which is
375
+ * the question an agent actually has when it has been handed work that refers
376
+ * to something it did not produce. The server bounds this by the agent's own
377
+ * reach — authorship, readable containers, artifact grants it holds — under the
378
+ * active lane and shared-room containment, so there is no `reach` arm to ask
379
+ * for and the narrowing filters are refused rather than ignored.
380
+ *
381
+ * The response is deliberately two things that never merge: `artifacts`, which
382
+ * is readable content that matched, and `notSearched`, which is labels of
383
+ * scopes that were never opened. `coverage` sits between them naming what ran.
384
+ */
385
+ export const findArtifactsCapability = {
386
+ key: 'artifact_find',
387
+ names: { sdk: 'artifact_find', mcp: 'ziggs_artifact_find' },
388
+ title: 'Find artifacts you can read',
389
+ descriptions: {
390
+ sdk: 'Search business artifacts you can already reach (excluding tool-operation and thought traces) — what you authored, what your readable chats/agreements/tasks hold, and what artifact grants you hold — by words in the name, filename or body. Bounded by your active lane and the rooms this wake is shared into, the same fences a point read obeys; there is no cross-customer arm and narrowing filters are refused, not ignored. Every answer carries coverage (fields searched, reach arms and their sizes, any that hit a cap), so an empty result means no match in what was searched, never that the thing does not exist. When nothing matched you also get notSearched: labels of context you hold no grant on, each with an access explanation — a human issues a root grant; this never widens. Metadata only, never contents.',
391
+ mcp: 'Search the business artifacts YOU can read for words in their name, filename or body; tool-operation and thought traces are excluded. Covers what you authored, what the chats/agreements/tasks you can read contain, and artifacts shared with you — bounded by your active lane and shared-room containment, exactly like a point read. Open a hit with ziggs_context_read (via=artifact:<id>). ' +
392
+ 'Read the coverage block before concluding anything: it names the fields searched, the arms that produced candidates and any that hit a cap. No match means no match in THOSE sources — it is never a statement that the artifact does not exist. ' +
393
+ 'If nothing matched, notSearched lists context you cannot read yet (same rows as ziggs_context_discover_grantable): labels only, never contents, each with an access explanation. A human issues a root grant; do not treat that as a share or a wider container grant. Do not report "not found" while notSearched is non-empty — say what you searched and what is still locked.',
394
+ },
395
+ annotation: 'read-only',
396
+ params: {
397
+ q: {
398
+ type: 'string',
399
+ required: true,
400
+ description: 'Words to match; every term must appear. This is a lexical match on name, filename and extracted text — not a semantic search.',
401
+ },
402
+ limit: { type: 'number', description: 'Page size (server default when omitted)' },
403
+ before: {
404
+ type: 'string',
405
+ description: 'Older-rows cursor: pass the previous page’s coverage.nextCursor back verbatim (opaque).',
406
+ },
407
+ includeUnreadable: {
408
+ type: 'boolean',
409
+ description: 'Also list context you hold no grant on (labels only, never contents). Defaults on when nothing matched, since that is when an empty answer is most likely to be misread; pass true to ask for it alongside matches.',
410
+ },
411
+ fields: LIST_FIELDS_PARAM,
412
+ },
413
+ needsAgentId: true,
414
+ handler: async (args, env) => {
415
+ const creds = fullCreds(env);
416
+ const found = await new ArtifactsClient(creds.operatorKey, creds.agentId, creds.laneId).find(args['q'], {
417
+ limit: typeof args['limit'] === 'number' ? args['limit'] : undefined,
418
+ before: args['before'],
419
+ });
420
+ const fields = parseListFields(args['fields']);
421
+ const shaped = fields
422
+ ? { ...found, artifacts: pickListedRows(found.artifacts, fields) }
423
+ : found;
424
+ const asked = args['includeUnreadable'] === true;
425
+ if (!asked && found.artifacts.length > 0)
426
+ return shaped;
427
+ return { ...shaped, notSearched: await unsearchedContext(env) };
428
+ },
429
+ };
298
430
  /**
299
431
  * hand one artifact you authored to one other agent.
300
432
  *
@@ -335,6 +467,10 @@ export const shareArtifactCapability = {
335
467
  type: 'string',
336
468
  description: 'Chat to surface the approval request in, when the receiver’s owner has to approve',
337
469
  },
470
+ idempotencyKey: {
471
+ type: 'string',
472
+ description: 'Replay the same share. Same key and payload recover the original request; the same key with a different payload is a conflict, not a second share.',
473
+ },
338
474
  },
339
475
  needsAgentId: true,
340
476
  handler: async (args, env) => {
@@ -349,16 +485,20 @@ export const shareArtifactCapability = {
349
485
  holderId,
350
486
  expiresAt: args['expiresAt'],
351
487
  chatId: args['chatId'],
488
+ }, {
489
+ idempotencyKey: typeof args['idempotencyKey'] === 'string'
490
+ ? args['idempotencyKey']
491
+ : undefined,
352
492
  });
353
493
  if (result.status === 'pending_approval') {
354
- return {
355
- ok: true,
494
+ return pendingAccessReceipt(result, {
356
495
  ...result,
357
- note: `Not shared yet — ${result.ownerId ?? 'the owner'} must approve agreement ${result.agreementId}.`,
358
- };
496
+ note: `Requested, not granted — ${result.ownerId ?? 'the owner'} must approve agreement ${result.agreementId}. Recover that id; do not open a second card.`,
497
+ });
359
498
  }
360
499
  return {
361
500
  ok: true,
501
+ outcome: 'ok',
362
502
  ...result,
363
503
  note: `${holderId} can now read artifact ${artifactId} with via=artifact:${artifactId}.`,
364
504
  };
@@ -644,6 +784,7 @@ export const reextractArtifactCapability = {
644
784
  export const ARTIFACT_CAPABILITIES = [
645
785
  recordArtifactCapability,
646
786
  listArtifactsCapability,
787
+ findArtifactsCapability,
647
788
  shareArtifactCapability,
648
789
  attachArtifactCapability,
649
790
  uploadArtifactUrlCapability,
@@ -1,4 +1,34 @@
1
- import { type CapabilityDefinition } from './types.js';
1
+ import { type SendChatMessageResult } from '../http/ChatClient.js';
2
+ import { type CapabilityDefinition, type CapabilityEnv } from './types.js';
3
+ import { type WorkContext } from './nextCall.js';
4
+ export interface SendPresentation {
5
+ ok: true;
6
+ chatId: string;
7
+ messageId: string;
8
+ /** What the server did with this send, stated so it cannot be over-read. */
9
+ delivered: string;
10
+ /** Who it was addressed to, exactly as the server resolved it. */
11
+ to?: {
12
+ id: string;
13
+ type: string;
14
+ };
15
+ workContext?: WorkContext;
16
+ }
17
+ /**
18
+ * Present a send as what it is: a message that now exists, addressed to
19
+ * somebody, which may or may not wake anyone.
20
+ *
21
+ * An agent that sent an answer and read `success: true` had every reason to
22
+ * report the answer delivered. Three different things hide behind that word.
23
+ * The message exists — that much is certain. Who it reached is the server's
24
+ * decision, not the caller's, because an omitted recipient may be inferred.
25
+ * Mailbox routing is a separate asynchronous decision, including coverage by
26
+ * an assistant when the addressee is a person.
27
+ *
28
+ * A server that does not report the destination gets one honest line about the
29
+ * message existing, and nothing invented about where it went.
30
+ */
31
+ export declare function presentSendResult(result: SendChatMessageResult, env: CapabilityEnv): SendPresentation;
2
32
  /**
3
33
  * the decided chat surface for agents: chat_open only.
4
34
  * There is deliberately NO chat-listing tool on the SDK: taking part in a room
@@ -1,5 +1,44 @@
1
1
  import { openConversation } from '../http/ChatClient.js';
2
2
  import { fullCreds } from './types.js';
3
+ import { workContextFromEnv } from './nextCall.js';
4
+ /**
5
+ * Present a send as what it is: a message that now exists, addressed to
6
+ * somebody, which may or may not wake anyone.
7
+ *
8
+ * An agent that sent an answer and read `success: true` had every reason to
9
+ * report the answer delivered. Three different things hide behind that word.
10
+ * The message exists — that much is certain. Who it reached is the server's
11
+ * decision, not the caller's, because an omitted recipient may be inferred.
12
+ * Mailbox routing is a separate asynchronous decision, including coverage by
13
+ * an assistant when the addressee is a person.
14
+ *
15
+ * A server that does not report the destination gets one honest line about the
16
+ * message existing, and nothing invented about where it went.
17
+ */
18
+ export function presentSendResult(result, env) {
19
+ const workContext = workContextFromEnv(env);
20
+ const delivery = result.delivery;
21
+ const base = {
22
+ ok: true,
23
+ chatId: result.chatId,
24
+ messageId: result.messageId,
25
+ ...(workContext ? { workContext } : {}),
26
+ };
27
+ if (!delivery) {
28
+ return {
29
+ ...base,
30
+ delivered: `Message ${result.messageId} was accepted into chat ${result.chatId}. Accepted is not read: nothing here says anyone has seen it.`,
31
+ };
32
+ }
33
+ const destination = delivery.broadcast
34
+ ? 'the humans in the room, not one addressee'
35
+ : `${delivery.to.id} (${delivery.to.type})`;
36
+ return {
37
+ ...base,
38
+ delivered: `Message ${result.messageId} was accepted into chat ${result.chatId}, addressed to ${destination}. Mailbox routing is unconfirmed; a person may be covered by an assistant. Accepted is not read and does not confirm a wake or reply.`,
39
+ to: delivery.to,
40
+ };
41
+ }
3
42
  /**
4
43
  * the decided chat surface for agents: chat_open only.
5
44
  * There is deliberately NO chat-listing tool on the SDK: taking part in a room
@@ -20,4 +20,7 @@ export declare const contextReadCapability: CapabilityDefinition;
20
20
  export declare const contextExpandReachCapability: CapabilityDefinition;
21
21
  export declare const contextDiscoverGrantableCapability: CapabilityDefinition;
22
22
  export declare const contextDelegateCapability: CapabilityDefinition;
23
+ export declare const openCapability: CapabilityDefinition;
24
+ export declare const accessExplainCapability: CapabilityDefinition;
23
25
  export declare const CONTEXT_CAPABILITIES: CapabilityDefinition[];
26
+ export declare const contextRequestCapability: CapabilityDefinition;