@ziggs-ai/api-client 0.1.26 → 0.1.29

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.
@@ -170,7 +170,6 @@ export declare function claimAgreement(agreementId: string, creds: Creds): Promi
170
170
  ok: boolean;
171
171
  agreement: Agreement;
172
172
  }>;
173
- export declare function getAgreementsByChat(chatId: string, creds: Creds): Promise<unknown[]>;
174
173
  export type ChatLinkType = 'origin' | 'mention' | 'delegation' | 'join';
175
174
  export declare function linkAgreementToChat(agreementId: string, chatId: string, linkType: ChatLinkType | undefined, creds: Creds): Promise<unknown | null>;
176
175
  export declare function getChatsForAgreement(agreementId: string, creds: Creds): Promise<unknown[]>;
@@ -219,7 +218,6 @@ export declare class AgreementClient {
219
218
  ok: boolean;
220
219
  agreement: Agreement;
221
220
  }>;
222
- listByChat(chatId: string): Promise<unknown[]>;
223
221
  linkToChat(id: string, chatId: string, linkType?: ChatLinkType): Promise<unknown>;
224
222
  listChats(id: string): Promise<unknown[]>;
225
223
  join(id: string): Promise<{
@@ -414,32 +414,6 @@ export async function claimAgreement(agreementId, creds) {
414
414
  }
415
415
  return data;
416
416
  }
