@ziggs-ai/api-client 0.13.0 → 0.14.1

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.
@@ -1,267 +0,0 @@
1
- import { ContextGrantsClient, GrantsClient, claimOpenAgreement, delegateAgreement, getMyAgreements, pullOffers, } from '../http/index.js';
2
- function providerAgentId(agreement) {
3
- return agreement.parties?.provider?.actor ?? 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
- /**
31
- * The default grant plumbing, built on the two clients that own these routes.
32
- *
33
- * A thin function seam rather than the clients themselves, so a test can drive
34
- * provisioning without a backend — the same reason the agreement calls are
35
- * injected.
36
- */
37
- export function defaultRelayProvisionDeps() {
38
- return {
39
- getMyAgreements,
40
- pullOffers,
41
- claimOpenAgreement,
42
- delegateAgreement,
43
- listAgreementGrants: async ({ agreementId, role }, creds) => {
44
- const client = new GrantsClient(creds.operatorKey, creds.agentId);
45
- return client.listAllGrants({
46
- scopeKind: 'agreement',
47
- scopeId: agreementId,
48
- role,
49
- health: 'active',
50
- });
51
- },
52
- delegateAgreementGrant: async ({ parentGrantId, holderId, agreementId }, creds) => {
53
- const client = new ContextGrantsClient(creds.operatorKey, creds.agentId);
54
- return client.delegateGrant(parentGrantId, {
55
- holderId,
56
- holderKind: 'agent',
57
- // `write` is exactly what `canActUnderAgreement` tests. Deliberately not
58
- // `admit`: that would let a coordinator widen its own crew.
59
- kind: 'write',
60
- scope: { kind: 'agreement', id: agreementId },
61
- // from-start, not from-now: the contract predates the grant, so a
62
- // from-now watermark would exclude the very row the grant is about.
63
- temporal: 'from-start',
64
- });
65
- },
66
- };
67
- }
68
- /**
69
- * Hand the coordinator a pass on one worker contract.
70
- *
71
- * Membership in work is a grant, which is what lets a coordinator create work
72
- * under a contract it is not a party to. Issuing that grant is the half that has
73
- * to happen here: without it the recipe fails at the coordinator's first
74
- * `task_create` with "Caller <coordinator> holds no grant on agreement <worker>,
75
- * so it cannot create work citing it."
76
- *
77
- * Reused before minted, because provisioning is re-run: a second call after an
78
- * agreement activates should not stack a second pass on a contract that already
79
- * has one.
80
- */
81
- async function grantCoordinatorAccess(args, deps) {
82
- const { agreementId, coordinatorId, status, creds } = args;
83
- // Participation grants are minted at ACTIVATION, so a contract still waiting
84
- // on a signature has nothing to delegate from yet. Say so rather than
85
- // reporting a failure the caller cannot act on.
86
- if (status !== 'active') {
87
- return {
88
- status: 'deferred',
89
- error: `${agreementId} is not active yet, so there is no participation grant to ` +
90
- 'delegate from — provision again once it activates',
91
- };
92
- }
93
- try {
94
- const alreadyIssued = await deps.listAgreementGrants({ agreementId, role: 'issuer' }, creds);
95
- const existing = alreadyIssued.find((g) => g.holderId === coordinatorId);
96
- if (existing?.grantId)
97
- return { status: 'reused', grantId: existing.grantId };
98
- const held = await deps.listAgreementGrants({ agreementId, role: 'holder' }, creds);
99
- // Only a `write` or `admit` parent can beget the `write` child the act rule
100
- // tests; a `read` participation grant cannot be widened by delegating it.
101
- // `access` is optional on GrantView because some rails do not record it —
102
- // context grants always do, so an absent value here is a malformed row and
103
- // must not be treated as strong enough.
104
- const parent = held.find((g) => g.access === 'write' || g.access === 'admit');
105
- if (!parent?.grantId) {
106
- return {
107
- status: 'failed',
108
- error: `no write-or-stronger grant on ${agreementId} to delegate from (found ` +
109
- `${held.length ? held.map((g) => g.access ?? 'unstated').join(', ') : 'none'}) — ` +
110
- 'the initiator must be a party to the worker contract it is provisioning',
111
- };
112
- }
113
- const result = await deps.delegateAgreementGrant({ parentGrantId: parent.grantId, holderId: coordinatorId, agreementId }, creds);
114
- if (result.status === 'pending_approval') {
115
- return { status: 'pending_approval', approvalAgreementId: result.agreementId };
116
- }
117
- return { status: 'granted', grantId: result.grant.grantId };
118
- }
119
- catch (err) {
120
- return {
121
- status: 'failed',
122
- error: err instanceof Error ? err.message : String(err),
123
- };
124
- }
125
- }
126
- export async function provisionRelayWorkers(input, deps = defaultRelayProvisionDeps()) {
127
- const { creds, hireAgreementId, coordinatorId, chatId, steps, inputArtifactIds } = input;
128
- if (!steps.length)
129
- throw new Error('steps must not be empty');
130
- if (!coordinatorId?.trim()) {
131
- throw new Error('coordinatorId is required: provisioning a worker for a coordinator means both ' +
132
- 'the contract and an agreement-scope grant to that coordinator on it, and ' +
133
- 'without the grant every branch of the run is refused');
134
- }
135
- const myAgreements = (await deps.getMyAgreements({}, creds));
136
- let offersCache = null;
137
- // The contracts first, then the passes. Two passes rather than one because a
138
- // grant can only be delegated from a participation grant, which exists only
139
- // once the contract is active — so the grant decision needs the settled
140
- // status of the row, not the intent that created it.
141
- const contracted = [];
142
- const pendingApprovals = [];
143
- for (const step of steps) {
144
- const existing = findExistingWorkerDelegation(hireAgreementId, step.assigneeId, myAgreements);
145
- if (existing?.agreementId) {
146
- contracted.push({
147
- stepId: step.stepId,
148
- order: step.order,
149
- agreementId: existing.agreementId,
150
- assigneeId: step.assigneeId,
151
- description: step.description,
152
- provisionMethod: 'existing',
153
- status: 'active',
154
- });
155
- continue;
156
- }
157
- if (step.offerAgreementId) {
158
- const { agreement: claimed } = await deps.claimOpenAgreement(step.offerAgreementId, creds);
159
- const agreementId = claimed.agreementId;
160
- const status = isActiveAgreement(claimed)
161
- ? 'active'
162
- : 'pending_approval';
163
- if (status === 'pending_approval')
164
- pendingApprovals.push(agreementId);
165
- contracted.push({
166
- stepId: step.stepId,
167
- order: step.order,
168
- agreementId,
169
- assigneeId: step.assigneeId,
170
- description: step.description,
171
- provisionMethod: 'claim',
172
- status,
173
- });
174
- continue;
175
- }
176
- if (!offersCache) {
177
- offersCache = await deps.pullOffers({ limit: 100 }, creds);
178
- }
179
- const openOffer = findOpenOfferForAgent(step.assigneeId, offersCache);
180
- if (openOffer?.agreementId) {
181
- const { agreement: claimed } = await deps.claimOpenAgreement(openOffer.agreementId, creds);
182
- const agreementId = claimed.agreementId;
183
- const status = isActiveAgreement(claimed)
184
- ? 'active'
185
- : 'pending_approval';
186
- if (status === 'pending_approval')
187
- pendingApprovals.push(agreementId);
188
- contracted.push({
189
- stepId: step.stepId,
190
- order: step.order,
191
- agreementId,
192
- assigneeId: step.assigneeId,
193
- description: step.description,
194
- provisionMethod: 'claim',
195
- status,
196
- });
197
- continue;
198
- }
199
- if (!chatId?.trim()) {
200
- throw new Error(`No standing offer found for ${step.assigneeId} and chatId is required to propose a delegation under the hire`);
201
- }
202
- const delegated = await deps.delegateAgreement({
203
- description: step.description,
204
- executorId: step.assigneeId,
205
- chatId: chatId.trim(),
206
- parentAgreementId: hireAgreementId,
207
- agreementDescription: `Relay step ${step.stepId}: ${step.description}`,
208
- lifecycle: 'count-bound',
209
- maxExecutions: 50,
210
- price: 0,
211
- }, creds);
212
- const agreementId = delegated.agreementId;
213
- const status = isActiveAgreement(delegated)
214
- ? 'active'
215
- : 'pending_approval';
216
- if (status === 'pending_approval')
217
- pendingApprovals.push(agreementId);
218
- contracted.push({
219
- stepId: step.stepId,
220
- order: step.order,
221
- agreementId,
222
- assigneeId: step.assigneeId,
223
- description: step.description,
224
- provisionMethod: 'delegate',
225
- status,
226
- });
227
- }
228
- contracted.sort((a, b) => a.order - b.order);
229
- // ── the second act of provisioning: the coordinator's passes ─────────────
230
- const provisioned = [];
231
- for (const step of contracted) {
232
- const grant = await grantCoordinatorAccess({
233
- agreementId: step.agreementId,
234
- coordinatorId,
235
- status: step.status,
236
- creds,
237
- }, deps);
238
- if (grant.status === 'pending_approval' && grant.approvalAgreementId) {
239
- pendingApprovals.push(grant.approvalAgreementId);
240
- }
241
- provisioned.push({ ...step, grant });
242
- }
243
- const payload = {
244
- inputArtifactIds,
245
- steps: provisioned.map(({ stepId, order, agreementId, assigneeId, description }) => ({
246
- stepId,
247
- order,
248
- agreementId,
249
- assigneeId,
250
- description,
251
- })),
252
- };
253
- /**
254
- * Ready means BOTH acts are done for every step. A run whose contracts are all
255
- * active but whose coordinator holds no passes looks provisioned and has every
256
- * branch refused, so the grant has to count here or this flag goes on lying.
257
- */
258
- const readyForKickoff = pendingApprovals.length === 0 &&
259
- provisioned.every((s) => s.status === 'active' &&
260
- (s.grant.status === 'granted' || s.grant.status === 'reused'));
261
- return {
262
- payload,
263
- steps: provisioned,
264
- pendingApprovals,
265
- readyForKickoff,
266
- };
267
- }