@ziggs-ai/api-client 0.1.25 → 0.1.28

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.
@@ -17,7 +17,12 @@ export interface ProposeTerms {
17
17
  agreementDescription?: string;
18
18
  parentAgreementId?: string;
19
19
  parentTaskId?: string;
20
- payerId?: string;
20
+ /**
21
+ * Who does the work. Required on direct proposals (your own id = you offer;
22
+ * the proposedTo id = you commission the recipient); forbidden on
23
+ * broadcasts. The payer is always derived server-side as the non-providing
24
+ * side — there is no payer input.
25
+ */
21
26
  providerId?: string;
22
27
  plan?: unknown;
23
28
  planReviewTiming?: PlanReviewTiming;
@@ -146,6 +151,12 @@ export declare function revokeAgreement(agreementId: string, creds: Creds): Prom
146
151
  ok: boolean;
147
152
  agreement: Agreement;
148
153
  }>;
154
+ /** ZIG-832: mark an agreement fulfilled (complete). A provider closing its own
155
+ * delivered work — party-gated server-side. */
156
+ export declare function fulfillAgreement(agreementId: string, creds: Creds): Promise<{
157
+ ok: boolean;
158
+ agreement: Agreement;
159
+ }>;
149
160
  /**
150
161
  * Claim an open agreement (ZIG-524 phase 2 / ZIG-525):
151
162
  * - open link invite (`engagementKind: link`, proposedTo everyone)
@@ -159,7 +170,6 @@ export declare function claimAgreement(agreementId: string, creds: Creds): Promi
159
170
  ok: boolean;
160
171
  agreement: Agreement;
161
172
  }>;
162
- export declare function getAgreementsByChat(chatId: string, creds: Creds): Promise<unknown[]>;
163
173
  export type ChatLinkType = 'origin' | 'mention' | 'delegation' | 'join';
164
174
  export declare function linkAgreementToChat(agreementId: string, chatId: string, linkType: ChatLinkType | undefined, creds: Creds): Promise<unknown | null>;
165
175
  export declare function getChatsForAgreement(agreementId: string, creds: Creds): Promise<unknown[]>;
@@ -200,11 +210,14 @@ export declare class AgreementClient {
200
210
  ok: boolean;
201
211
  agreement: Agreement;
202
212
  }>;
213
+ fulfill(id: string): Promise<{
214
+ ok: boolean;
215
+ agreement: Agreement;
216
+ }>;
203
217
  claimAgreement(id: string): Promise<{
204
218
  ok: boolean;
205
219
  agreement: Agreement;
206
220
  }>;
207
- listByChat(chatId: string): Promise<unknown[]>;
208
221
  linkToChat(id: string, chatId: string, linkType?: ChatLinkType): Promise<unknown>;
209
222
  listChats(id: string): Promise<unknown[]>;
210
223
  join(id: string): Promise<{
@@ -370,6 +370,26 @@ export async function revokeAgreement(agreementId, creds) {
370
370
  }
371
371
  return data;
372
372
  }
373
+ /** ZIG-832: mark an agreement fulfilled (complete). A provider closing its own
374
+ * delivered work — party-gated server-side. */
375
+ export async function fulfillAgreement(agreementId, creds) {
376
+ if (!agreementId)
377
+ throw new Error('agreementId is required to fulfill');
378
+ assertCreds(creds, 'agreement fulfillment');
379
+ const res = await fetch(`${getAgreementBaseUrl()}/${encodeURIComponent(agreementId)}/fulfill`, {
380
+ method: 'POST',
381
+ headers: buildHeaders(creds),
382
+ });
383
+ if (!res.ok) {
384
+ const body = await res.text().catch(() => '');
385
+ throwApiError(res, body, `Agreement fulfillment failed: ${res.status} ${res.statusText}`);
386
+ }
387
+ const data = await res.json().catch(() => null);
388
+ if (!data?.['agreement']) {
389
+ throw new Error('Invalid response: expected { ok, agreement } from POST /agreements/:id/fulfill');
390
+ }
391
+ return data;
392
+ }
373
393
  /**
374
394
  * Claim an open agreement (ZIG-524 phase 2 / ZIG-525):
375
395
  * - open link invite (`engagementKind: link`, proposedTo everyone)
@@ -394,32 +414,6 @@ export async function claimAgreement(agreementId, creds) {
394
414
  }
395
415
  return data;
396
416
  }
397
- // ---------------------------------------------------------------------------
398
- // Chat links
399
- // ---------------------------------------------------------------------------
400
- export async function getAgreementsByChat(chatId, creds) {
401
- if (!chatId)
402
- return [];
403
- assertCreds(creds, 'get agreements by chat');
404
- try {
405
- const res = await fetch(`${getAgreementBaseUrl()}/by-chat/${encodeURIComponent(chatId)}`, {
406
- method: 'GET',
407
- headers: buildHeaders(creds),
408
- });
409
- if (!res.ok) {
410
- const body = await res.text().catch(() => '');
411
- runtimeLog.warn('AgreementClient', `⚠️ Get agreements by chat failed: ${res.status} ${res.statusText} ${body?.slice(0, 200)}`);
412
- return [];
413
- }
414
- const data = await res.json().catch(() => null);
415
- const links = data?.['links'] ?? data?.['agreements'];
416
- return Array.isArray(links) ? links : [];
417
- }
418
- catch (e) {
419
- runtimeLog.warn('AgreementClient', `⚠️ Get agreements by chat failed: ${e.message}`);
420
- return [];
421
- }
422
- }
423
417
  export async function linkAgreementToChat(agreementId, chatId, linkType = 'mention', creds) {
424
418
  if (!agreementId || !chatId)
425
419
  return null;
@@ -596,8 +590,8 @@ export class AgreementClient {
596
590
  get(id) { return getAgreement(id, this.creds); }
597
591
  create(data) { return createAgreement(data, this.creds); }
598
592
  revoke(id) { return revokeAgreement(id, this.creds); }
593
+ fulfill(id) { return fulfillAgreement(id, this.creds); }
599
594
  claimAgreement(id) { return claimAgreement(id, this.creds); }
600
- listByChat(chatId) { return getAgreementsByChat(chatId, this.creds); }
601
595
  linkToChat(id, chatId, linkType) { return linkAgreementToChat(id, chatId, linkType ?? 'mention', this.creds); }
602
596
  listChats(id) { return getChatsForAgreement(id, this.creds); }
603
597
  join(id) { return joinAgreement(id, this.creds); }
@@ -1,23 +1,20 @@
1
1
  import 'dotenv/config';
2
+ import type { GrantView } from './grants.js';
2
3
  export type ContextGrantScopeKind = 'chat' | 'agreement' | 'org';
3
4
  export type ContextTemporal = 'from-now' | 'from-start';
4
5
  export interface ContextGrantScope {
5
6
  kind: ContextGrantScopeKind;
6
7
  id: string;
7
8
  }
8
- export interface ContextGrantRecord {
9
- grantId: string;
10
- issuerId: string;
11
- holderId: string;
12
- scopeKind: ContextGrantScopeKind;
13
- scopeId: string;
14
- temporal: ContextTemporal;
15
- watermarkAt: string;
16
- parentGrantId: string | null;
17
- expiresAt: string | null;
18
- revoked: boolean;
19
- createdAt: string;
20
- }
9
+ /**
10
+ * @deprecated ZIG-646 — context grants now serialise to the canonical
11
+ * {@link GrantView} like every other rail. This alias previously declared a
12
+ * flat `scopeKind`/`scopeId` + `revoked` shape that the backend never actually
13
+ * emitted (it sends nested `scope: { kind, id }` and folds temporal/watermark
14
+ * into `caveats`), so reads like `record.scopeKind` were always `undefined`.
15
+ * Use `GrantView`; this remains only so existing name-imports keep resolving.
16
+ */
17
+ export type ContextGrantRecord = GrantView;
21
18
  export interface IssueContextGrantInput {
22
19
  holderId: string;
23
20
  scope: ContextGrantScope;
@@ -36,7 +33,7 @@ export interface DelegateContextGrantInput {
36
33
  * approves — no grant is minted until then. */
37
34
  export type DelegateContextGrantResult = {
38
35
  status: 'granted';
39
- grant: ContextGrantRecord;
36
+ grant: GrantView;
40
37
  } | {
41
38
  status: 'pending_approval';
42
39
  agreementId: string;
@@ -54,7 +51,7 @@ export declare class ContextGrantsClient {
54
51
  * @param agentId Required for fleet keys (sent as X-Agent-Id). Omit for agent-scoped keys.
55
52
  */
56
53
  constructor(operatorKey: string, agentId?: string, baseUrl?: string);
57
- issueGrant(input: IssueContextGrantInput): Promise<ContextGrantRecord>;
54
+ issueGrant(input: IssueContextGrantInput): Promise<GrantView>;
58
55
  delegateGrant(parentGrantId: string, input: DelegateContextGrantInput): Promise<DelegateContextGrantResult>;
59
56
  revokeGrant(grantId: string): Promise<{
60
57
  status: string;
@@ -20,6 +20,22 @@ export interface ContextReadEnvelope<T = unknown> {
20
20
  nextCursor: string | null;
21
21
  latestSequence?: string | null;
22
22
  }
23
+ /** Aggregated chat snapshot from `GET /context/snapshot` (ZIG-775 follow-up). */
24
+ export interface ContextSnapshotResult {
25
+ history: unknown[];
26
+ agreements: unknown[];
27
+ agents: Array<{
28
+ agentId: string;
29
+ role: string;
30
+ isYou: boolean;
31
+ }>;
32
+ users: Array<{
33
+ userId: string;
34
+ role: string;
35
+ }>;
36
+ latestSequence?: string | null;
37
+ chatMissing?: boolean;
38
+ }
23
39
  /**
24
40
  * Protocol-first uniform context reads (ZIG-427).
25
41
  * Wraps `GET /context/read/:type` — one client, one envelope, four types.
@@ -34,4 +50,14 @@ export declare class ContextReadClient {
34
50
  */
35
51
  constructor(operatorKey: string, agentId?: string, baseUrl?: string);
36
52
  read<T = unknown>(type: ContextReadType, query: ContextReadQuery): Promise<ContextReadEnvelope<T>>;
53
+ /**
54
+ * Aggregated chat snapshot — `GET /context/snapshot?via=chat:<id>`. The
55
+ * server composes history + agreements + roster through the same grant-fenced
56
+ * readers as {@link read}, so hosted agents pull one fenced view per wake.
57
+ * Rejects with an `Error & { status }` on non-OK so callers can branch (404).
58
+ */
59
+ snapshot(chatId: string, opts?: {
60
+ maxMessages?: number;
61
+ contextGrantId?: string;
62
+ }): Promise<ContextSnapshotResult>;
37
63
  }
@@ -53,4 +53,39 @@ export class ContextReadClient {
53
53
  }
54
54
  return JSON.parse(body);
55
55
  }
56
+ /**
57
+ * Aggregated chat snapshot — `GET /context/snapshot?via=chat:<id>`. The
58
+ * server composes history + agreements + roster through the same grant-fenced
59
+ * readers as {@link read}, so hosted agents pull one fenced view per wake.
60
+ * Rejects with an `Error & { status }` on non-OK so callers can branch (404).
61
+ */
62
+ async snapshot(chatId, opts = {}) {
63
+ if (!chatId?.trim()) {
64
+ throw new Error('ContextReadClient.snapshot: chatId is required');
65
+ }
66
+ const url = new URL(`${this.baseUrl}/context/snapshot`);
67
+ url.searchParams.set('via', `chat:${chatId}`);
68
+ if (opts.maxMessages != null) {
69
+ url.searchParams.set('maxMessages', String(opts.maxMessages));
70
+ }
71
+ if (opts.contextGrantId) {
72
+ url.searchParams.set('contextGrantId', opts.contextGrantId);
73
+ }
74
+ const headers = {
75
+ Authorization: `Bearer ${this.operatorKey}`,
76
+ };
77
+ if (this.agentId)
78
+ headers['X-Agent-Id'] = this.agentId;
79
+ if (opts.contextGrantId) {
80
+ headers['X-Context-Grant-Id'] = opts.contextGrantId;
81
+ }
82
+ const res = await fetch(url.toString(), { headers });
83
+ const body = await res.text().catch(() => '');
84
+ if (!res.ok) {
85
+ const err = new Error(`ContextReadClient.snapshot ${res.status} ${body.slice(0, 200)}`);
86
+ err.status = res.status;
87
+ throw err;
88
+ }
89
+ return JSON.parse(body);
90
+ }
56
91
  }
@@ -35,7 +35,11 @@ export class MessagesClient {
35
35
  const res = await fetch(url.toString(), { headers: this._headers() });
36
36
  if (!res.ok) {
37
37
  const body = await res.text().catch(() => '');
38
- throw new Error(`MessagesClient.list ${res.status} ${res.statusText} ${body.slice(0, 200)}`);
38
+ const err = new Error(`MessagesClient.list ${res.status} ${res.statusText} ${body.slice(0, 200)}`);
39
+ // Callers branch on the HTTP status (404 = chat deleted/not visible)
40
+ // without parsing the message string.
41
+ err.status = res.status;
42
+ throw err;
39
43
  }
40
44
  return (await res.json());
41
45
  }
@@ -20,7 +20,13 @@ function parseErrorMessage(responseBody, defaultMessage) {
20
20
  return defaultMessage;
21
21
  try {
22
22
  const d = JSON.parse(responseBody);
23
- return d['details'] || d['error'] || d['message'] || defaultMessage;
23
+ // NestJS ValidationPipe returns { message: string[] | string, error: 'Bad
24
+ // Request', statusCode }. Prefer the detailed `message` over the generic
25
+ // `error` so the real cause (which field failed validation) is surfaced
26
+ // instead of a bare "Bad Request". (ZIG-832)
27
+ const msg = d['message'];
28
+ const detailed = Array.isArray(msg) ? msg.join('; ') : (typeof msg === 'string' ? msg : undefined);
29
+ return d['details'] || detailed || d['error'] || defaultMessage;
24
30
  }
25
31
  catch {
26
32
  return responseBody || defaultMessage;
@@ -117,7 +123,38 @@ export async function getActiveTasksForAgent(agentId, creds) {
117
123
  throwApiError(res, body, `Get active tasks for agent failed: ${res.status} ${res.statusText}`);
118
124
  }
119
125
  const data = await res.json().catch(() => null);
120
- return Array.isArray(data?.['tasks']) ? data['tasks'] : [];
126
+ const fromAgent = Array.isArray(data?.['tasks']) ? data['tasks'] : [];
127
+ if (fromAgent.length > 0)
128
+ return fromAgent;
129
+ // Fallback when /agents/:id/tasks is empty but party-agreement tasks exist (ZIG-802).
130
+ return getActiveTasksForAgentViaPartyAgreements(agentId, creds);
131
+ }
132
+ async function getActiveTasksForAgentViaPartyAgreements(agentId, creds) {
133
+ const { listAgreements } = await import('./AgreementClient.js');
134
+ const agreements = await listAgreements({ status: 'active' }, creds);
135
+ const partyIds = agreements
136
+ .filter((a) => {
137
+ const p = a.parties ?? {};
138
+ return (p.provider === agentId ||
139
+ p.providerAgent === agentId ||
140
+ p.creator === agentId ||
141
+ p.payer === agentId);
142
+ })
143
+ .map((a) => a.agreementId)
144
+ .filter(Boolean);
145
+ if (partyIds.length === 0)
146
+ return [];
147
+ const lists = await Promise.all(partyIds.map(async (aid) => {
148
+ const url = new URL(`${getBackendUrl()}/agreements/${encodeURIComponent(aid)}/tasks`);
149
+ url.searchParams.set('state', 'active');
150
+ const res = await fetch(url.toString(), { method: 'GET', headers: buildHeaders(creds) });
151
+ if (!res.ok)
152
+ return [];
153
+ const data = await res.json().catch(() => null);
154
+ return Array.isArray(data?.['tasks']) ? data['tasks'] : [];
155
+ }));
156
+ const merged = lists.flat();
157
+ return merged.filter((t) => !t.assigneeId || t.assigneeId === agentId);
121
158
  }
122
159
  // Backend deliberately has no `/chats/:id/tasks` route — it's composable
123
160
  // from links → per-agreement tasks. We do the composition here so callers
@@ -0,0 +1,39 @@
1
+ /**
2
+ * ZIG-646: the one grant shape the backend presents on the wire, mirrored for
3
+ * SDK consumers. Context, connection, and payment grants all serialise to this
4
+ * exact object (see backend `src/grants/grant-view.ts`), so a caller reads one
5
+ * shape to answer "what grants exist, and are they live?" regardless of rail.
6
+ *
7
+ * `scope.kind` names the rail's resource: context grants keep chat/agreement/org;
8
+ * a connection grant is `connection` + connectionId; a payment grant is
9
+ * `wallet` + the root wallet id. A grant's bounds are its `caveats` — for
10
+ * context grants (which have no caveat column) the temporal mode and read
11
+ * watermark are presented as `temporal` / `watermark_at` caveats.
12
+ */
13
+ export type GrantHealth = 'active' | 'expired' | 'revoked';
14
+ export type GrantScopeKind = 'chat' | 'agreement' | 'org' | 'connection' | 'wallet';
15
+ export interface GrantScopeView {
16
+ kind: GrantScopeKind;
17
+ id: string;
18
+ }
19
+ export interface GrantCaveatView {
20
+ type: string;
21
+ value: unknown;
22
+ }
23
+ export interface GrantView {
24
+ grantId: string;
25
+ issuerId: string;
26
+ holderId: string;
27
+ parentGrantId: string | null;
28
+ agreementId: string | null;
29
+ scope: GrantScopeView;
30
+ caveats: GrantCaveatView[];
31
+ revoked: boolean;
32
+ /** ISO-8601, or null for no expiry. */
33
+ expiresAt: string | null;
34
+ /** ISO-8601. */
35
+ createdAt: string;
36
+ health: GrantHealth;
37
+ }
38
+ /** Value of the first caveat of `type` on a grant, or undefined. */
39
+ export declare function grantCaveat(grant: GrantView, type: string): unknown | undefined;
@@ -0,0 +1,4 @@
1
+ /** Value of the first caveat of `type` on a grant, or undefined. */
2
+ export function grantCaveat(grant, type) {
3
+ return grant.caveats.find((c) => c.type === type)?.value;
4
+ }
@@ -6,14 +6,14 @@ export { MessagesClient } from './MessagesClient.js';
6
6
  export type { ListMessagesOptions, ListMessagesResult } from './MessagesClient.js';
7
7
  export { ArtifactsClient } from './ArtifactsClient.js';
8
8
  export type { ArtifactVisibility, ListArtifactsOptions, ListArtifactsQuery, ListArtifactsResult, WriteArtifactInput, } from './ArtifactsClient.js';
9
- export { ScopeClient } from './ScopeClient.js';
10
- export type { ScopeKind, ScopeResult, PartyRef } from './ScopeClient.js';
11
9
  export { ContextReadClient } from './ContextReadClient.js';
12
- export type { ContextReadType, ContextReadQuery, ContextReadEnvelope, } from './ContextReadClient.js';
10
+ export type { ContextReadType, ContextReadQuery, ContextReadEnvelope, ContextSnapshotResult, } from './ContextReadClient.js';
13
11
  export { ContextDiscoveryClient } from './ContextDiscoveryClient.js';
14
12
  export type { ContextReachDescriptor } from './ContextDiscoveryClient.js';
15
13
  export { ContextGrantsClient } from './ContextGrantsClient.js';
16
14
  export type { ContextGrantRecord, ContextGrantScope, ContextGrantScopeKind, ContextTemporal, IssueContextGrantInput, DelegateContextGrantInput, } from './ContextGrantsClient.js';
15
+ export { grantCaveat } from './grants.js';
16
+ export type { GrantView, GrantScopeKind, GrantScopeView, GrantCaveatView, GrantHealth, } from './grants.js';
17
17
  export { AgentSearchClient } from './AgentSearchClient.js';
18
18
  export { TelemetryClient } from './TelemetryClient.js';
19
19
  export { InboxClient } from './InboxClient.js';
@@ -4,10 +4,10 @@ export * from './MarketplaceClient.js';
4
4
  export * from './ChatClient.js';
5
5
  export { MessagesClient } from './MessagesClient.js';
6
6
  export { ArtifactsClient } from './ArtifactsClient.js';
7
- export { ScopeClient } from './ScopeClient.js';
8
7
  export { ContextReadClient } from './ContextReadClient.js';
9
8
  export { ContextDiscoveryClient } from './ContextDiscoveryClient.js';
10
9
  export { ContextGrantsClient } from './ContextGrantsClient.js';
10
+ export { grantCaveat } from './grants.js';
11
11
  export { AgentSearchClient } from './AgentSearchClient.js';
12
12
  export { TelemetryClient } from './TelemetryClient.js';
13
13
  export { InboxClient } from './InboxClient.js';
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './http/index.js';
2
+ export * from './relay/provisionRelayWorkers.js';
2
3
  export { WebSocketClient } from './websocket/index.js';
3
4
  export { createControlSocket } from './websocket/ControlSocket.js';
4
5
  export { ConnectionManager } from './ConnectionManager.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './http/index.js';
2
+ export * from './relay/provisionRelayWorkers.js';
2
3
  export { WebSocketClient } from './websocket/index.js';
3
4
  export { createControlSocket } from './websocket/ControlSocket.js';
4
5
  export { ConnectionManager } from './ConnectionManager.js';
@@ -0,0 +1,53 @@
1
+ import type { Agreement, Creds } from '../types.js';
2
+ import { claimOffer, delegateAgreement, getMyAgreements, pullOffers } from '../http/index.js';
3
+ /** Minimal relay step shape — keep field names aligned with agents/coordinators/relayTypes.ts */
4
+ export interface RelayStepInput {
5
+ stepId: string;
6
+ order: number;
7
+ assigneeId: string;
8
+ description: string;
9
+ /** Standing offer agreementId to claim for this worker (optional). */
10
+ offerAgreementId?: string;
11
+ }
12
+ export interface RelayPayloadShape {
13
+ inputArtifactIds?: string[];
14
+ steps: Array<{
15
+ stepId: string;
16
+ order: number;
17
+ agreementId: string;
18
+ assigneeId: string;
19
+ description: string;
20
+ }>;
21
+ }
22
+ export type ProvisionMethod = 'existing' | 'claim' | 'delegate';
23
+ export type StepProvisionStatus = 'active' | 'pending_approval';
24
+ export type ProvisionedRelayStep = RelayPayloadShape['steps'][number] & {
25
+ provisionMethod: ProvisionMethod;
26
+ status: StepProvisionStatus;
27
+ };
28
+ export interface RelayProvisionDeps {
29
+ getMyAgreements: typeof getMyAgreements;
30
+ pullOffers: typeof pullOffers;
31
+ claimOffer: typeof claimOffer;
32
+ delegateAgreement: typeof delegateAgreement;
33
+ }
34
+ export interface ProvisionRelayWorkersInput {
35
+ creds: Creds;
36
+ hireAgreementId: string;
37
+ chatId?: string;
38
+ steps: RelayStepInput[];
39
+ inputArtifactIds?: string[];
40
+ }
41
+ export interface ProvisionRelayWorkersResult {
42
+ payload: RelayPayloadShape;
43
+ steps: ProvisionedRelayStep[];
44
+ pendingApprovals: string[];
45
+ readyForKickoff: boolean;
46
+ }
47
+ type AgreementWithParent = Agreement & {
48
+ parentAgreementId?: string;
49
+ };
50
+ export declare function findExistingWorkerDelegation(hireAgreementId: string, assigneeId: string, agreements: AgreementWithParent[]): AgreementWithParent | null;
51
+ export declare function findOpenOfferForAgent(assigneeId: string, offers: Agreement[]): Agreement | null;
52
+ export declare function provisionRelayWorkers(input: ProvisionRelayWorkersInput, deps?: RelayProvisionDeps): Promise<ProvisionRelayWorkersResult>;
53
+ export {};
@@ -0,0 +1,147 @@
1
+ import { claimOffer, delegateAgreement, getMyAgreements, pullOffers, } from '../http/index.js';
2
+ function providerAgentId(agreement) {
3
+ return agreement.parties?.providerAgent ?? agreement.parties?.provider ?? null;
4
+ }
5
+ function isActiveAgreement(agreement) {
6
+ return agreement.status === 'active';
7
+ }
8
+ export function findExistingWorkerDelegation(hireAgreementId, assigneeId, agreements) {
9
+ const matches = agreements.filter((a) => {
10
+ if (a.parentAgreementId !== hireAgreementId)
11
+ return false;
12
+ if (!isActiveAgreement(a))
13
+ return false;
14
+ return providerAgentId(a) === assigneeId;
15
+ });
16
+ matches.sort((a, b) => {
17
+ const ta = a.updatedAt ?? a.createdAt ?? '';
18
+ const tb = b.updatedAt ?? b.createdAt ?? '';
19
+ return tb.localeCompare(ta);
20
+ });
21
+ return matches[0] ?? null;
22
+ }
23
+ export function findOpenOfferForAgent(assigneeId, offers) {
24
+ return (offers.find((o) => {
25
+ if (o.status !== 'open' && o.status !== 'active')
26
+ return false;
27
+ return providerAgentId(o) === assigneeId;
28
+ }) ?? null);
29
+ }
30
+ export async function provisionRelayWorkers(input, deps = {
31
+ getMyAgreements,
32
+ pullOffers,
33
+ claimOffer,
34
+ delegateAgreement,
35
+ }) {
36
+ const { creds, hireAgreementId, chatId, steps, inputArtifactIds } = input;
37
+ if (!steps.length)
38
+ throw new Error('steps must not be empty');
39
+ const myAgreements = (await deps.getMyAgreements({}, creds));
40
+ let offersCache = null;
41
+ const provisioned = [];
42
+ const pendingApprovals = [];
43
+ for (const step of steps) {
44
+ const existing = findExistingWorkerDelegation(hireAgreementId, step.assigneeId, myAgreements);
45
+ if (existing?.agreementId) {
46
+ provisioned.push({
47
+ stepId: step.stepId,
48
+ order: step.order,
49
+ agreementId: existing.agreementId,
50
+ assigneeId: step.assigneeId,
51
+ description: step.description,
52
+ provisionMethod: 'existing',
53
+ status: 'active',
54
+ });
55
+ continue;
56
+ }
57
+ if (step.offerAgreementId) {
58
+ const claimed = await deps.claimOffer(step.offerAgreementId, creds);
59
+ const agreementId = claimed.agreementId;
60
+ const status = isActiveAgreement(claimed)
61
+ ? 'active'
62
+ : 'pending_approval';
63
+ if (status === 'pending_approval')
64
+ pendingApprovals.push(agreementId);
65
+ provisioned.push({
66
+ stepId: step.stepId,
67
+ order: step.order,
68
+ agreementId,
69
+ assigneeId: step.assigneeId,
70
+ description: step.description,
71
+ provisionMethod: 'claim',
72
+ status,
73
+ });
74
+ continue;
75
+ }
76
+ if (!offersCache) {
77
+ offersCache = await deps.pullOffers({ limit: 100 }, creds);
78
+ }
79
+ const openOffer = findOpenOfferForAgent(step.assigneeId, offersCache);
80
+ if (openOffer?.agreementId) {
81
+ const claimed = await deps.claimOffer(openOffer.agreementId, creds);
82
+ const agreementId = claimed.agreementId;
83
+ const status = isActiveAgreement(claimed)
84
+ ? 'active'
85
+ : 'pending_approval';
86
+ if (status === 'pending_approval')
87
+ pendingApprovals.push(agreementId);
88
+ provisioned.push({
89
+ stepId: step.stepId,
90
+ order: step.order,
91
+ agreementId,
92
+ assigneeId: step.assigneeId,
93
+ description: step.description,
94
+ provisionMethod: 'claim',
95
+ status,
96
+ });
97
+ continue;
98
+ }
99
+ if (!chatId?.trim()) {
100
+ throw new Error(`No standing offer found for ${step.assigneeId} and chatId is required to propose a delegation under the hire`);
101
+ }
102
+ const delegated = await deps.delegateAgreement({
103
+ description: step.description,
104
+ executorId: step.assigneeId,
105
+ chatId: chatId.trim(),
106
+ parentAgreementId: hireAgreementId,
107
+ agreementDescription: `Relay step ${step.stepId}: ${step.description}`,
108
+ lifecycle: 'count-bound',
109
+ maxExecutions: 50,
110
+ price: 0,
111
+ }, creds);
112
+ const agreementId = delegated.agreementId;
113
+ const status = isActiveAgreement(delegated)
114
+ ? 'active'
115
+ : 'pending_approval';
116
+ if (status === 'pending_approval')
117
+ pendingApprovals.push(agreementId);
118
+ provisioned.push({
119
+ stepId: step.stepId,
120
+ order: step.order,
121
+ agreementId,
122
+ assigneeId: step.assigneeId,
123
+ description: step.description,
124
+ provisionMethod: 'delegate',
125
+ status,
126
+ });
127
+ }
128
+ provisioned.sort((a, b) => a.order - b.order);
129
+ const payload = {
130
+ inputArtifactIds,
131
+ steps: provisioned.map(({ stepId, order, agreementId, assigneeId, description }) => ({
132
+ stepId,
133
+ order,
134
+ agreementId,
135
+ assigneeId,
136
+ description,
137
+ })),
138
+ };
139
+ const readyForKickoff = pendingApprovals.length === 0 &&
140
+ provisioned.every((s) => s.status === 'active');
141
+ return {
142
+ payload,
143
+ steps: provisioned,
144
+ pendingApprovals,
145
+ readyForKickoff,
146
+ };
147
+ }
package/dist/types.d.ts CHANGED
@@ -173,5 +173,13 @@ export interface MessageMetadata {
173
173
  agreement?: Record<string, unknown> | null;
174
174
  operation?: string | null;
175
175
  agreementId?: string | null;
176
+ /**
177
+ * Message send time from the wire payload (ZIG-860). The live-path inbox
178
+ * watermark ack (AgentHost, ZIG-832 P4) needs it — without a timestamp the
179
+ * ack never fires and catch-up re-delivers the chat's whole backlog after a
180
+ * host eviction, re-running already-answered goals.
181
+ */
182
+ timestamp?: string;
183
+ sentTimestamp?: string;
176
184
  }
177
185
  export type MessageHandler = (text: string, metadata: MessageMetadata) => Promise<void>;
@@ -27,6 +27,12 @@ export interface ResourceEvent {
27
27
  taskId?: string;
28
28
  change?: 'created' | 'updated' | 'state-changed';
29
29
  reason?: string;
30
+ /**
31
+ * Principal whose action produced the event, when the backend knows it.
32
+ * Hosts drop events the receiving agent itself caused — its own writes
33
+ * aren't news to it.
34
+ */
35
+ actorId?: string;
30
36
  }
31
37
  export type ResourceEventHandler = (event: ResourceEvent) => void | Promise<void>;
32
38
  export interface SendOptions {
@@ -191,7 +191,7 @@ export class WebSocketClient {
191
191
  const agreement = p['agreement'] ?? null;
192
192
  const operation = p['operation'] ?? null;
193
193
  const agreementId = p['agreementId'] ?? null;
194
- runtimeLog.info(this.label, `[wire-debug] incoming chatId=${chatId} sender=${senderId} content_type=${p['content_type'] ?? p['contentType'] ?? '<none>'} entryType=${p['entryType'] ?? '<none>'} task?=${task ? `taskId=${task['taskId']} state=${task['state']}` : 'no'} agreement?=${agreement ? `agreementId=${agreement['agreementId']} status=${agreement['status']}` : 'no'} operation?=${operation ?? 'no'}`);
194
+ runtimeLog.info(this.label, `[wire-debug] incoming chatId=${chatId} sender=${senderId} content_type=${p['content_type'] ?? p['contentType'] ?? '<none>'} entryType=${p['entryType'] ?? '<none>'} ts=${typeof p['timestamp']}/${typeof p['sentTimestamp']} keys=${Object.keys(p).join(',')} task?=${task ? `taskId=${task['taskId']} state=${task['state']}` : 'no'} agreement?=${agreement ? `agreementId=${agreement['agreementId']} status=${agreement['status']}` : 'no'} operation?=${operation ?? 'no'}`);
195
195
  const metadata = {
196
196
  chatId,
197
197
  messageId: typeof p['messageId'] === 'string' ? p['messageId'] : undefined,
@@ -208,6 +208,11 @@ export class WebSocketClient {
208
208
  agreement,
209
209
  operation,
210
210
  agreementId,
211
+ // ZIG-860: carry the send time through — the live-path watermark ack
212
+ // (AgentHost, ZIG-832 P4) reads it, and dropping it here meant the ack
213
+ // never fired for any live message.
214
+ timestamp: typeof p['timestamp'] === 'string' ? p['timestamp'] : undefined,
215
+ sentTimestamp: typeof p['sentTimestamp'] === 'string' ? p['sentTimestamp'] : undefined,
211
216
  };
212
217
  await this.messageHandler(p['text'], metadata);
213
218
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/api-client",
3
- "version": "0.1.25",
3
+ "version": "0.1.28",
4
4
  "description": "HTTP and WebSocket client for the Ziggs backend API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,37 +0,0 @@
1
- import 'dotenv/config';
2
- export type ScopeKind = 'chat' | 'agreement' | 'task' | 'counterparty';
3
- export interface PartyRef {
4
- id: string;
5
- role: 'payer' | 'provider' | 'creator' | 'proposedTo' | 'providerPrincipal';
6
- }
7
- export interface ScopeResult {
8
- via: {
9
- kind: ScopeKind;
10
- id: string;
11
- };
12
- agent: string;
13
- accessible: {
14
- chats: string[];
15
- agreements: string[];
16
- tasks: string[];
17
- counterparties: PartyRef[];
18
- };
19
- permissions: string[];
20
- }
21
- /**
22
- * Client for `GET /scope?via=<kind>:<id>`. Returns the access graph reachable
23
- * from an entry point for the impersonated agent. Used by `runTurn` to
24
- * bootstrap "what does my current session-key resolve to" before fanning out
25
- * to primitive reads.
26
- */
27
- export declare class ScopeClient {
28
- private readonly operatorKey;
29
- private readonly agentId?;
30
- /**
31
- * @param operatorKey Agent-scoped or fleet operator key.
32
- * @param agentId Required for fleet keys (sent as X-Agent-Id). Omit for agent-scoped keys.
33
- */
34
- constructor(operatorKey: string, agentId?: string);
35
- get(kind: ScopeKind, id: string): Promise<ScopeResult>;
36
- private _headers;
37
- }
@@ -1,40 +0,0 @@
1
- import 'dotenv/config';
2
- import { getBackendUrl } from '../utils/urlUtils.js';
3
- import { buildOperatorHeaders } from './operatorHeaders.js';
4
- /**
5
- * Client for `GET /scope?via=<kind>:<id>`. Returns the access graph reachable
6
- * from an entry point for the impersonated agent. Used by `runTurn` to
7
- * bootstrap "what does my current session-key resolve to" before fanning out
8
- * to primitive reads.
9
- */
10
- export class ScopeClient {
11
- operatorKey;
12
- agentId;
13
- /**
14
- * @param operatorKey Agent-scoped or fleet operator key.
15
- * @param agentId Required for fleet keys (sent as X-Agent-Id). Omit for agent-scoped keys.
16
- */
17
- constructor(operatorKey, agentId) {
18
- if (!operatorKey)
19
- throw new Error('ScopeClient: operatorKey is required');
20
- this.operatorKey = operatorKey;
21
- this.agentId = agentId;
22
- }
23
- async get(kind, id) {
24
- if (!id)
25
- throw new Error('ScopeClient.get: id is required');
26
- const url = new URL(`${getBackendUrl()}/scope`);
27
- url.searchParams.set('via', `${kind}:${id}`);
28
- const res = await fetch(url.toString(), { headers: this._headers() });
29
- if (!res.ok) {
30
- const body = await res.text().catch(() => '');
31
- throw new Error(`ScopeClient.get ${res.status} ${res.statusText} ${body.slice(0, 200)}`);
32
- }
33
- return (await res.json());
34
- }
35
- _headers() {
36
- return buildOperatorHeaders(this.operatorKey, this.agentId, {
37
- 'content-type': 'application/json',
38
- });
39
- }
40
- }