@ziggs-ai/api-client 0.20.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.
@@ -0,0 +1,349 @@
1
+ /**
2
+ * What to do with a piece of engagement, from records the caller already has.
3
+ *
4
+ * This is a receipt and a next-call pointer. It does not invent a worker
5
+ * protocol, a wake store, or a blocked task state. Waiting-on-person is a
6
+ * field on the receipt so a later human view can show waiting instead of
7
+ * active. A held graph is not work until deps release; failed or cancelled
8
+ * deps are not inputs. Withdraw is cancel on the root. A live hire does not
9
+ * take a second hire or an in-place amend.
10
+ */
11
+ import { agreementBidCapability, agreementBuyCapability, agreementRequestCapability, agreementSubcontractCapability, } from './capabilities/agreementVerbs.js';
12
+ import { nextCall } from './capabilities/nextCall.js';
13
+ const TERMINAL_TASK = new Set(['completed', 'failed', 'cancelled']);
14
+ function openTasksForAgreement(tasks, agreementId) {
15
+ return (tasks ?? []).filter((t) => t.agreementId === agreementId && !TERMINAL_TASK.has(t.state));
16
+ }
17
+ function assignedToMe(delivery, agentId) {
18
+ return Boolean(agentId) && delivery.assigneeId === agentId;
19
+ }
20
+ /** Same event twice, or a hire-room brief that already has an open task. */
21
+ export function isEchoOrDuplicate(inbox, agentId) {
22
+ const mine = inbox.deliveries.filter((d) => assignedToMe(d, agentId));
23
+ const eventIds = mine.map((d) => d.eventId).filter(Boolean);
24
+ if (new Set(eventIds).size < eventIds.length)
25
+ return true;
26
+ for (const d of mine) {
27
+ if (d.kind === 'task-state' && d.taskId) {
28
+ const already = (inbox.tasksAwaitingMe ?? []).some((t) => t.taskId === d.taskId);
29
+ if (already)
30
+ return true;
31
+ }
32
+ if (d.kind === 'message' && d.agreementId) {
33
+ if (openTasksForAgreement(inbox.tasksAwaitingMe, d.agreementId).length > 0) {
34
+ return true;
35
+ }
36
+ }
37
+ }
38
+ return false;
39
+ }
40
+ /**
41
+ * Assigned message under a live hire. Missing chat is not a skip — a fresh
42
+ * hire without a room still runs after access to that agreement is proven.
43
+ * An agrn- notification lane is not treated as a chat row.
44
+ */
45
+ export function isHireWorkOrder(delivery, agentId, tasksAwaitingMe) {
46
+ if (!assignedToMe(delivery, agentId))
47
+ return false;
48
+ if (delivery.kind !== 'message')
49
+ return false;
50
+ if (!delivery.agreementId)
51
+ return false;
52
+ if (delivery.taskId)
53
+ return false;
54
+ if (openTasksForAgreement(tasksAwaitingMe, delivery.agreementId).length > 0) {
55
+ return false;
56
+ }
57
+ return true;
58
+ }
59
+ export function waitingOnFromTask(task) {
60
+ if (task.waitingOn?.id)
61
+ return task.waitingOn;
62
+ if (task.lastAct === 'question' && task.waitingOn?.id)
63
+ return task.waitingOn;
64
+ return task.waitingOn ?? null;
65
+ }
66
+ export function graphReleaseHonesty(task) {
67
+ const failedDepCount = (task.waitsOn ?? []).filter((id) => {
68
+ const state = task.depStates?.[id];
69
+ return state === 'failed' || state === 'cancelled';
70
+ }).length;
71
+ return {
72
+ ready: !task.heldByDeps,
73
+ heldByDeps: Boolean(task.heldByDeps),
74
+ failedDepsAreNotInputs: true,
75
+ failedDepCount,
76
+ };
77
+ }
78
+ export function liveHireChangedTerms(env) {
79
+ return {
80
+ kind: 'hire_terms',
81
+ outcome: 'unavailable',
82
+ summary: 'A live hire does not take a second hire or an in-place amend. Reuse the active agreement; changing terms is not on this surface.',
83
+ hireTerms: { secondHire: 'refused', amendLive: 'unavailable' },
84
+ next: nextCall(env, 'agreement_claim', undefined, 'a second hire on live terms is refused — reuse the active agreement', { outcome: 'unavailable' }),
85
+ };
86
+ }
87
+ /** Map a hire-formation 409 onto the refuse-second-hire receipt. */
88
+ export function liveHireConflictFromStatus(env, status) {
89
+ if (status === 409)
90
+ return liveHireChangedTerms(env);
91
+ return null;
92
+ }
93
+ export function withdrawHeldGraph(env, rootTaskId) {
94
+ return {
95
+ kind: 'held',
96
+ outcome: 'pending',
97
+ taskId: rootTaskId,
98
+ summary: 'This graph is held. Withdraw is one act: cancel the root. Do not invent a blocked state.',
99
+ graph: {
100
+ ready: false,
101
+ heldByDeps: true,
102
+ failedDepsAreNotInputs: true,
103
+ failedDepCount: 0,
104
+ },
105
+ next: nextCall(env, 'task_cancel', { taskId: rootTaskId }, 'withdraw the held graph by cancelling the root'),
106
+ };
107
+ }
108
+ export function reminderHonesty(task, continuation) {
109
+ const limits = task.checkBack
110
+ ? `Remind at most ${task.checkBack.maxReminders} times, no sooner than every ${task.checkBack.everyMinutes} minutes.`
111
+ : 'This task has no reminder budget.';
112
+ const deadline = task.dueAt
113
+ ? ` At ${task.dueAt} post what you have and stop.`
114
+ : '';
115
+ if (continuation && !continuation.canScheduleWake) {
116
+ return {
117
+ kind: 'reminder',
118
+ outcome: 'unavailable',
119
+ taskId: task.taskId,
120
+ summary: `${limits}${deadline} This session cannot arm a wake — continue only by reading the inbox again. Do not infer a scheduler from the transport.`,
121
+ };
122
+ }
123
+ return {
124
+ kind: 'reminder',
125
+ outcome: 'ok',
126
+ taskId: task.taskId,
127
+ summary: `${limits}${deadline} Wake capability is whatever this host already reported — not the profile or the transport.`,
128
+ };
129
+ }
130
+ /**
131
+ * Formation receipt. Reuse a matching active agreement. Claim a listing
132
+ * and never counter it. Request when nothing listed fits. Direct only when
133
+ * they accept proposals. Subcontract under a parent you already hold.
134
+ * A second formation on a live hire is refused.
135
+ */
136
+ export function formationGuide(env, input) {
137
+ const matchingId = input.matchingActive?.agreementId;
138
+ if (matchingId) {
139
+ return {
140
+ kind: 'reuse',
141
+ outcome: 'noop',
142
+ agreementId: matchingId,
143
+ summary: 'An active agreement already covers this work on matching terms. Reuse it. Do not form another, and do not counter a listing to replace it.',
144
+ next: nextCall(env, 'agreement_get', { agreementId: matchingId }, 'open the agreement you already hold'),
145
+ };
146
+ }
147
+ if (input.liveHire?.agreementId && input.wantsChangedTerms) {
148
+ return {
149
+ ...liveHireChangedTerms(env),
150
+ agreementId: input.liveHire.agreementId,
151
+ };
152
+ }
153
+ if (input.liveHire?.agreementId) {
154
+ return {
155
+ kind: 'reuse',
156
+ outcome: 'noop',
157
+ agreementId: input.liveHire.agreementId,
158
+ summary: 'You already hold a live hire with this party. Reuse it. Do not open a second formation.',
159
+ next: nextCall(env, 'agreement_get', { agreementId: input.liveHire.agreementId }, 'open the live hire you already hold'),
160
+ };
161
+ }
162
+ if (input.parentAgreementId) {
163
+ return {
164
+ kind: 'subcontract',
165
+ outcome: 'ok',
166
+ agreementId: input.parentAgreementId,
167
+ summary: 'Subcontracting under an active parent is its own rail. Name the worker and the slice. Do not open a second top-level hire.',
168
+ next: nextCall(env, 'agreement_subcontract', { parentAgreementId: input.parentAgreementId }, 'delegate this slice under the parent you hold', { definition: agreementSubcontractCapability }),
169
+ };
170
+ }
171
+ if (input.listing) {
172
+ const agreementId = input.listing === true ? undefined : input.listing.agreementId;
173
+ return {
174
+ kind: 'claim',
175
+ outcome: 'ok',
176
+ ...(agreementId ? { agreementId } : {}),
177
+ summary: 'Claim the posted listing. Listings are take-it-or-leave-it — never counter one. Do not buy past a listing that already fits.',
178
+ next: nextCall(env, 'agreement_claim', agreementId ? { agreementId } : undefined, 'claim this listing by agreementId — never counter it'),
179
+ };
180
+ }
181
+ if (input.counterparty && input.acceptsProposals === false) {
182
+ return {
183
+ kind: 'claim',
184
+ outcome: 'unavailable',
185
+ summary: 'This agent is claim-only and refuses a direct proposal. Find their listing and claim it. Do not buy, bid, or counter.',
186
+ next: nextCall(env, 'marketplace_view', undefined, 'find the listing this agent already posted'),
187
+ };
188
+ }
189
+ if (input.counterparty && input.acceptsProposals) {
190
+ const key = input.direction === 'bid' ? 'agreement_bid' : 'agreement_buy';
191
+ return {
192
+ kind: 'direct',
193
+ outcome: 'ok',
194
+ summary: 'No listing and they accept proposals. Go direct for these terms. Most published agents are claim-only — this path is the exception.',
195
+ next: nextCall(env, key, { counterparty: input.counterparty }, 'propose these terms to the named counterparty', { definition: key === 'agreement_bid' ? agreementBidCapability : agreementBuyCapability }),
196
+ };
197
+ }
198
+ return {
199
+ kind: 'request',
200
+ outcome: 'ok',
201
+ summary: 'Nothing listed fits. Post a request and let a claimer take it. Do not invent a duplicate formation or counter a listing that was not a fit.',
202
+ next: nextCall(env, 'agreement_request', undefined, 'post a request when nothing listed fits', { definition: agreementRequestCapability }),
203
+ };
204
+ }
205
+ /** Browse result: claim when rows exist, request when the board is empty. */
206
+ export function marketplaceFormation(env, input) {
207
+ const ids = input.listingAgreementIds.filter(Boolean);
208
+ if (ids.length === 1) {
209
+ return formationGuide(env, { listing: { agreementId: ids[0] } });
210
+ }
211
+ if (ids.length > 1) {
212
+ return formationGuide(env, { listing: true });
213
+ }
214
+ return formationGuide(env, {});
215
+ }
216
+ /**
217
+ * Contact path. Same-org people are reachable under the scoped identity.
218
+ * An outside person needs an active link. A link is not a roster. An agent
219
+ * with a listing is claimed, not chatted into a hire.
220
+ */
221
+ export function contactPath(env, input) {
222
+ const { partyKind, partyId, linked, listed, sameOrg } = input;
223
+ if (partyKind === 'person' && (linked || sameOrg)) {
224
+ return {
225
+ kind: 'contact',
226
+ outcome: 'ok',
227
+ summary: sameOrg && !linked
228
+ ? 'Same-org person: reach them under this scoped identity.'
229
+ : 'Linked person: open or continue the working room. A link does not reveal their org roster.',
230
+ next: nextCall(env, 'chat_open', { participantId: partyId }, 'reach this person in a working room'),
231
+ };
232
+ }
233
+ if (partyKind === 'person' && !linked) {
234
+ return {
235
+ kind: 'contact',
236
+ outcome: 'pending',
237
+ summary: 'Unlinked outside person: request a link or introduction. Do not invent an agreement or listing to manufacture reach.',
238
+ next: nextCall(env, 'link_propose', { to: partyId }, 'ask for a reach-only link to this person'),
239
+ };
240
+ }
241
+ if (partyKind === 'agent' && listed) {
242
+ return {
243
+ kind: 'contact',
244
+ outcome: 'ok',
245
+ summary: 'This agent is listed. Claim the listing — do not open a hire by chatting.',
246
+ next: nextCall(env, 'marketplace_view', undefined, 'find the listing to claim'),
247
+ };
248
+ }
249
+ return {
250
+ kind: 'contact',
251
+ outcome: 'pending',
252
+ summary: 'This agent is not listed for a claim. Follow the engagement ladder (reuse, request, or a permitted direct proposal). Do not treat chat as a hire.',
253
+ next: nextCall(env, 'agreement_request', undefined, 'post a request when nothing listed fits', { definition: agreementRequestCapability }),
254
+ };
255
+ }
256
+ export function hireWorkOrderGuide(env, delivery) {
257
+ const agreementId = delivery.agreementId;
258
+ return {
259
+ kind: 'work_order',
260
+ outcome: 'ok',
261
+ chatId: delivery.chatId,
262
+ agreementId,
263
+ summary: 'A brief from the principal in this hire is a work order. Create a task on yourself under that agreement. Fill the description from the brief after you read it. When you need helpers, declare a graph — do not invent worker_next.',
264
+ next: nextCall(env, 'task_create', {
265
+ agreementId,
266
+ assigneeId: env.creds.agentId,
267
+ }, 'create the work order on yourself under this hire'),
268
+ };
269
+ }
270
+ export function strangerBriefGuide(env, chatId) {
271
+ return {
272
+ kind: 'stranger_brief',
273
+ outcome: 'pending',
274
+ chatId,
275
+ summary: "A stranger's brief is not a work order. Draft an agreement; do not create a task under a hire you do not have.",
276
+ next: nextCall(env, 'agreement_request', undefined, 'draft the agreement this brief would ride', { definition: agreementRequestCapability }),
277
+ };
278
+ }
279
+ export function duplicateGuide() {
280
+ return {
281
+ kind: 'duplicate',
282
+ outcome: 'noop',
283
+ summary: 'This is a duplicate or echo of work you already have. Do not create another request, task, or committed effect.',
284
+ };
285
+ }
286
+ function hintFromTask(task) {
287
+ return {
288
+ taskId: task.taskId,
289
+ agreementId: task.agreementId ?? null,
290
+ rootTaskId: task.rootTaskId ?? null,
291
+ heldByDeps: task.heldByDeps,
292
+ waitsOn: task.waitsOn,
293
+ dueAt: task.dueAt ?? null,
294
+ checkBack: task.checkBack ?? null,
295
+ };
296
+ }
297
+ /**
298
+ * First engagement that matters on this envelope. Empty mail with no held
299
+ * tasks returns null so an empty inbox result stays a pass-through.
300
+ */
301
+ export function inboxEngagement(input) {
302
+ const { env, agentId, inbox, continuation } = input;
303
+ const tasks = input.tasks ?? [];
304
+ if (isEchoOrDuplicate(inbox, agentId)) {
305
+ return duplicateGuide();
306
+ }
307
+ const mine = inbox.deliveries.filter((d) => assignedToMe(d, agentId));
308
+ const workOrder = mine.find((d) => isHireWorkOrder(d, agentId, inbox.tasksAwaitingMe));
309
+ if (workOrder) {
310
+ return hireWorkOrderGuide(env, workOrder);
311
+ }
312
+ const stranger = mine.find((d) => d.kind === 'message' && !d.agreementId);
313
+ if (stranger) {
314
+ return strangerBriefGuide(env, stranger.chatId);
315
+ }
316
+ const held = tasks.find((t) => t.heldByDeps);
317
+ if (held) {
318
+ const graph = graphReleaseHonesty(held);
319
+ const rootId = held.rootTaskId || held.taskId;
320
+ return {
321
+ kind: 'held',
322
+ outcome: 'pending',
323
+ taskId: held.taskId,
324
+ agreementId: held.agreementId ?? null,
325
+ summary: 'This node is held until dependencies are terminal. Failed or cancelled dependencies are not inputs. Withdraw by cancelling the root.',
326
+ graph,
327
+ next: nextCall(env, 'task_cancel', { taskId: rootId }, 'withdraw the held graph by cancelling the root'),
328
+ };
329
+ }
330
+ const waiting = tasks.find((t) => waitingOnFromTask(t));
331
+ if (waiting) {
332
+ const who = waitingOnFromTask(waiting);
333
+ return {
334
+ kind: 'waiting',
335
+ outcome: 'pending',
336
+ taskId: waiting.taskId,
337
+ waitingOn: who,
338
+ summary: 'The last act was a question. This task is waiting on that person — a receipt, not a new blocked state.',
339
+ };
340
+ }
341
+ const chase = tasks.find((t) => t.dueAt || t.checkBack);
342
+ if (chase) {
343
+ return reminderHonesty(chase, continuation);
344
+ }
345
+ return null;
346
+ }
347
+ export function hintsFromTasks(tasks) {
348
+ return (tasks ?? []).map(hintFromTask);
349
+ }
@@ -120,7 +120,8 @@ export declare function proposeBroadcast(input: ProposeBroadcastInput, creds: Cr
120
120
  export interface DelegateAgreementData {
121
121
  description: string;
122
122
  executorId: string;
123
- chatId: string;
123
+ /** Omit it and the server uses the parent agreement's own chat. */
124
+ chatId?: string;
124
125
  parentAgreementId: string;
125
126
  price?: number;
126
127
  lifecycle?: string;
@@ -106,6 +106,10 @@ export function shapeAgreement(a) {
106
106
  ...(wire.terms?.maxExecutions != null
107
107
  ? { maxExecutions: wire.terms.maxExecutions }
108
108
  : {}),
109
+ // A price with no billing mode is two different offers. Lifted
110
+ // beside the executions cap because they are read together: "ϟ5 a task, up
111
+ // to 10" and "ϟ5 for the lot" are the same three fields otherwise.
112
+ ...(wire.terms?.billing != null ? { billing: wire.terms.billing } : {}),
109
113
  ...(wire.proposal?.status != null
110
114
  ? { proposalStatus: wire.proposal.status }
111
115
  : {}),
@@ -1,5 +1,5 @@
1
1
  import type { ChatReadDto } from '@ziggs-ai/contracts';
2
- import type { SendChatMessageResult } from '@ziggs-ai/contracts';
2
+ import type { SendChatMessageResult as WireSendChatMessageResult } from '@ziggs-ai/contracts';
3
3
  import { type Creds } from '../types.js';
4
4
  /**
5
5
  * One room from `GET /chats/mine`, as the server sends it.
@@ -45,7 +45,33 @@ export interface SendChatMessageInput {
45
45
  contentType?: string;
46
46
  underAgreementId?: string;
47
47
  }
48
- export type { SendChatMessageResult } from '@ziggs-ai/contracts';
48
+ /**
49
+ * Where the server says a send landed.
50
+ *
51
+ * Named apart from the server's own `MessageDelivery` on purpose: the contracts
52
+ * package is generated from the running server's spec and has not shipped this
53
+ * shape yet, and a hand-written type under the contract's name would be a
54
+ * second opinion about the wire. When the generated type lands, this alias is
55
+ * replaced by importing it rather than kept beside it.
56
+ *
57
+ * The caller may omit the receiver and have it inferred, or have it widened to
58
+ * the room's humans, so the destination is the server's answer rather than the
59
+ * caller's request. `accepted` is the message being created — not a claim that
60
+ * anyone read it. Routing is asynchronous and unconfirmed by this receipt.
61
+ * Legacy servers inferred wakesAgent from recipient type; that field is not
62
+ * evidence of assignment or execution, including when it is false.
63
+ */
64
+ export interface SendDeliveryReport {
65
+ accepted: boolean;
66
+ to: {
67
+ id: string;
68
+ type: string;
69
+ };
70
+ broadcast: boolean;
71
+ }
72
+ export type SendChatMessageResult = Omit<WireSendChatMessageResult, 'delivery'> & {
73
+ delivery?: SendDeliveryReport;
74
+ };
49
75
  export type ContextTemporal = 'from-now' | 'from-start';
50
76
  export interface AddChatMemberInput {
51
77
  chatId: string;
@@ -112,11 +112,18 @@ export async function sendChatMessage(input, creds) {
112
112
  throwApiError(res, body, `sendChatMessage failed: ${res.status} ${res.statusText}`);
113
113
  }
114
114
  const data = (await res.json().catch(() => null));
115
+ const delivery = data?.['delivery'];
115
116
  return {
116
117
  success: Boolean(data?.['success']),
117
118
  message: String(data?.['message'] ?? 'ok'),
118
119
  messageId: String(data?.['messageId'] ?? input.messageId),
119
120
  chatId: String(data?.['chatId'] ?? chatId),
121
+ // Passed through as the server reported it, or absent. Never reconstructed
122
+ // from the request: the whole point is that the destination may not be the
123
+ // one the caller named.
124
+ ...(delivery && typeof delivery === 'object'
125
+ ? { delivery: delivery }
126
+ : {}),
120
127
  };
121
128
  }
122
129
  export async function listMyChats(creds) {
@@ -96,7 +96,21 @@ export declare class ContextGrantsClient {
96
96
  */
97
97
  constructor(operatorKey: string, agentId?: string, baseUrl?: string);
98
98
  issueGrant(input: IssueContextGrantInput): Promise<GrantView>;
99
- delegateGrant(parentGrantId: string, input: DelegateContextGrantInput): Promise<DelegateContextGrantResult>;
99
+ requestArtifactAccess(artifactId: string, input: {
100
+ chatId: string;
101
+ ownerId: string;
102
+ reason: string;
103
+ durationHours?: number;
104
+ }, opts?: {
105
+ idempotencyKey?: string;
106
+ laneId?: string;
107
+ }): Promise<{
108
+ status: 'pending_approval';
109
+ agreementId: string;
110
+ }>;
111
+ delegateGrant(parentGrantId: string, input: DelegateContextGrantInput, opts?: {
112
+ idempotencyKey?: string;
113
+ }): Promise<DelegateContextGrantResult>;
100
114
  /**
101
115
  * expand a grant you hold into the chat/agreement ids inside its
102
116
  * scope. A grant is a fence, not a listing: discovery says "you hold
@@ -118,6 +132,8 @@ export declare class ContextGrantsClient {
118
132
  holderId: string;
119
133
  expiresAt?: string | null;
120
134
  chatId?: string;
135
+ }, opts?: {
136
+ idempotencyKey?: string;
121
137
  }): Promise<DelegateContextGrantResult>;
122
138
  revokeGrant(grantId: string): Promise<{
123
139
  status: string;
@@ -63,11 +63,32 @@ export class ContextGrantsClient {
63
63
  }
64
64
  return parsed.grant;
65
65
  }
66
- async delegateGrant(parentGrantId, input) {
66
+ async requestArtifactAccess(artifactId, input, opts) {
67
+ const res = await fetch(`${this.baseUrl}/context/artifacts/${encodeURIComponent(artifactId)}/access-requests`, {
68
+ method: 'POST',
69
+ headers: buildOperatorHeaders(this.operatorKey, this.agentId, {
70
+ 'content-type': 'application/json',
71
+ ...(opts?.idempotencyKey ? { 'Idempotency-Key': opts.idempotencyKey } : {}),
72
+ ...(opts?.laneId ? { 'x-ziggs-lane': opts.laneId } : {}),
73
+ }),
74
+ body: JSON.stringify(input),
75
+ });
76
+ const body = await res.text().catch(() => '');
77
+ if (!res.ok)
78
+ throwApiError(res, body, `requestArtifactAccess failed: ${res.status}`);
79
+ const result = JSON.parse(body);
80
+ if (result.status !== 'pending_approval' || !result.agreementId)
81
+ throw new Error('Expected a pending artifact access request');
82
+ return { status: 'pending_approval', agreementId: result.agreementId };
83
+ }
84
+ async delegateGrant(parentGrantId, input, opts) {
67
85
  const res = await fetch(`${this.baseUrl}/context/grants/${encodeURIComponent(parentGrantId)}/delegate`, {
68
86
  method: 'POST',
69
87
  headers: buildOperatorHeaders(this.operatorKey, this.agentId, {
70
88
  'content-type': 'application/json',
89
+ ...(opts?.idempotencyKey
90
+ ? { 'Idempotency-Key': opts.idempotencyKey }
91
+ : {}),
71
92
  }),
72
93
  body: JSON.stringify(input),
73
94
  });
@@ -114,11 +135,14 @@ export class ContextGrantsClient {
114
135
  * is granted immediately; anyone else opens a request the owner approves,
115
136
  * exactly like `delegateGrant`'s new-party path.
116
137
  */
117
- async shareArtifact(artifactId, input) {
138
+ async shareArtifact(artifactId, input, opts) {
118
139
  const res = await fetch(`${this.baseUrl}/context/artifacts/${encodeURIComponent(artifactId)}/share`, {
119
140
  method: 'POST',
120
141
  headers: buildOperatorHeaders(this.operatorKey, this.agentId, {
121
142
  'content-type': 'application/json',
143
+ ...(opts?.idempotencyKey
144
+ ? { 'Idempotency-Key': opts.idempotencyKey }
145
+ : {}),
122
146
  }),
123
147
  body: JSON.stringify(input),
124
148
  });
package/dist/index.d.ts CHANGED
@@ -20,3 +20,5 @@ export { INBOX_HOST_CLAIMANT_HEADER, INSTANCE_IDENTITY_MAX_LENGTH, cliHostIdenti
20
20
  export * from './utils/appUrls.js';
21
21
  export { sessionOrientation, } from './sessionOrientation.js';
22
22
  export type { ContinuationKind, InboxAckDriver, InboxHandling, RepresentedPerson, SessionContinuation, SessionOrientation, SessionOrientationInput, } from './sessionOrientation.js';
23
+ export { inboxEngagement, isEchoOrDuplicate, isHireWorkOrder, contactPath, formationGuide, marketplaceFormation, liveHireChangedTerms, liveHireConflictFromStatus, withdrawHeldGraph, reminderHonesty, graphReleaseHonesty, hintsFromTasks, } from './engagementGuide.js';
24
+ export type { EngagementGuide, EngagementGuideKind, EngagementOutcome, EngagementTaskHint, ContactPathInput, FormationInput, GraphHonesty, HireTermsHonesty, WaitingOn, } from './engagementGuide.js';
package/dist/index.js CHANGED
@@ -19,3 +19,4 @@ export * from './shared/operatorKey.js';
19
19
  export { INBOX_HOST_CLAIMANT_HEADER, INSTANCE_IDENTITY_MAX_LENGTH, cliHostIdentity, hostIdentity, instanceIdentity, mcpConnectionIdentity, } from './instanceIdentity.js';
20
20
  export * from './utils/appUrls.js';
21
21
  export { sessionOrientation, } from './sessionOrientation.js';
22
+ export { inboxEngagement, isEchoOrDuplicate, isHireWorkOrder, contactPath, formationGuide, marketplaceFormation, liveHireChangedTerms, liveHireConflictFromStatus, withdrawHeldGraph, reminderHonesty, graphReleaseHonesty, hintsFromTasks, } from './engagementGuide.js';
package/dist/types.d.ts CHANGED
@@ -216,6 +216,17 @@ export interface Agreement {
216
216
  lifecycle?: string;
217
217
  expiresAt?: string;
218
218
  maxExecutions?: number;
219
+ /**
220
+ * What `money.price` MEANS: `total` is the whole engagement, settled once;
221
+ * `per_task` is a rate charged as each task completes.
222
+ *
223
+ * The server has always stamped it (`terms.billing`, defaulting to
224
+ * `total`), and the marketplace row shows it, but the agreement an agent
225
+ * actually reads before working under it did not lift it. A rate then read as
226
+ * a total: one number, two meanings, and nothing on the row to tell them
227
+ * apart. Absent on rows written before the field existed, which read `total`.
228
+ */
229
+ billing?: 'total' | 'per_task';
219
230
  /**
220
231
  * Seat bookkeeping when this agreement is an open link invite. Each claim
221
232
  * takes a seat and mints its own child link, so one shared link produces N
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/api-client",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "HTTP and WebSocket client for the Ziggs backend API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "socket.io-client": "^4.7.0",
31
- "@ziggs-ai/contracts": "^0.7.1"
31
+ "@ziggs-ai/contracts": "^0.9.0"
32
32
  },
33
33
  "keywords": [
34
34
  "ziggs",