@ziggs-ai/api-client 0.20.0 → 0.22.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 { MessageDelivery, 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
+ * The contract's own shape, re-exported rather than restated: it shipped in
52
+ * contracts 0.7.7, and the hand-written stand-in that carried it until then is
53
+ * gone. What the fields mean is worth keeping in front of a caller, because
54
+ * the receipt is easy to over-read:
55
+ *
56
+ * The caller may omit the receiver and have it inferred, or have it widened to
57
+ * the room's humans, so the destination is the server's answer rather than the
58
+ * caller's request. `accepted` is the message being created — not a claim that
59
+ * anyone read it. Routing is asynchronous and unconfirmed by this receipt; the
60
+ * `wakesAgent` field that once rode along was removed for exactly that reason
61
+ * and is not coming back.
62
+ */
63
+ export type { MessageDelivery } from '@ziggs-ai/contracts';
64
+ /**
65
+ * The send result as a caller here holds it.
66
+ *
67
+ * `delivery` is optional on the way in and required in the contract, which is
68
+ * not a disagreement about the wire: this client is installed independently of
69
+ * the backend it talks to, and a server from before the field existed answers
70
+ * without one.
71
+ */
72
+ export type SendChatMessageResult = Omit<WireSendChatMessageResult, 'delivery'> & {
73
+ delivery?: MessageDelivery;
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
  });
@@ -27,7 +27,7 @@ export interface ContextReadQuery {
27
27
  cursor?: string;
28
28
  limit?: number;
29
29
  after?: string;
30
- direction?: 'forward';
30
+ direction?: 'forward' | 'backward';
31
31
  state?: string;
32
32
  contextGrantId?: string;
33
33
  }