417
- // ---------------------------------------------------------------------------
418
- // Chat links
419
- // ---------------------------------------------------------------------------
420
- export async function getAgreementsByChat(chatId, creds) {
421
- if (!chatId)
422
- return [];
423
- assertCreds(creds, 'get agreements by chat');
424
- try {
425
- const res = await fetch(`${getAgreementBaseUrl()}/by-chat/${encodeURIComponent(chatId)}`, {
426
- method: 'GET',
427
- headers: buildHeaders(creds),
428
- });
429
- if (!res.ok) {
430
- const body = await res.text().catch(() => '');
431
- runtimeLog.warn('AgreementClient', `⚠️ Get agreements by chat failed: ${res.status} ${res.statusText} ${body?.slice(0, 200)}`);
432
- return [];
433
- }
434
- const data = await res.json().catch(() => null);
435
- const links = data?.['links'] ?? data?.['agreements'];
436
- return Array.isArray(links) ? links : [];
437
- }
438
- catch (e) {
439
- runtimeLog.warn('AgreementClient', `⚠️ Get agreements by chat failed: ${e.message}`);
440
- return [];
441
- }
442
- }
443
417
  export async function linkAgreementToChat(agreementId, chatId, linkType = 'mention', creds) {
444
418
  if (!agreementId || !chatId)
445
419
  return null;
@@ -618,7 +592,6 @@ export class AgreementClient {
618
592
  revoke(id) { return revokeAgreement(id, this.creds); }
619
593
  fulfill(id) { return fulfillAgreement(id, this.creds); }
620
594
  claimAgreement(id) { return claimAgreement(id, this.creds); }
621
- listByChat(chatId) { return getAgreementsByChat(chatId, this.creds); }
622
595
  linkToChat(id, chatId, linkType) { return linkAgreementToChat(id, chatId, linkType ?? 'mention', this.creds); }
623
596
  listChats(id) { return getChatsForAgreement(id, this.creds); }
624
597
  join(id) { return joinAgreement(id, this.creds); }
@@ -1,16 +1,4 @@
1
1
  import 'dotenv/config';
2
- export interface ContextReachDescriptor {
3
- grantId: string;
4
- scope: {
5
- kind: 'chat' | 'agreement' | 'org';
6
- id: string;
7
- };
8
- temporal: 'from-now' | 'from-start';
9
- watermarkAt: string;
10
- expiresAt: string | null;
11
- parentGrantId: string | null;
12
- createdAt: string;
13
- }
14
2
  /** Labels-only pointer to context the agent could request access to (P4). */
15
3
  export interface DiscoverableItem {
16
4
  type: 'chat';
@@ -22,7 +10,9 @@ export interface DiscoverableItem {
22
10
  orgId: string;
23
11
  }
24
12
  /**
25
- * ZIG-412 discovery — scope descriptors only, no content.
13
+ * P4 discovery — labels-only pointers to context the agent could REQUEST but
14
+ * does not yet hold (`GET /context/discovery/available`). Grants the agent
15
+ * already holds are listed via the unified `GrantsClient` (GET /grants).
26
16
  */
27
17
  export declare class ContextDiscoveryClient {
28
18
  private readonly operatorKey;
@@ -33,7 +23,6 @@ export declare class ContextDiscoveryClient {
33
23
  * @param agentId Required for fleet keys (sent as X-Agent-Id). Omit for agent-scoped keys.
34
24
  */
35
25
  constructor(operatorKey: string, agentId?: string, baseUrl?: string);
36
- discover(): Promise<ContextReachDescriptor[]>;
37
26
  /**
38
27
  * P4: what context EXISTS in the agent's engaged orgs that it does NOT hold
39
28
  * a grant for — so it can request access rather than fail blind. Labels only.
@@ -2,7 +2,9 @@ import 'dotenv/config';
2
2
  import { getBackendUrl } from '../utils/urlUtils.js';
3
3
  import { buildOperatorHeaders } from './operatorHeaders.js';
4
4
  /**
5
- * ZIG-412 discovery — scope descriptors only, no content.
5
+ * P4 discovery — labels-only pointers to context the agent could REQUEST but
6
+ * does not yet hold (`GET /context/discovery/available`). Grants the agent
7
+ * already holds are listed via the unified `GrantsClient` (GET /grants).
6
8
  */
7
9
  export class ContextDiscoveryClient {
8
10
  operatorKey;
@@ -20,17 +22,6 @@ export class ContextDiscoveryClient {
20
22
  this.agentId = agentId;
21
23
  this.baseUrl = baseUrl || getBackendUrl();
22
24
  }
23
- async discover() {
24
- const res = await fetch(`${this.baseUrl}/context/discovery`, {
25
- headers: buildOperatorHeaders(this.operatorKey, this.agentId),
26
- });
27
- const body = await res.text().catch(() => '');
28
- if (!res.ok) {
29
- throw new Error(`ContextDiscoveryClient.discover ${res.status} ${body.slice(0, 200)}`);
30
- }
31
- const parsed = JSON.parse(body);
32
- return parsed.reach ?? [];
33
- }
34
25
  /**
35
26
  * P4: what context EXISTS in the agent's engaged orgs that it does NOT hold
36
27
  * a grant for — so it can request access rather than fail blind. Labels only.
@@ -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
  }
@@ -0,0 +1,45 @@
1
+ import 'dotenv/config';
2
+ import type { GrantView, GrantScopeKind, GrantHealth } from './grants.js';
3
+ export interface ListGrantsQuery {
4
+ /**
5
+ * Filter to grants held by this agent. Admin-gated server-side when the caller
6
+ * is not impersonating that agent.
7
+ */
8
+ holderId?: string;
9
+ /**
10
+ * Rail/resource filter. One kind or several (the context rail spans three
11
+ * kinds: chat, agreement, org). Absent = every rail.
12
+ */
13
+ scopeKind?: GrantScopeKind | GrantScopeKind[];
14
+ /** Filter to grants of one health. */
15
+ health?: GrantHealth;
16
+ cursor?: string;
17
+ limit?: number;
18
+ }
19
+ export interface ListGrantsResult {
20
+ items: GrantView[];
21
+ nextCursor: string | null;
22
+ hasMore: boolean;
23
+ }
24
+ /**
25
+ * ZIG-648 — unified grant listing across every rail. `GET /grants` returns the
26
+ * canonical GrantView for each grant the caller holds (or, admin-gated, a named
27
+ * holder), filterable by scope kind and health, with cursor pagination. Replaces
28
+ * the old per-rail listers (context discovery, connection-grants,
29
+ * payment-grants).
30
+ */
31
+ export declare class GrantsClient {
32
+ private readonly operatorKey;
33
+ private readonly agentId?;
34
+ private readonly baseUrl;
35
+ constructor(operatorKey: string, agentId?: string, baseUrl?: string);
36
+ listGrants(query?: ListGrantsQuery): Promise<ListGrantsResult>;
37
+ /**
38
+ * Every grant matching `query`, following the cursor to completion. Use when a
39
+ * caller needs the whole set (reach tagging, catch-up, an owner listing) rather
40
+ * than a single page — `listGrants` returns only one page (server default 30),
41
+ * so a holder with more grants than a page would otherwise be silently
42
+ * truncated. `maxPages` bounds the loop as a runaway guard.
43
+ */
44
+ listAllGrants(query?: Omit<ListGrantsQuery, 'cursor'>, maxPages?: number): Promise<GrantView[]>;
45
+ }
@@ -0,0 +1,72 @@
1
+ import 'dotenv/config';
2
+ import { getBackendUrl } from '../utils/urlUtils.js';
3
+ import { buildOperatorHeaders } from './operatorHeaders.js';
4
+ /**
5
+ * ZIG-648 — unified grant listing across every rail. `GET /grants` returns the
6
+ * canonical GrantView for each grant the caller holds (or, admin-gated, a named
7
+ * holder), filterable by scope kind and health, with cursor pagination. Replaces
8
+ * the old per-rail listers (context discovery, connection-grants,
9
+ * payment-grants).
10
+ */
11
+ export class GrantsClient {
12
+ operatorKey;
13
+ agentId;
14
+ baseUrl;
15
+ constructor(operatorKey, agentId, baseUrl) {
16
+ if (!operatorKey)
17
+ throw new Error('GrantsClient: operatorKey is required');
18
+ this.operatorKey = operatorKey;
19
+ this.agentId = agentId;
20
+ this.baseUrl = baseUrl || getBackendUrl();
21
+ }
22
+ async listGrants(query = {}) {
23
+ const url = new URL(`${this.baseUrl}/grants`);
24
+ if (query.holderId)
25
+ url.searchParams.set('holderId', query.holderId);
26
+ if (query.scopeKind) {
27
+ const kinds = Array.isArray(query.scopeKind)
28
+ ? query.scopeKind
29
+ : [query.scopeKind];
30
+ for (const k of kinds)
31
+ url.searchParams.append('scopeKind', k);
32
+ }
33
+ if (query.health)
34
+ url.searchParams.set('health', query.health);
35
+ if (query.cursor)
36
+ url.searchParams.set('cursor', query.cursor);
37
+ if (query.limit != null)
38
+ url.searchParams.set('limit', String(query.limit));
39
+ const res = await fetch(url.toString(), {
40
+ headers: buildOperatorHeaders(this.operatorKey, this.agentId),
41
+ });
42
+ const body = await res.text().catch(() => '');
43
+ if (!res.ok) {
44
+ throw new Error(`GrantsClient.listGrants ${res.status} ${body.slice(0, 200)}`);
45
+ }
46
+ const parsed = JSON.parse(body);
47
+ return {
48
+ items: parsed.items ?? [],
49
+ nextCursor: parsed.nextCursor ?? null,
50
+ hasMore: parsed.hasMore ?? false,
51
+ };
52
+ }
53
+ /**
54
+ * Every grant matching `query`, following the cursor to completion. Use when a
55
+ * caller needs the whole set (reach tagging, catch-up, an owner listing) rather
56
+ * than a single page — `listGrants` returns only one page (server default 30),
57
+ * so a holder with more grants than a page would otherwise be silently
58
+ * truncated. `maxPages` bounds the loop as a runaway guard.
59
+ */
60
+ async listAllGrants(query = {}, maxPages = 50) {
61
+ const all = [];
62
+ let cursor;
63
+ for (let page = 0; page < maxPages; page++) {
64
+ const res = await this.listGrants({ ...query, cursor });
65
+ all.push(...res.items);
66
+ if (!res.nextCursor)
67
+ return all;
68
+ cursor = res.nextCursor;
69
+ }
70
+ return all;
71
+ }
72
+ }
@@ -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;
@@ -15,6 +15,12 @@ export type GrantScopeKind = 'chat' | 'agreement' | 'org' | 'connection' | 'wall
15
15
  export interface GrantScopeView {
16
16
  kind: GrantScopeKind;
17
17
  id: string;
18
+ /**
19
+ * Human-readable name for the scoped resource, populated only by the unified
20
+ * list read (GET /grants / GrantsClient); the per-rail issue/delegate
21
+ * responses omit it. Optional so the one shape stays additive.
22
+ */
23
+ label?: string;
18
24
  }
19
25
  export interface GrantCaveatView {
20
26
  type: string;
@@ -6,12 +6,12 @@ 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
- export type { ContextReachDescriptor } from './ContextDiscoveryClient.js';
12
+ export type { DiscoverableItem } from './ContextDiscoveryClient.js';
13
+ export { GrantsClient } from './GrantsClient.js';
14
+ export type { ListGrantsQuery, ListGrantsResult } from './GrantsClient.js';
15
15
  export { ContextGrantsClient } from './ContextGrantsClient.js';
16
16
  export type { ContextGrantRecord, ContextGrantScope, ContextGrantScopeKind, ContextTemporal, IssueContextGrantInput, DelegateContextGrantInput, } from './ContextGrantsClient.js';
17
17
  export { grantCaveat } from './grants.js';
@@ -4,9 +4,9 @@ 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';
9
+ export { GrantsClient } from './GrantsClient.js';
10
10
  export { ContextGrantsClient } from './ContextGrantsClient.js';
11
11
  export { grantCaveat } from './grants.js';
12
12
  export { AgentSearchClient } from './AgentSearchClient.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>;
@@ -109,6 +109,15 @@ export class WebSocketClient {
109
109
  this.socket.on('error', (error) => {
110
110
  runtimeLog.error(this.label, `Socket.IO error: ${String(error)}`);
111
111
  });
112
+ // ZIG-860: the send path is fire-and-forget (no ack callback), so the
113
+ // backend's structured rejection is the ONLY failure signal. Not listening
114
+ // made every rejected send silent — an agent's message just vanished (the
115
+ // a2a schema bug hid behind exactly this). Log loudly; delivery recovery
116
+ // stays with the inbox/catch-up machinery.
117
+ this.socket.on('chat:error', (payload) => {
118
+ const p = (payload ?? {});
119
+ runtimeLog.error(this.label, `chat:error [${p['code'] ?? 'UNKNOWN'}] chatId=${p['chatId'] ?? '-'} messageId=${p['messageId'] ?? '-'}: ${p['message'] ?? ''}`);
120
+ });
112
121
  }
113
122
  connectAsync(timeout = 10_000) {
114
123
  if (this.socket?.connected)
@@ -191,7 +200,7 @@ export class WebSocketClient {
191
200
  const agreement = p['agreement'] ?? null;
192
201
  const operation = p['operation'] ?? null;
193
202
  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'}`);
203
+ 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
204
  const metadata = {
196
205
  chatId,
197
206
  messageId: typeof p['messageId'] === 'string' ? p['messageId'] : undefined,
@@ -208,6 +217,11 @@ export class WebSocketClient {
208
217
  agreement,
209
218
  operation,
210
219
  agreementId,
220
+ // ZIG-860: carry the send time through — the live-path watermark ack
221
+ // (AgentHost, ZIG-832 P4) reads it, and dropping it here meant the ack
222
+ // never fired for any live message.
223
+ timestamp: typeof p['timestamp'] === 'string' ? p['timestamp'] : undefined,
224
+ sentTimestamp: typeof p['sentTimestamp'] === 'string' ? p['sentTimestamp'] : undefined,
211
225
  };
212
226
  await this.messageHandler(p['text'], metadata);
213
227
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/api-client",
3
- "version": "0.1.26",
3
+ "version": "0.1.29",
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
- }