@ziggs-ai/api-client 0.1.3 → 0.1.5
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.
- package/README.md +13 -7
- package/dist/ConnectionManager.d.ts +46 -0
- package/dist/ConnectionManager.js +132 -0
- package/dist/http/AgentSearchClient.d.ts +36 -0
- package/dist/http/AgentSearchClient.js +72 -0
- package/dist/http/AgreementClient.d.ts +153 -0
- package/dist/http/AgreementClient.js +477 -0
- package/dist/http/ArtifactsClient.d.ts +48 -0
- package/dist/http/ArtifactsClient.js +90 -0
- package/dist/http/ChatClient.d.ts +34 -0
- package/dist/http/ChatClient.js +104 -0
- package/dist/http/ContextDiscoveryClient.d.ts +23 -0
- package/dist/http/ContextDiscoveryClient.js +35 -0
- package/dist/http/ContextReadClient.d.ts +33 -0
- package/dist/http/ContextReadClient.js +54 -0
- package/dist/http/MarketplaceClient.d.ts +19 -0
- package/dist/http/MarketplaceClient.js +72 -0
- package/dist/http/MessagesClient.d.ts +26 -0
- package/dist/http/MessagesClient.js +49 -0
- package/dist/http/ScopeClient.d.ts +33 -0
- package/dist/http/ScopeClient.js +39 -0
- package/dist/http/TaskClient.d.ts +75 -0
- package/dist/http/TaskClient.js +352 -0
- package/dist/http/TelemetryClient.d.ts +11 -0
- package/dist/http/TelemetryClient.js +53 -0
- package/dist/http/index.d.ts +16 -0
- package/dist/http/index.js +11 -0
- package/dist/index.d.ts +9 -0
- package/{src → dist}/index.js +2 -12
- package/dist/shared/runtimeLog.d.ts +14 -0
- package/dist/shared/runtimeLog.js +64 -0
- package/dist/types.d.ts +130 -0
- package/dist/types.js +50 -0
- package/dist/utils/urlUtils.d.ts +2 -0
- package/dist/utils/urlUtils.js +8 -0
- package/dist/websocket/ControlSocket.d.ts +13 -0
- package/dist/websocket/ControlSocket.js +37 -0
- package/dist/websocket/WebSocketClient.d.ts +71 -0
- package/dist/websocket/WebSocketClient.js +233 -0
- package/dist/websocket/index.js +1 -0
- package/package.json +20 -7
- package/src/ConnectionManager.ts +172 -0
- package/src/http/AgentSearchClient.ts +115 -0
- package/src/http/AgreementClient.ts +721 -0
- package/src/http/ArtifactsClient.ts +133 -0
- package/src/http/ChatClient.ts +147 -0
- package/src/http/ContextDiscoveryClient.ts +52 -0
- package/src/http/ContextReadClient.ts +83 -0
- package/src/http/MarketplaceClient.ts +94 -0
- package/src/http/MessagesClient.ts +71 -0
- package/src/http/ScopeClient.ts +64 -0
- package/src/http/TaskClient.ts +450 -0
- package/src/http/{TelemetryClient.js → TelemetryClient.ts} +21 -7
- package/src/http/index.ts +26 -0
- package/src/index.ts +27 -0
- package/src/shared/runtimeLog.ts +68 -0
- package/src/types.ts +158 -0
- package/src/utils/urlUtils.ts +9 -0
- package/src/websocket/ControlSocket.ts +51 -0
- package/src/websocket/WebSocketClient.ts +315 -0
- package/src/websocket/index.ts +1 -0
- package/src/ConnectionManager.js +0 -179
- package/src/http/AgentSearchClient.js +0 -113
- package/src/http/ContextReader.js +0 -99
- package/src/http/ContextWriter.js +0 -98
- package/src/http/TaskClient.js +0 -612
- package/src/http/index.js +0 -6
- package/src/types.js +0 -28
- package/src/utils/urlUtils.js +0 -17
- package/src/websocket/ControlSocket.js +0 -55
- package/src/websocket/WebSocketClient.js +0 -318
- /package/{src/websocket/index.js → dist/websocket/index.d.ts} +0 -0
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
3
|
+
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
4
|
+
import { type Creds, type Agreement, type EngagementKind, OPEN_AGREEMENT_TARGET, ApiError } from '../types.js';
|
|
5
|
+
import { publishToLedger, type PublishToLedgerPayload } from './TaskClient.js';
|
|
6
|
+
|
|
7
|
+
// Lazy: read at call time so dotenv loaded after this module is imported
|
|
8
|
+
// still takes effect. Baking it at module-load time would freeze the URL
|
|
9
|
+
// before the caller's dotenv.config() runs.
|
|
10
|
+
function getAgreementBaseUrl(): string {
|
|
11
|
+
return `${getBackendUrl()}/agreements`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function buildHeaders(creds: Creds): Record<string, string> {
|
|
15
|
+
return {
|
|
16
|
+
'content-type': 'application/json',
|
|
17
|
+
Authorization: `Bearer ${creds.operatorKey}`,
|
|
18
|
+
'X-Agent-Id': creds.agentId,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function assertCreds(creds: Creds | undefined | null, op: string): asserts creds is Creds {
|
|
23
|
+
if (!creds?.operatorKey) throw new Error(`operatorKey is required for ${op}`);
|
|
24
|
+
if (!creds?.agentId) throw new Error(`agentId is required for ${op}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseErrorMessage(responseBody: string, defaultMessage: string): string {
|
|
28
|
+
if (!responseBody) return defaultMessage;
|
|
29
|
+
try {
|
|
30
|
+
const d = JSON.parse(responseBody) as Record<string, unknown>;
|
|
31
|
+
return (d['details'] as string) || (d['error'] as string) || (d['message'] as string) || defaultMessage;
|
|
32
|
+
} catch {
|
|
33
|
+
return responseBody || defaultMessage;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function throwApiError(response: Response, responseBody: string, defaultMessage: string): never {
|
|
38
|
+
throw new ApiError(parseErrorMessage(responseBody, defaultMessage), response.status, responseBody);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Proposal lifecycle
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
export type PlanReviewTiming = 'with_proposal' | 'before_execution';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Shared proposal terms. When `engagementKind` is omitted the server defaults to `service`.
|
|
49
|
+
* Use `proposedTo: OPEN_AGREEMENT_TARGET` (`"everyone"`) for open buyer-broadcast quests.
|
|
50
|
+
*/
|
|
51
|
+
export interface ProposeTerms {
|
|
52
|
+
description: string;
|
|
53
|
+
price?: number;
|
|
54
|
+
lifecycle?: string;
|
|
55
|
+
expiresAt?: string;
|
|
56
|
+
maxExecutions?: number;
|
|
57
|
+
agreementDescription?: string;
|
|
58
|
+
parentAgreementId?: string;
|
|
59
|
+
parentTaskId?: string;
|
|
60
|
+
payerId?: string;
|
|
61
|
+
providerId?: string;
|
|
62
|
+
plan?: unknown;
|
|
63
|
+
planReviewTiming?: PlanReviewTiming;
|
|
64
|
+
requireMidWorkPlanAck?: boolean;
|
|
65
|
+
/** Defaults to `service` on the server when omitted. Set `hire` for representation contracts. */
|
|
66
|
+
engagementKind?: EngagementKind;
|
|
67
|
+
idempotencyKey?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 1:1 proposal to a specific user or agent (`proposedTo` = their id). */
|
|
71
|
+
export interface ProposeDirectInput extends ProposeTerms {
|
|
72
|
+
proposedTo: string;
|
|
73
|
+
chatId: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Open buyer-broadcast: any agent may claim (`proposedTo` is set to `"everyone"`). */
|
|
77
|
+
export type ProposeBroadcastInput = Omit<ProposeDirectInput, 'proposedTo'>;
|
|
78
|
+
|
|
79
|
+
/** @deprecated Alias for {@link ProposeDirectInput}. */
|
|
80
|
+
export type ProposeAgreementData = ProposeDirectInput;
|
|
81
|
+
|
|
82
|
+
export async function proposeAgreement(
|
|
83
|
+
proposalData: ProposeDirectInput,
|
|
84
|
+
creds: Creds,
|
|
85
|
+
): Promise<Agreement> {
|
|
86
|
+
if (!proposalData) throw new Error('Proposal data is required for proposal creation');
|
|
87
|
+
assertCreds(creds, 'proposal creation');
|
|
88
|
+
|
|
89
|
+
const { idempotencyKey, ...bodyData } = proposalData;
|
|
90
|
+
const headers = buildHeaders(creds);
|
|
91
|
+
if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;
|
|
92
|
+
|
|
93
|
+
// ZIG-207: canonical REST path. Backend keeps /propose serving the
|
|
94
|
+
// identical handler with a Deprecation/Sunset header until PR-F removes it.
|
|
95
|
+
const res = await fetch(`${getAgreementBaseUrl()}/proposals`, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers,
|
|
98
|
+
body: JSON.stringify(bodyData),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (!res.ok) {
|
|
102
|
+
const body = await res.text().catch(() => '');
|
|
103
|
+
throwApiError(res, body, `Proposal creation failed: ${res.status} ${res.statusText}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const data = await res.json().catch(() => null) as Record<string, unknown> | null;
|
|
107
|
+
if (!data?.['agreement']) {
|
|
108
|
+
throw new Error('Invalid response: expected { agreement } from POST /agreements/proposals');
|
|
109
|
+
}
|
|
110
|
+
return data['agreement'] as Agreement;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Propose a contract to one party (user or agent). Server defaults `engagementKind` to `service`. */
|
|
114
|
+
export async function proposeDirectTo(
|
|
115
|
+
input: ProposeDirectInput,
|
|
116
|
+
creds: Creds,
|
|
117
|
+
): Promise<Agreement> {
|
|
118
|
+
return proposeAgreement(input, creds);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Propose an open quest (`proposedTo: "everyone"`). Server defaults `engagementKind` to `service`. */
|
|
122
|
+
export async function proposeBroadcast(
|
|
123
|
+
input: ProposeBroadcastInput,
|
|
124
|
+
creds: Creds,
|
|
125
|
+
): Promise<Agreement> {
|
|
126
|
+
return proposeAgreement({ ...input, proposedTo: OPEN_AGREEMENT_TARGET }, creds);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface DelegateAgreementData {
|
|
130
|
+
description: string;
|
|
131
|
+
executorId: string;
|
|
132
|
+
chatId: string;
|
|
133
|
+
parentAgreementId: string;
|
|
134
|
+
parentTaskId?: string;
|
|
135
|
+
price?: number;
|
|
136
|
+
lifecycle?: string;
|
|
137
|
+
expiresAt?: string;
|
|
138
|
+
maxExecutions?: number;
|
|
139
|
+
agreementDescription?: string;
|
|
140
|
+
payerId?: string;
|
|
141
|
+
plan?: unknown;
|
|
142
|
+
planReviewTiming?: PlanReviewTiming;
|
|
143
|
+
requireMidWorkPlanAck?: boolean;
|
|
144
|
+
idempotencyKey?: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function delegateAgreement(
|
|
148
|
+
proposalData: DelegateAgreementData,
|
|
149
|
+
creds: Creds,
|
|
150
|
+
): Promise<Agreement> {
|
|
151
|
+
if (!proposalData) throw new Error('Proposal data is required for delegation');
|
|
152
|
+
if (!proposalData.parentAgreementId)
|
|
153
|
+
throw new Error('parentAgreementId is required for delegation');
|
|
154
|
+
assertCreds(creds, 'proposal creation');
|
|
155
|
+
|
|
156
|
+
const { idempotencyKey, parentAgreementId } = proposalData;
|
|
157
|
+
const headers = buildHeaders(creds);
|
|
158
|
+
if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;
|
|
159
|
+
|
|
160
|
+
// ZIG-207 / ZIG-270: parentAgreementId in path AND body. Backend's
|
|
161
|
+
// `DelegateTaskDto` validates `parentAgreementId` as a required string;
|
|
162
|
+
// controller (`POST /agreements/:parentAgreementId/delegations`) merges
|
|
163
|
+
// path over body, so duplicating the value is harmless. Stripping it
|
|
164
|
+
// from the body (as ZIG-207 #72 did) trips DTO validation and returns
|
|
165
|
+
// `400: parentAgreementId must be a string` → infinite retry loop in
|
|
166
|
+
// `agreement_subcontract`. Send both, let the path win.
|
|
167
|
+
const bodyWithParent = { ...proposalData };
|
|
168
|
+
delete (bodyWithParent as { idempotencyKey?: string }).idempotencyKey;
|
|
169
|
+
const res = await fetch(
|
|
170
|
+
`${getAgreementBaseUrl()}/${encodeURIComponent(parentAgreementId)}/delegations`,
|
|
171
|
+
{
|
|
172
|
+
method: 'POST',
|
|
173
|
+
headers,
|
|
174
|
+
body: JSON.stringify(bodyWithParent),
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (!res.ok) {
|
|
179
|
+
const body = await res.text().catch(() => '');
|
|
180
|
+
throwApiError(res, body, `Delegation failed: ${res.status} ${res.statusText}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const data = await res.json().catch(() => null) as Record<string, unknown> | null;
|
|
184
|
+
if (!data?.['agreement']) {
|
|
185
|
+
throw new Error('Invalid response: expected { agreement } from POST /agreements/:parentAgreementId/delegations');
|
|
186
|
+
}
|
|
187
|
+
return data['agreement'] as Agreement;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function respondToAgreement(
|
|
191
|
+
agreementId: string,
|
|
192
|
+
action: 'approve' | 'reject',
|
|
193
|
+
creds: Creds,
|
|
194
|
+
): Promise<Agreement> {
|
|
195
|
+
if (!agreementId || !action) throw new Error('Agreement ID and action are required for proposal response');
|
|
196
|
+
assertCreds(creds, 'proposal response');
|
|
197
|
+
|
|
198
|
+
const res = await fetch(`${getAgreementBaseUrl()}/${agreementId}/respond`, {
|
|
199
|
+
method: 'POST',
|
|
200
|
+
headers: buildHeaders(creds),
|
|
201
|
+
body: JSON.stringify({ action }),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (!res.ok) {
|
|
205
|
+
const body = await res.text().catch(() => '');
|
|
206
|
+
throwApiError(res, body, `Proposal response failed: ${res.status} ${res.statusText}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const data = await res.json().catch(() => null) as Record<string, unknown> | null;
|
|
210
|
+
if (!data?.['agreement']) {
|
|
211
|
+
throw new Error('Invalid response: expected { agreement } from /agreements/:id/respond');
|
|
212
|
+
}
|
|
213
|
+
return data['agreement'] as Agreement;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface CounterAgreementData {
|
|
217
|
+
price?: number;
|
|
218
|
+
agreementDescription?: string;
|
|
219
|
+
expiresAt?: string;
|
|
220
|
+
lifecycle?: string;
|
|
221
|
+
maxExecutions?: number;
|
|
222
|
+
description?: string;
|
|
223
|
+
/** Override plan `{ steps: [...] }`; omit to copy from original proposal task */
|
|
224
|
+
plan?: unknown;
|
|
225
|
+
planReviewTiming?: PlanReviewTiming;
|
|
226
|
+
requireMidWorkPlanAck?: boolean;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function counterAgreement(
|
|
230
|
+
agreementId: string,
|
|
231
|
+
counter: CounterAgreementData,
|
|
232
|
+
creds: Creds,
|
|
233
|
+
): Promise<Agreement> {
|
|
234
|
+
if (!agreementId) throw new Error('agreementId is required for counter-offer');
|
|
235
|
+
assertCreds(creds, 'counter proposal');
|
|
236
|
+
|
|
237
|
+
const res = await fetch(`${getAgreementBaseUrl()}/${agreementId}/counter`, {
|
|
238
|
+
method: 'POST',
|
|
239
|
+
headers: buildHeaders(creds),
|
|
240
|
+
body: JSON.stringify(counter || {}),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
if (!res.ok) {
|
|
244
|
+
const body = await res.text().catch(() => '');
|
|
245
|
+
throwApiError(res, body, `Counter proposal failed: ${res.status} ${res.statusText}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const data = await res.json().catch(() => null) as Record<string, unknown> | null;
|
|
249
|
+
if (!data?.['agreement']) {
|
|
250
|
+
throw new Error('Invalid response: expected { agreement } from /agreements/:id/counter');
|
|
251
|
+
}
|
|
252
|
+
return data['agreement'] as Agreement;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function getAgreementStatus(agreementId: string, creds: Creds): Promise<unknown | null> {
|
|
256
|
+
if (!agreementId) return null;
|
|
257
|
+
assertCreds(creds, 'proposal status read');
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const res = await fetch(`${getAgreementBaseUrl()}/${agreementId}/status`, {
|
|
261
|
+
method: 'GET',
|
|
262
|
+
headers: buildHeaders(creds),
|
|
263
|
+
});
|
|
264
|
+
if (res.status === 404) return null;
|
|
265
|
+
if (!res.ok) {
|
|
266
|
+
const body = await res.text().catch(() => '');
|
|
267
|
+
runtimeLog.warn('AgreementClient', `⚠️ Proposal status read failed: ${res.status} ${res.statusText} ${body}`);
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
return await res.json().catch(() => null);
|
|
271
|
+
} catch (e) {
|
|
272
|
+
runtimeLog.warn('AgreementClient', `⚠️ Proposal status read failed: ${(e as Error).message}`);
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
// CRUD
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
|
|
281
|
+
export interface ListAgreementsFilters {
|
|
282
|
+
status?: string;
|
|
283
|
+
engagementKind?: EngagementKind;
|
|
284
|
+
proposalStatus?: string;
|
|
285
|
+
hasTask?: boolean;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export async function listAgreements(filters: ListAgreementsFilters = {}, creds: Creds): Promise<Agreement[]> {
|
|
289
|
+
assertCreds(creds, 'list agreements');
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
// Canonical query path: GET /agreements?scope=&status=&engagementKind=&...
|
|
293
|
+
const url = new URL(getAgreementBaseUrl());
|
|
294
|
+
if (filters.status) url.searchParams.set('status', filters.status);
|
|
295
|
+
if (filters.engagementKind) url.searchParams.set('engagementKind', filters.engagementKind);
|
|
296
|
+
if (filters.proposalStatus) url.searchParams.set('proposalStatus', filters.proposalStatus);
|
|
297
|
+
if (filters.hasTask != null) url.searchParams.set('hasTask', String(filters.hasTask));
|
|
298
|
+
|
|
299
|
+
const res = await fetch(url.toString(), { method: 'GET', headers: buildHeaders(creds) });
|
|
300
|
+
|
|
301
|
+
if (!res.ok) {
|
|
302
|
+
const body = await res.text().catch(() => '');
|
|
303
|
+
runtimeLog.warn('AgreementClient', `⚠️ List agreements failed: ${res.status} ${res.statusText} ${body?.slice(0, 200)}`);
|
|
304
|
+
return [];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const data = await res.json().catch(() => null) as Record<string, unknown> | null;
|
|
308
|
+
return Array.isArray(data?.['agreements']) ? (data['agreements'] as Agreement[]) : [];
|
|
309
|
+
} catch (e) {
|
|
310
|
+
runtimeLog.warn('AgreementClient', `⚠️ List agreements failed: ${(e as Error).message}`);
|
|
311
|
+
return [];
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export interface GetMyAgreementsFilters {
|
|
316
|
+
engagementKind?: EngagementKind;
|
|
317
|
+
proposalStatus?: string;
|
|
318
|
+
hasTask?: boolean;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export async function getMyAgreements(filters: GetMyAgreementsFilters = {}, creds: Creds): Promise<Agreement[]> {
|
|
322
|
+
assertCreds(creds, 'get my agreements');
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
// Canonical query path: GET /agreements?scope=mine returns the enriched
|
|
326
|
+
// shape (tasks + originChatId + isYou flags).
|
|
327
|
+
const url = new URL(getAgreementBaseUrl());
|
|
328
|
+
url.searchParams.set('scope', 'mine');
|
|
329
|
+
if (filters.engagementKind) url.searchParams.set('engagementKind', filters.engagementKind);
|
|
330
|
+
if (filters.proposalStatus) url.searchParams.set('proposalStatus', filters.proposalStatus);
|
|
331
|
+
if (filters.hasTask != null) url.searchParams.set('hasTask', String(filters.hasTask));
|
|
332
|
+
|
|
333
|
+
const res = await fetch(url.toString(), { method: 'GET', headers: buildHeaders(creds) });
|
|
334
|
+
|
|
335
|
+
if (!res.ok) {
|
|
336
|
+
const body = await res.text().catch(() => '');
|
|
337
|
+
runtimeLog.warn('AgreementClient', `⚠️ Get my agreements failed: ${res.status} ${res.statusText} ${body?.slice(0, 200)}`);
|
|
338
|
+
return [];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const data = await res.json().catch(() => null) as Record<string, unknown> | null;
|
|
342
|
+
return Array.isArray(data?.['agreements']) ? (data['agreements'] as Agreement[]) : [];
|
|
343
|
+
} catch (e) {
|
|
344
|
+
runtimeLog.warn('AgreementClient', `⚠️ Get my agreements failed: ${(e as Error).message}`);
|
|
345
|
+
return [];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export async function getAgreement(agreementId: string, creds: Creds): Promise<Agreement | null> {
|
|
350
|
+
if (!agreementId) return null;
|
|
351
|
+
assertCreds(creds, 'get agreement');
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const res = await fetch(`${getAgreementBaseUrl()}/${agreementId}`, {
|
|
355
|
+
method: 'GET',
|
|
356
|
+
headers: buildHeaders(creds),
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
if (res.status === 404) return null;
|
|
360
|
+
if (!res.ok) {
|
|
361
|
+
const body = await res.text().catch(() => '');
|
|
362
|
+
runtimeLog.warn('AgreementClient', `⚠️ Get agreement failed: ${res.status} ${res.statusText} ${body?.slice(0, 200)}`);
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const data = await res.json().catch(() => null) as Record<string, unknown> | null;
|
|
367
|
+
return (data?.['agreement'] as Agreement) ?? null;
|
|
368
|
+
} catch (e) {
|
|
369
|
+
runtimeLog.warn('AgreementClient', `⚠️ Get agreement failed: ${(e as Error).message}`);
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export interface CreateAgreementBody {
|
|
375
|
+
proposedToId?: string;
|
|
376
|
+
providerId?: string;
|
|
377
|
+
agentId?: string;
|
|
378
|
+
price?: number;
|
|
379
|
+
lifecycle?: string;
|
|
380
|
+
expiresAt?: string;
|
|
381
|
+
maxExecutions?: number;
|
|
382
|
+
description?: string;
|
|
383
|
+
engagementKind?: EngagementKind;
|
|
384
|
+
metadata?: Record<string, unknown>;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export async function createAgreement(
|
|
388
|
+
body: CreateAgreementBody,
|
|
389
|
+
creds: Creds,
|
|
390
|
+
): Promise<{ ok: boolean; agreement: Agreement }> {
|
|
391
|
+
if (!body) throw new Error('Body is required for agreement creation');
|
|
392
|
+
assertCreds(creds, 'agreement creation');
|
|
393
|
+
|
|
394
|
+
// Canonical REST create: POST /agreements.
|
|
395
|
+
const res = await fetch(getAgreementBaseUrl(), {
|
|
396
|
+
method: 'POST',
|
|
397
|
+
headers: buildHeaders(creds),
|
|
398
|
+
body: JSON.stringify(body),
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
if (!res.ok) {
|
|
402
|
+
const responseBody = await res.text().catch(() => '');
|
|
403
|
+
throwApiError(res, responseBody, `Agreement creation failed: ${res.status} ${res.statusText}`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const data = await res.json().catch(() => null) as Record<string, unknown> | null;
|
|
407
|
+
if (!data?.['agreement']) {
|
|
408
|
+
throw new Error('Invalid response: expected { ok, agreement } from POST /agreements');
|
|
409
|
+
}
|
|
410
|
+
return data as unknown as { ok: boolean; agreement: Agreement };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export async function revokeAgreement(
|
|
414
|
+
agreementId: string,
|
|
415
|
+
creds: Creds,
|
|
416
|
+
): Promise<{ ok: boolean; agreement: Agreement }> {
|
|
417
|
+
if (!agreementId) throw new Error('agreementId is required for revocation');
|
|
418
|
+
assertCreds(creds, 'agreement revocation');
|
|
419
|
+
|
|
420
|
+
// ZIG-207: canonical REST verb. Legacy POST /agreements/:id/revoke stays
|
|
421
|
+
// serving the identical handler (with Deprecation headers) until PR-F.
|
|
422
|
+
const res = await fetch(`${getAgreementBaseUrl()}/${encodeURIComponent(agreementId)}`, {
|
|
423
|
+
method: 'DELETE',
|
|
424
|
+
headers: buildHeaders(creds),
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
if (!res.ok) {
|
|
428
|
+
const body = await res.text().catch(() => '');
|
|
429
|
+
throwApiError(res, body, `Agreement revocation failed: ${res.status} ${res.statusText}`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const data = await res.json().catch(() => null) as Record<string, unknown> | null;
|
|
433
|
+
if (!data?.['agreement']) {
|
|
434
|
+
throw new Error('Invalid response: expected { ok, agreement } from DELETE /agreements/:id');
|
|
435
|
+
}
|
|
436
|
+
return data as unknown as { ok: boolean; agreement: Agreement };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ---------------------------------------------------------------------------
|
|
440
|
+
// Chat links
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
|
|
443
|
+
export async function getAgreementsByChat(chatId: string, creds: Creds): Promise<unknown[]> {
|
|
444
|
+
if (!chatId) return [];
|
|
445
|
+
assertCreds(creds, 'get agreements by chat');
|
|
446
|
+
|
|
447
|
+
try {
|
|
448
|
+
const res = await fetch(`${getAgreementBaseUrl()}/by-chat/${encodeURIComponent(chatId)}`, {
|
|
449
|
+
method: 'GET',
|
|
450
|
+
headers: buildHeaders(creds),
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
if (!res.ok) {
|
|
454
|
+
const body = await res.text().catch(() => '');
|
|
455
|
+
runtimeLog.warn('AgreementClient', `⚠️ Get agreements by chat failed: ${res.status} ${res.statusText} ${body?.slice(0, 200)}`);
|
|
456
|
+
return [];
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const data = await res.json().catch(() => null) as Record<string, unknown> | null;
|
|
460
|
+
const links = data?.['links'] ?? data?.['agreements'];
|
|
461
|
+
return Array.isArray(links) ? links : [];
|
|
462
|
+
} catch (e) {
|
|
463
|
+
runtimeLog.warn('AgreementClient', `⚠️ Get agreements by chat failed: ${(e as Error).message}`);
|
|
464
|
+
return [];
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export type ChatLinkType = 'origin' | 'mention' | 'delegation' | 'join';
|
|
469
|
+
|
|
470
|
+
export async function linkAgreementToChat(
|
|
471
|
+
agreementId: string,
|
|
472
|
+
chatId: string,
|
|
473
|
+
linkType: ChatLinkType = 'mention',
|
|
474
|
+
creds: Creds,
|
|
475
|
+
): Promise<unknown | null> {
|
|
476
|
+
if (!agreementId || !chatId) return null;
|
|
477
|
+
assertCreds(creds, 'link agreement to chat');
|
|
478
|
+
|
|
479
|
+
try {
|
|
480
|
+
const res = await fetch(`${getAgreementBaseUrl()}/${agreementId}/link`, {
|
|
481
|
+
method: 'POST',
|
|
482
|
+
headers: buildHeaders(creds),
|
|
483
|
+
body: JSON.stringify({ chatId, linkType }),
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
if (!res.ok) {
|
|
487
|
+
const body = await res.text().catch(() => '');
|
|
488
|
+
runtimeLog.warn('AgreementClient', `⚠️ Link agreement to chat failed: ${res.status} ${res.statusText} ${body?.slice(0, 200)}`);
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return await res.json().catch(() => null);
|
|
493
|
+
} catch (e) {
|
|
494
|
+
runtimeLog.warn('AgreementClient', `⚠️ Link agreement to chat failed: ${(e as Error).message}`);
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export async function getChatsForAgreement(agreementId: string, creds: Creds): Promise<unknown[]> {
|
|
500
|
+
if (!agreementId) return [];
|
|
501
|
+
assertCreds(creds, 'get chats for agreement');
|
|
502
|
+
|
|
503
|
+
try {
|
|
504
|
+
const res = await fetch(`${getAgreementBaseUrl()}/${agreementId}/chats`, {
|
|
505
|
+
method: 'GET',
|
|
506
|
+
headers: buildHeaders(creds),
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
if (!res.ok) {
|
|
510
|
+
const body = await res.text().catch(() => '');
|
|
511
|
+
runtimeLog.warn('AgreementClient', `⚠️ Get chats for agreement failed: ${res.status} ${res.statusText} ${body?.slice(0, 200)}`);
|
|
512
|
+
return [];
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const data = await res.json().catch(() => null) as Record<string, unknown> | null;
|
|
516
|
+
return Array.isArray(data?.['links']) ? data['links'] : [];
|
|
517
|
+
} catch (e) {
|
|
518
|
+
runtimeLog.warn('AgreementClient', `⚠️ Get chats for agreement failed: ${(e as Error).message}`);
|
|
519
|
+
return [];
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export async function joinAgreement(
|
|
524
|
+
agreementId: string,
|
|
525
|
+
creds: Creds,
|
|
526
|
+
): Promise<{ chatId: string; agentId: string | null; isNew: boolean }> {
|
|
527
|
+
if (!agreementId) throw new Error('agreementId is required for join');
|
|
528
|
+
assertCreds(creds, 'join agreement');
|
|
529
|
+
|
|
530
|
+
const res = await fetch(`${getAgreementBaseUrl()}/${agreementId}/join`, {
|
|
531
|
+
method: 'POST',
|
|
532
|
+
headers: buildHeaders(creds),
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
if (!res.ok) {
|
|
536
|
+
const body = await res.text().catch(() => '');
|
|
537
|
+
throwApiError(res, body, `Join agreement failed: ${res.status} ${res.statusText}`);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const data = await res.json().catch(() => null) as Record<string, unknown> | null;
|
|
541
|
+
if (!data?.['chatId']) {
|
|
542
|
+
throw new Error('Invalid response: expected { chatId } from /agreements/:id/join');
|
|
543
|
+
}
|
|
544
|
+
return data as unknown as { chatId: string; agentId: string | null; isNew: boolean };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ---------------------------------------------------------------------------
|
|
548
|
+
// Artifact links
|
|
549
|
+
// ---------------------------------------------------------------------------
|
|
550
|
+
|
|
551
|
+
export type ArtifactLinkType = 'produced' | 'referenced';
|
|
552
|
+
|
|
553
|
+
export async function linkArtifactToAgreement(
|
|
554
|
+
agreementId: string,
|
|
555
|
+
artifactId: string,
|
|
556
|
+
linkType: ArtifactLinkType = 'produced',
|
|
557
|
+
creds: Creds,
|
|
558
|
+
): Promise<unknown | null> {
|
|
559
|
+
if (!agreementId || !artifactId) return null;
|
|
560
|
+
assertCreds(creds, 'link artifact to agreement');
|
|
561
|
+
|
|
562
|
+
try {
|
|
563
|
+
const res = await fetch(`${getAgreementBaseUrl()}/${agreementId}/artifacts`, {
|
|
564
|
+
method: 'POST',
|
|
565
|
+
headers: buildHeaders(creds),
|
|
566
|
+
body: JSON.stringify({ artifactId, linkType }),
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
if (!res.ok) {
|
|
570
|
+
const body = await res.text().catch(() => '');
|
|
571
|
+
runtimeLog.warn('AgreementClient', `⚠️ Link artifact failed: ${res.status} ${res.statusText} ${body?.slice(0, 200)}`);
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return await res.json().catch(() => null);
|
|
576
|
+
} catch (e) {
|
|
577
|
+
runtimeLog.warn('AgreementClient', `⚠️ Link artifact failed: ${(e as Error).message}`);
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export async function getArtifactsForAgreement(agreementId: string, creds: Creds): Promise<unknown[]> {
|
|
583
|
+
if (!agreementId) return [];
|
|
584
|
+
assertCreds(creds, 'get artifacts for agreement');
|
|
585
|
+
|
|
586
|
+
try {
|
|
587
|
+
const res = await fetch(`${getAgreementBaseUrl()}/${agreementId}/artifacts`, {
|
|
588
|
+
method: 'GET',
|
|
589
|
+
headers: buildHeaders(creds),
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
if (!res.ok) {
|
|
593
|
+
const body = await res.text().catch(() => '');
|
|
594
|
+
runtimeLog.warn('AgreementClient', `⚠️ Get artifacts failed: ${res.status} ${res.statusText} ${body?.slice(0, 200)}`);
|
|
595
|
+
return [];
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const data = await res.json().catch(() => null) as Record<string, unknown> | null;
|
|
599
|
+
return Array.isArray(data?.['artifacts']) ? data['artifacts'] : [];
|
|
600
|
+
} catch (e) {
|
|
601
|
+
runtimeLog.warn('AgreementClient', `⚠️ Get artifacts failed: ${(e as Error).message}`);
|
|
602
|
+
return [];
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// ---------------------------------------------------------------------------
|
|
607
|
+
// User links
|
|
608
|
+
// ---------------------------------------------------------------------------
|
|
609
|
+
|
|
610
|
+
export type UserRole = 'payer' | 'provider' | 'participant' | 'observer';
|
|
611
|
+
|
|
612
|
+
export async function linkUserToAgreement(
|
|
613
|
+
agreementId: string,
|
|
614
|
+
userId: string,
|
|
615
|
+
role: UserRole,
|
|
616
|
+
creds: Creds,
|
|
617
|
+
): Promise<unknown | null> {
|
|
618
|
+
if (!agreementId || !userId || !role) return null;
|
|
619
|
+
assertCreds(creds, 'link user to agreement');
|
|
620
|
+
|
|
621
|
+
try {
|
|
622
|
+
const res = await fetch(`${getAgreementBaseUrl()}/${agreementId}/users`, {
|
|
623
|
+
method: 'POST',
|
|
624
|
+
headers: buildHeaders(creds),
|
|
625
|
+
body: JSON.stringify({ userId, role }),
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
if (!res.ok) {
|
|
629
|
+
const body = await res.text().catch(() => '');
|
|
630
|
+
runtimeLog.warn('AgreementClient', `⚠️ Link user failed: ${res.status} ${res.statusText} ${body?.slice(0, 200)}`);
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return await res.json().catch(() => null);
|
|
635
|
+
} catch (e) {
|
|
636
|
+
runtimeLog.warn('AgreementClient', `⚠️ Link user failed: ${(e as Error).message}`);
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export async function getUsersForAgreement(agreementId: string, creds: Creds): Promise<unknown[]> {
|
|
642
|
+
if (!agreementId) return [];
|
|
643
|
+
assertCreds(creds, 'get users for agreement');
|
|
644
|
+
|
|
645
|
+
try {
|
|
646
|
+
const res = await fetch(`${getAgreementBaseUrl()}/${agreementId}/users`, {
|
|
647
|
+
method: 'GET',
|
|
648
|
+
headers: buildHeaders(creds),
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
if (!res.ok) {
|
|
652
|
+
const body = await res.text().catch(() => '');
|
|
653
|
+
runtimeLog.warn('AgreementClient', `⚠️ Get users failed: ${res.status} ${res.statusText} ${body?.slice(0, 200)}`);
|
|
654
|
+
return [];
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const data = await res.json().catch(() => null) as Record<string, unknown> | null;
|
|
658
|
+
return Array.isArray(data?.['users']) ? data['users'] : [];
|
|
659
|
+
} catch (e) {
|
|
660
|
+
runtimeLog.warn('AgreementClient', `⚠️ Get users failed: ${(e as Error).message}`);
|
|
661
|
+
return [];
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// ---------------------------------------------------------------------------
|
|
666
|
+
// Unified contract creation (legacy) — prefer proposeDirectTo / proposeBroadcast /
|
|
667
|
+
// delegateAgreement / publishOpenQuest for clarity.
|
|
668
|
+
// ---------------------------------------------------------------------------
|
|
669
|
+
|
|
670
|
+
/** @deprecated Routing is expressed via `proposedTo` (specific id vs `OPEN_AGREEMENT_TARGET`), not engagementKind. */
|
|
671
|
+
export type ContractKind = 'direct' | 'delegate' | 'open';
|
|
672
|
+
|
|
673
|
+
interface ContractBase extends Omit<ProposeTerms, 'description'> {
|
|
674
|
+
description: string;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
interface DirectContract extends ProposeDirectInput {
|
|
678
|
+
kind: 'direct';
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
interface DelegateContract extends ContractBase {
|
|
682
|
+
kind: 'delegate';
|
|
683
|
+
executorId: string;
|
|
684
|
+
chatId: string;
|
|
685
|
+
parentAgreementId: string;
|
|
686
|
+
parentTaskId?: string;
|
|
687
|
+
payerId?: string;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
interface OpenContract extends ContractBase {
|
|
691
|
+
kind: 'open';
|
|
692
|
+
chatId?: string;
|
|
693
|
+
payerId?: string;
|
|
694
|
+
parentTaskId?: string;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
export type CreateContractInput = DirectContract | DelegateContract | OpenContract;
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* @deprecated Prefer {@link proposeDirectTo}, {@link proposeBroadcast},
|
|
701
|
+
* {@link delegateAgreement}, or {@link publishOpenQuest}.
|
|
702
|
+
*/
|
|
703
|
+
export async function createContract(
|
|
704
|
+
input: CreateContractInput,
|
|
705
|
+
creds: Creds,
|
|
706
|
+
): Promise<Agreement> {
|
|
707
|
+
assertCreds(creds, 'contract creation');
|
|
708
|
+
const { kind, ...rest } = input;
|
|
709
|
+
|
|
710
|
+
if (kind === 'direct') return proposeDirectTo(rest as ProposeDirectInput, creds);
|
|
711
|
+
if (kind === 'delegate') return delegateAgreement(rest as DelegateAgreementData, creds);
|
|
712
|
+
return publishOpenQuest(rest as PublishToLedgerPayload, creds);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/** Publish a buyer-broadcast quest to the marketplace ledger (Store "Wanted"). */
|
|
716
|
+
export async function publishOpenQuest(
|
|
717
|
+
payload: PublishToLedgerPayload,
|
|
718
|
+
creds: Creds,
|
|
719
|
+
): Promise<Agreement> {
|
|
720
|
+
return publishToLedger(payload, creds);
|
|
721
|
+
}
|