@@ -58,6 +58,13 @@ export interface ContextSnapshotParticipant {
58
58
  export interface ContextSnapshotResult {
59
59
  history: unknown[];
60
60
  agreements: unknown[];
61
+ /** Bounded room inventory, without bodies. Absent on older backends. */
62
+ artifacts?: ContextReadEnvelope<{
63
+ artifactId: string;
64
+ name: string | null;
65
+ contentType: string;
66
+ timestamp: string | null;
67
+ }>;
61
68
  agents: Array<ContextSnapshotParticipant & {
62
69
  agentId: string;
63
70
  }>;
@@ -137,8 +137,20 @@ export type PlanStepPatch = {
137
137
  status: 'pending' | 'in_progress' | 'completed' | 'skipped';
138
138
  result?: unknown;
139
139
  };
140
- export declare function updateTaskPlanSteps(taskId: string, patches: PlanStepPatch[], creds: Creds): Promise<TaskWriteConfirm>;
141
- export declare function updateTaskPlanStep(taskId: string, stepId: string, status: string, result: unknown, creds: Creds): Promise<TaskWriteConfirm>;
140
+ /**
141
+ * Who the holder is waiting on, because its last act was asking them.
142
+ *
143
+ * Rides the progress write because that is when it is known and that is what
144
+ * makes it false again: the server clears it on the next progress write that
145
+ * does not restate it, and on any terminal transition. A holder that did
146
+ * something else after asking is no longer waiting.
147
+ */
148
+ export type TaskWaitingOn = {
149
+ kind: 'user' | 'agent';
150
+ id: string;
151
+ };
152
+ export declare function updateTaskPlanSteps(taskId: string, patches: PlanStepPatch[], creds: Creds, waitingOn?: TaskWaitingOn | null): Promise<TaskWriteConfirm>;
153
+ export declare function updateTaskPlanStep(taskId: string, stepId: string, status: string, result: unknown, creds: Creds, waitingOn?: TaskWaitingOn | null): Promise<TaskWriteConfirm>;
142
154
  export declare function reportTask(taskId: string, message: string | undefined, creds: Creds): Promise<TaskWriteConfirm>;
143
155
  export declare function setTaskSatisfaction(taskId: string, satisfaction: 'positive' | 'negative', creds: Creds): Promise<TaskWriteConfirm>;
144
156
  export interface ListTasksOptions {
@@ -277,13 +277,17 @@ export async function replaceTaskPlan(taskId, steps, creds) {
277
277
  throw new Error('Invalid response: task write confirm not found');
278
278
  return confirm;
279
279
  }
280
- export async function updateTaskPlanSteps(taskId, patches, creds) {
280
+ export async function updateTaskPlanSteps(taskId, patches, creds, waitingOn) {
281
281
  if (!taskId)
282
282
  throw new Error('updateTaskPlanSteps: taskId is required');
283
- if (!patches || !Array.isArray(patches) || patches.length === 0) {
283
+ const named = Array.isArray(patches) ? patches : [];
284
+ // A bare waiting-on write is the ask that produced no other progress, and
285
+ // the route takes it on its own. Without this the only way to say "I asked
286
+ // Dana" was to invent a plan step to tick.
287
+ if (named.length === 0 && !waitingOn) {
284
288
  throw new Error('updateTaskPlanSteps: patches must name at least one step');
285
289
  }
286
- for (const patch of patches) {
290
+ for (const patch of named) {
287
291
  if (!patch?.stepId)
288
292
  throw new Error('updateTaskPlanSteps: each patch needs a stepId');
289
293
  if (!patch?.status)
@@ -293,7 +297,10 @@ export async function updateTaskPlanSteps(taskId, patches, creds) {
293
297
  const res = await fetch(`${getTaskBaseUrl()}/${taskId}/plan`, {
294
298
  method: 'PATCH',
295
299
  headers: buildHeaders(creds),
296
- body: JSON.stringify({ patches }),
300
+ body: JSON.stringify({
301
+ ...(named.length > 0 ? { patches: named } : {}),
302
+ ...(waitingOn !== undefined ? { waitingOn } : {}),
303
+ }),
297
304
  });
298
305
  if (!res.ok) {
299
306
  const responseBody = await res.text().catch(() => '');
@@ -305,7 +312,7 @@ export async function updateTaskPlanSteps(taskId, patches, creds) {
305
312
  throw new Error('Invalid response: task write confirm not found');
306
313
  return confirm;
307
314
  }
308
- export async function updateTaskPlanStep(taskId, stepId, status, result, creds) {
315
+ export async function updateTaskPlanStep(taskId, stepId, status, result, creds, waitingOn) {
309
316
  if (!taskId)
310
317
  throw new Error('updateTaskPlanStep: taskId is required');
311
318
  if (!stepId)
@@ -316,7 +323,15 @@ export async function updateTaskPlanStep(taskId, stepId, status, result, creds)
316
323
  const res = await fetch(`${getTaskBaseUrl()}/${taskId}/plan`, {
317
324
  method: 'PATCH',
318
325
  headers: buildHeaders(creds),
319
- body: JSON.stringify({ stepId, status, ...(result !== undefined && { result }) }),
326
+ // One write, not two: the server clears `waitingOn` on a progress write
327
+ // that does not restate it, so posting the tick and the ask separately
328
+ // would set it and then immediately drop it.
329
+ body: JSON.stringify({
330
+ stepId,
331
+ status,
332
+ ...(result !== undefined && { result }),
333
+ ...(waitingOn !== undefined ? { waitingOn } : {}),
334
+ }),
320
335
  });
321
336
  if (!res.ok) {
322
337
  const responseBody = await res.text().catch(() => '');
package/dist/index.d.ts CHANGED
@@ -12,6 +12,8 @@ export { parseErrorMessage, parseErrorCode, throwApiError, } from './shared/apiE
12
12
  export { ApiError } from './types.js';
13
13
  export type { Creds, Task, TaskState, PlanStep, PlanStepStatus, Agreement, AgreementParties, AgreementPartySide, EngagementKind, BroadcastAudience, EntryType, ContentType, MessageMetadata, MessageHandler, InboxDeliveryKind, InboxDeliveryRef, InboxChatNews, InboxProposalRef, InboxTaskRef, InboxRequestRef, InboxConnectionRequestRef, InboxHumanAttention, InboxEnvelope, InboxAckResult, InboxPeek, InboxReadOptions, } from './types.js';
14
14
  export type { ProposeTerms, ProposeDirectInput, ProposeBroadcastInput, ProposeAgreementData, ClaimOptions, } from './http/AgreementClient.js';
15
+ export { decisionWords } from './decisionWords.js';
16
+ export type { DecisionOutcome, DecisionWords, DecisionWordsInput, } from './decisionWords.js';
15
17
  export { planInboxAck, planPartialInboxAck, inboxAckAllowed, inboxAckHandledIds, } from './inboxAck.js';
16
18
  export type { InboxAckDecision, InboxAckStep, InboxAckEnvelope, PlanInboxAckOptions, } from './inboxAck.js';
17
19
  export { agreementLaneId, agreementIdFromLane, isAgreementLaneId, isAgreementId } from '@ziggs-ai/contracts';
@@ -20,3 +22,5 @@ export { INBOX_HOST_CLAIMANT_HEADER, INSTANCE_IDENTITY_MAX_LENGTH, cliHostIdenti
20
22
  export * from './utils/appUrls.js';
21
23
  export { sessionOrientation, } from './sessionOrientation.js';
22
24
  export type { ContinuationKind, InboxAckDriver, InboxHandling, RepresentedPerson, SessionContinuation, SessionOrientation, SessionOrientationInput, } from './sessionOrientation.js';
25
+ export { inboxEngagement, isEchoOrDuplicate, isHireWorkOrder, contactPath, formationGuide, marketplaceFormation, liveHireChangedTerms, liveHireConflictFromStatus, withdrawHeldGraph, reminderHonesty, graphReleaseHonesty, hintsFromTasks, } from './engagementGuide.js';
26
+ export type { EngagementGuide, EngagementGuideKind, EngagementOutcome, EngagementTaskHint, ContactPathInput, FormationInput, GraphHonesty, HireTermsHonesty, WaitingOn, } from './engagementGuide.js';
package/dist/index.js CHANGED
@@ -13,9 +13,13 @@ export { RateLimitedError, isRateLimited, parseRetryAfterMs, pollSurfaceError, }
13
13
  // half of them had drifted).
14
14
  export { parseErrorMessage, parseErrorCode, throwApiError, } from './shared/apiError.js';
15
15
  export { ApiError } from './types.js';
16
+ // One decision, one sentence, on every rail: the reason a delivery row carries
17
+ // when somebody answered a request this agent made, rendered as words.
18
+ export { decisionWords } from './decisionWords.js';
16
19
  export { planInboxAck, planPartialInboxAck, inboxAckAllowed, inboxAckHandledIds, } from './inboxAck.js';
17
20
  export { agreementLaneId, agreementIdFromLane, isAgreementLaneId, isAgreementId } from '@ziggs-ai/contracts';
18
21
  export * from './shared/operatorKey.js';
19
22
  export { INBOX_HOST_CLAIMANT_HEADER, INSTANCE_IDENTITY_MAX_LENGTH, cliHostIdentity, hostIdentity, instanceIdentity, mcpConnectionIdentity, } from './instanceIdentity.js';
20
23
  export * from './utils/appUrls.js';
21
24
  export { sessionOrientation, } from './sessionOrientation.js';
25
+ export { inboxEngagement, isEchoOrDuplicate, isHireWorkOrder, contactPath, formationGuide, marketplaceFormation, liveHireChangedTerms, liveHireConflictFromStatus, withdrawHeldGraph, reminderHonesty, graphReleaseHonesty, hintsFromTasks, } from './engagementGuide.js';