@ziggs-ai/api-client 0.9.0 → 0.9.2
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/dist/ConnectionManager.d.ts +23 -59
- package/dist/ConnectionManager.js +37 -166
- package/dist/capabilities/agreements.js +2 -2
- package/dist/capabilities/chat.js +13 -3
- package/dist/capabilities/connections.js +1 -1
- package/dist/capabilities/index.d.ts +1 -1
- package/dist/capabilities/index.js +1 -1
- package/dist/capabilities/links.d.ts +0 -8
- package/dist/capabilities/links.js +3 -25
- package/dist/capabilities/payments.js +5 -0
- package/dist/capabilities/types.js +3 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.js +42 -0
- package/dist/http/AgentSearchClient.d.ts +0 -1
- package/dist/http/AgentSearchClient.js +0 -1
- package/dist/http/AgreementClient.d.ts +32 -1
- package/dist/http/AgreementClient.js +96 -23
- package/dist/http/ArtifactsClient.d.ts +0 -1
- package/dist/http/ArtifactsClient.js +0 -1
- package/dist/http/ChatClient.d.ts +6 -3
- package/dist/http/ChatClient.js +7 -6
- package/dist/http/ConnectionsClient.d.ts +0 -1
- package/dist/http/ConnectionsClient.js +4 -5
- package/dist/http/ContextDiscoveryClient.d.ts +0 -1
- package/dist/http/ContextDiscoveryClient.js +2 -2
- package/dist/http/ContextGrantsClient.d.ts +0 -1
- package/dist/http/ContextGrantsClient.js +0 -1
- package/dist/http/ContextReadClient.d.ts +0 -1
- package/dist/http/ContextReadClient.js +5 -17
- package/dist/http/GrantsClient.d.ts +0 -1
- package/dist/http/GrantsClient.js +2 -2
- package/dist/http/InboxClient.d.ts +1 -177
- package/dist/http/InboxClient.js +0 -40
- package/dist/http/MarketplaceClient.d.ts +0 -1
- package/dist/http/MarketplaceClient.js +5 -4
- package/dist/http/MessagesClient.d.ts +0 -1
- package/dist/http/MessagesClient.js +3 -6
- package/dist/http/OrgsClient.d.ts +0 -1
- package/dist/http/OrgsClient.js +3 -3
- package/dist/http/PaymentsClient.d.ts +4 -2
- package/dist/http/PaymentsClient.js +27 -7
- package/dist/http/TaskClient.d.ts +0 -1
- package/dist/http/TaskClient.js +0 -1
- package/dist/http/TelemetryClient.d.ts +0 -1
- package/dist/http/TelemetryClient.js +0 -1
- package/dist/http/agreementFlows.d.ts +13 -5
- package/dist/http/agreementFlows.js +18 -24
- package/dist/http/index.d.ts +0 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +5 -2
- package/dist/shared/apiError.d.ts +22 -1
- package/dist/shared/apiError.js +60 -3
- package/dist/shared/rateLimit.d.ts +12 -22
- package/dist/shared/rateLimit.js +18 -51
- package/dist/shared/runtimeLog.d.ts +0 -6
- package/dist/shared/runtimeLog.js +10 -7
- package/dist/types.d.ts +167 -1
- package/dist/types.js +20 -1
- package/dist/utils/urlUtils.js +3 -2
- package/package.json +1 -2
|
@@ -1,87 +1,51 @@
|
|
|
1
|
-
import { type InboxOperatorAgentEntry } from './http/InboxClient.js';
|
|
2
1
|
type OpenFn = () => Promise<unknown>;
|
|
3
2
|
type CloseFn = (handle: unknown) => Promise<void>;
|
|
4
|
-
export interface StartAgentOptions {
|
|
5
|
-
/**
|
|
6
|
-
* Keep this host running until it is stopped explicitly — exempt from the
|
|
7
|
-
* idle sweep, last choice for LRU eviction. For agents the operator named
|
|
8
|
-
* (launcher `preStart` / `WAKE_AGENTS`): they were started because someone
|
|
9
|
-
* decided they must be up, not because work arrived, so "no work for a
|
|
10
|
-
* while" is not a reason to retire them.
|
|
11
|
-
*/
|
|
12
|
-
pinned?: boolean;
|
|
13
|
-
}
|
|
14
3
|
export interface ConnectionManagerMeta {
|
|
15
4
|
domain?: string;
|
|
16
5
|
expertise?: string[];
|
|
17
6
|
tags?: string[];
|
|
18
7
|
[key: string]: unknown;
|
|
19
8
|
}
|
|
20
|
-
export interface ConnectionManagerOptions {
|
|
21
|
-
maxActive?: number;
|
|
22
|
-
idleTimeoutMs?: number;
|
|
23
|
-
/**
|
|
24
|
-
* Fleet control (delivery law, phase 4): the operator key whose owned agents
|
|
25
|
-
* this manager lazily starts. `start()` long-polls `GET /inbox/operator` with
|
|
26
|
-
* this key; when an agent's slice shows actionable content, it starts that
|
|
27
|
-
* agent's host (which then consumes via its own per-agent inbox loop). No
|
|
28
|
-
* socket, no push notification — lazy process management driven by the poll. `wsUrl`
|
|
29
|
-
* is accepted for back-compat but unused (the poll is HTTP).
|
|
30
|
-
*/
|
|
31
|
-
control?: {
|
|
32
|
-
wsUrl?: string;
|
|
33
|
-
operatorKey?: string;
|
|
34
|
-
};
|
|
35
|
-
/**
|
|
36
|
-
* Gate on the operator-poll lazy start (ZIG-910). Called for each agent
|
|
37
|
-
* slice with actionable content before its host is started; return false to
|
|
38
|
-
* leave the host down this pass. The slice stays in later envelopes (reading
|
|
39
|
-
* never acks), so the gate is consulted again on every poll. Explicit
|
|
40
|
-
* `startAgent()` calls are not gated. Default: everything actionable starts.
|
|
41
|
-
*/
|
|
42
|
-
shouldLazyStart?: (entry: InboxOperatorAgentEntry) => boolean;
|
|
43
|
-
}
|
|
44
9
|
export interface QueryFilter {
|
|
45
10
|
domain?: string;
|
|
46
11
|
expertise?: string[];
|
|
47
12
|
tags?: string[];
|
|
48
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* ConnectionManager — the set of agent hosts this process runs.
|
|
16
|
+
*
|
|
17
|
+
* Every registered agent is connected and stays connected (ZIG-1108). There is
|
|
18
|
+
* no subset, no cap, no idle sweep and no lifecycle policy: an agent is alive
|
|
19
|
+
* because its launcher registered it, and the only thing that takes it down is
|
|
20
|
+
* the process exiting. Whether work has arrived recently is not a fact about
|
|
21
|
+
* whether an agent should exist.
|
|
22
|
+
*
|
|
23
|
+
* What this replaced: an operator long-poll that asked the backend which agents
|
|
24
|
+
* had mail, an LRU that evicted hosts past `maxActive`, an idle timer that slept
|
|
25
|
+
* quiet hosts, and a `pinned` flag to exempt the ones an operator had named.
|
|
26
|
+
* Together they made liveness depend on someone remembering to edit a Terraform
|
|
27
|
+
* string, and cost a fleet-wide sweep per poll to find out nothing had changed.
|
|
28
|
+
*/
|
|
49
29
|
export declare class ConnectionManager {
|
|
50
|
-
private maxActive;
|
|
51
|
-
private idleTimeoutMs;
|
|
52
|
-
private _operatorKey;
|
|
53
|
-
private _polling;
|
|
54
|
-
private _pollDone;
|
|
55
30
|
private _entries;
|
|
56
31
|
private _active;
|
|
57
32
|
private _meta;
|
|
58
33
|
private _starting;
|
|
59
|
-
|
|
60
|
-
private _shouldLazyStart;
|
|
61
|
-
constructor({ maxActive, idleTimeoutMs, control, shouldLazyStart }?: ConnectionManagerOptions);
|
|
34
|
+
constructor();
|
|
62
35
|
register(id: string, openFn: OpenFn, closeFn: CloseFn, meta?: ConnectionManagerMeta): void;
|
|
63
|
-
start(operatorKey?: string): void;
|
|
64
|
-
stop(): Promise<void>;
|
|
65
36
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* acks — it only decides which hosts to run. Errors retry with backoff+jitter;
|
|
70
|
-
* the loop never exits until `stop()` (nothing to resynchronize).
|
|
37
|
+
* Connect every registered agent. One host's failure to come up is reported
|
|
38
|
+
* and does not hold back the rest — its own transport retries, and a fleet
|
|
39
|
+
* where 28 of 29 are live is not a launch worth aborting.
|
|
71
40
|
*/
|
|
72
|
-
|
|
73
|
-
startAgent(id: string
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
touch(id: string): void;
|
|
41
|
+
connectAll(): Promise<void>;
|
|
42
|
+
startAgent(id: string): Promise<unknown>;
|
|
43
|
+
stopAgent(id: string): Promise<void>;
|
|
44
|
+
stopAll(): Promise<void>;
|
|
77
45
|
list(): string[];
|
|
78
46
|
listActive(): string[];
|
|
79
|
-
listPinned(): string[];
|
|
80
47
|
get size(): number;
|
|
81
48
|
query({ domain, expertise, tags }?: QueryFilter): string[];
|
|
82
49
|
getMeta(id: string): ConnectionManagerMeta | undefined;
|
|
83
|
-
private _scheduleIdle;
|
|
84
|
-
private _resetTimer;
|
|
85
|
-
private _evictLRU;
|
|
86
50
|
}
|
|
87
51
|
export {};
|
|
@@ -1,36 +1,29 @@
|
|
|
1
|
-
import { InboxClient } from './http/InboxClient.js';
|
|
2
1
|
import { runtimeLog } from './shared/runtimeLog.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
/**
|
|
3
|
+
* ConnectionManager — the set of agent hosts this process runs.
|
|
4
|
+
*
|
|
5
|
+
* Every registered agent is connected and stays connected (ZIG-1108). There is
|
|
6
|
+
* no subset, no cap, no idle sweep and no lifecycle policy: an agent is alive
|
|
7
|
+
* because its launcher registered it, and the only thing that takes it down is
|
|
8
|
+
* the process exiting. Whether work has arrived recently is not a fact about
|
|
9
|
+
* whether an agent should exist.
|
|
10
|
+
*
|
|
11
|
+
* What this replaced: an operator long-poll that asked the backend which agents
|
|
12
|
+
* had mail, an LRU that evicted hosts past `maxActive`, an idle timer that slept
|
|
13
|
+
* quiet hosts, and a `pinned` flag to exempt the ones an operator had named.
|
|
14
|
+
* Together they made liveness depend on someone remembering to edit a Terraform
|
|
15
|
+
* string, and cost a fleet-wide sweep per poll to find out nothing had changed.
|
|
16
|
+
*/
|
|
10
17
|
export class ConnectionManager {
|
|
11
|
-
maxActive;
|
|
12
|
-
idleTimeoutMs;
|
|
13
|
-
_operatorKey;
|
|
14
|
-
_polling;
|
|
15
|
-
_pollDone;
|
|
16
18
|
_entries;
|
|
17
19
|
_active;
|
|
18
20
|
_meta;
|
|
19
21
|
_starting;
|
|
20
|
-
|
|
21
|
-
_shouldLazyStart;
|
|
22
|
-
constructor({ maxActive = 50, idleTimeoutMs = 60_000, control, shouldLazyStart } = {}) {
|
|
23
|
-
this.maxActive = maxActive;
|
|
24
|
-
this.idleTimeoutMs = idleTimeoutMs;
|
|
25
|
-
this._shouldLazyStart = shouldLazyStart ?? null;
|
|
26
|
-
this._operatorKey = control?.operatorKey ?? null;
|
|
27
|
-
this._polling = false;
|
|
28
|
-
this._pollDone = null;
|
|
22
|
+
constructor() {
|
|
29
23
|
this._entries = new Map();
|
|
30
24
|
this._active = new Map();
|
|
31
25
|
this._meta = new Map();
|
|
32
26
|
this._starting = new Map();
|
|
33
|
-
this._pinned = new Set();
|
|
34
27
|
}
|
|
35
28
|
register(id, openFn, closeFn, meta) {
|
|
36
29
|
if (!id)
|
|
@@ -43,103 +36,34 @@ export class ConnectionManager {
|
|
|
43
36
|
if (meta)
|
|
44
37
|
this._meta.set(id, meta);
|
|
45
38
|
}
|
|
46
|
-
start(operatorKey) {
|
|
47
|
-
if (operatorKey)
|
|
48
|
-
this._operatorKey = operatorKey;
|
|
49
|
-
if (this._polling || !this._operatorKey)
|
|
50
|
-
return;
|
|
51
|
-
this._polling = true;
|
|
52
|
-
this._pollDone = this._runOperatorPoll(this._operatorKey);
|
|
53
|
-
}
|
|
54
|
-
async stop() {
|
|
55
|
-
this._polling = false;
|
|
56
|
-
if (this._pollDone)
|
|
57
|
-
await this._pollDone.catch(() => { });
|
|
58
|
-
this._pollDone = null;
|
|
59
|
-
await this.sleepAll();
|
|
60
|
-
}
|
|
61
39
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* acks — it only decides which hosts to run. Errors retry with backoff+jitter;
|
|
66
|
-
* the loop never exits until `stop()` (nothing to resynchronize).
|
|
40
|
+
* Connect every registered agent. One host's failure to come up is reported
|
|
41
|
+
* and does not hold back the rest — its own transport retries, and a fleet
|
|
42
|
+
* where 28 of 29 are live is not a launch worth aborting.
|
|
67
43
|
*/
|
|
68
|
-
async
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// (ONLY_AGENTS) paid for a sweep of the owner's whole seeded fleet.
|
|
77
|
-
//
|
|
78
|
-
// Already-running agents are excluded: this loop's only job is
|
|
79
|
-
// lazy-start, and a running host polls its own inbox. Sending them
|
|
80
|
-
// made the server answer for agents whose entry we then discarded
|
|
81
|
-
// below — pure duplicate work, growing with how healthy the fleet is.
|
|
82
|
-
const startable = [...this._entries.keys()].filter((id) => !this._active.has(id));
|
|
83
|
-
const env = await inbox.getOperatorInbox({
|
|
84
|
-
waitSeconds: OPERATOR_POLL_WAIT_SECONDS,
|
|
85
|
-
agents: startable,
|
|
86
|
-
});
|
|
87
|
-
consecutiveErrors = 0;
|
|
88
|
-
for (const entry of env.agents) {
|
|
89
|
-
const id = entry.agentId;
|
|
90
|
-
// Only slices with actionable content are listed; start the host if
|
|
91
|
-
// it isn't already running and we know how to open it.
|
|
92
|
-
if (!id || this._active.has(id) || !this._entries.has(id))
|
|
93
|
-
continue;
|
|
94
|
-
if (this._shouldLazyStart && !this._shouldLazyStart(entry))
|
|
95
|
-
continue;
|
|
96
|
-
this.startAgent(id).catch((err) => runtimeLog.warn('ConnectionManager', `lazy-start("${id}") failed: ${err.message}`));
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
catch (err) {
|
|
100
|
-
if (!this._polling)
|
|
101
|
-
break;
|
|
102
|
-
consecutiveErrors++;
|
|
103
|
-
// ZIG-1019: same rule as the per-agent loop — when the server throttles
|
|
104
|
-
// us it also says for how long, and retrying sooner just spends more of
|
|
105
|
-
// the budget we were told we are out of.
|
|
106
|
-
const throttleMs = isRateLimited(err) ? (err.retryAfterMs ?? 30_000) : null;
|
|
107
|
-
if (throttleMs !== null) {
|
|
108
|
-
consecutiveErrors = 0;
|
|
109
|
-
runtimeLog.warn('ConnectionManager', `operator poll throttled: ${err.message} — waiting ${Math.round(throttleMs)}ms as instructed`);
|
|
110
|
-
await sleep(throttleMs);
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
const backoff = Math.min(POLL_BACKOFF_MAX_MS, POLL_BACKOFF_BASE_MS * 2 ** (consecutiveErrors - 1));
|
|
114
|
-
const jitter = backoff * (0.5 + (consecutiveErrors % 7) / 14);
|
|
115
|
-
runtimeLog.warn('ConnectionManager', `operator poll failed (#${consecutiveErrors}): ${err.message} — retrying in ~${Math.round(jitter)}ms`);
|
|
116
|
-
await sleep(jitter);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
44
|
+
async connectAll() {
|
|
45
|
+
const ids = [...this._entries.keys()];
|
|
46
|
+
// The count is the cost: every registered agent is one WS connection plus
|
|
47
|
+
// one inbox long-poll, held for the life of the process. There is no cap,
|
|
48
|
+
// so a registration list that balloons shows up here first — loudly, with
|
|
49
|
+
// the number, before the sockets open.
|
|
50
|
+
runtimeLog.info('ConnectionManager', `connecting all ${ids.length} registered agent(s) — one connection + inbox long-poll each, for the life of the process`);
|
|
51
|
+
await Promise.all(ids.map((id) => this.startAgent(id).catch((err) => runtimeLog.warn('ConnectionManager', `start("${id}") failed: ${err.message}`))));
|
|
119
52
|
}
|
|
120
|
-
async startAgent(id
|
|
121
|
-
// Recorded before any await so a pin is never lost to a concurrent
|
|
122
|
-
// unpinned start of the same id.
|
|
123
|
-
if (pinned)
|
|
124
|
-
this._pinned.add(id);
|
|
53
|
+
async startAgent(id) {
|
|
125
54
|
const existing = this._active.get(id);
|
|
126
|
-
if (existing)
|
|
127
|
-
|
|
128
|
-
return existing.handle;
|
|
129
|
-
}
|
|
55
|
+
if (existing)
|
|
56
|
+
return existing;
|
|
130
57
|
const pending = this._starting.get(id);
|
|
131
58
|
if (pending)
|
|
132
59
|
return pending;
|
|
133
60
|
const entry = this._entries.get(id);
|
|
134
61
|
if (!entry)
|
|
135
62
|
throw new Error(`[ConnectionManager] unknown id: "${id}"`);
|
|
136
|
-
if (this._active.size >= this.maxActive)
|
|
137
|
-
await this._evictLRU();
|
|
138
63
|
const startPromise = (async () => {
|
|
139
64
|
try {
|
|
140
65
|
const handle = await entry.openFn();
|
|
141
|
-
this._active.set(id,
|
|
142
|
-
this._scheduleIdle(id);
|
|
66
|
+
this._active.set(id, handle);
|
|
143
67
|
return handle;
|
|
144
68
|
}
|
|
145
69
|
finally {
|
|
@@ -149,23 +73,20 @@ export class ConnectionManager {
|
|
|
149
73
|
this._starting.set(id, startPromise);
|
|
150
74
|
return startPromise;
|
|
151
75
|
}
|
|
152
|
-
async
|
|
153
|
-
|
|
154
|
-
if (!entry)
|
|
76
|
+
async stopAgent(id) {
|
|
77
|
+
if (!this._active.has(id))
|
|
155
78
|
return;
|
|
156
|
-
|
|
79
|
+
const handle = this._active.get(id);
|
|
157
80
|
this._active.delete(id);
|
|
158
81
|
const reg = this._entries.get(id);
|
|
159
82
|
if (reg)
|
|
160
|
-
await reg.closeFn(
|
|
83
|
+
await reg.closeFn(handle);
|
|
161
84
|
}
|
|
162
|
-
async
|
|
163
|
-
await Promise.all([...this._active.keys()].map(id => this.
|
|
85
|
+
async stopAll() {
|
|
86
|
+
await Promise.all([...this._active.keys()].map(id => this.stopAgent(id)));
|
|
164
87
|
}
|
|
165
|
-
touch(id) { this._resetTimer(id); }
|
|
166
88
|
list() { return [...this._entries.keys()]; }
|
|
167
89
|
listActive() { return [...this._active.keys()]; }
|
|
168
|
-
listPinned() { return [...this._pinned]; }
|
|
169
90
|
get size() { return this._entries.size; }
|
|
170
91
|
query({ domain, expertise, tags } = {}) {
|
|
171
92
|
const results = [];
|
|
@@ -181,54 +102,4 @@ export class ConnectionManager {
|
|
|
181
102
|
return results;
|
|
182
103
|
}
|
|
183
104
|
getMeta(id) { return this._meta.get(id); }
|
|
184
|
-
_scheduleIdle(id) {
|
|
185
|
-
const entry = this._active.get(id);
|
|
186
|
-
if (!entry)
|
|
187
|
-
return;
|
|
188
|
-
clearTimeout(entry.timer);
|
|
189
|
-
// A pinned host has no idle timer at all. Empty polls don't count as
|
|
190
|
-
// activity (only delivered items call touch()), so a pinned agent nobody
|
|
191
|
-
// talks to would otherwise be swept — and with lazy start unable to keep
|
|
192
|
-
// up at fleet size, nothing brings it back until the process restarts.
|
|
193
|
-
if (this._pinned.has(id)) {
|
|
194
|
-
entry.timer = null;
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
entry.timer = setTimeout(() => {
|
|
198
|
-
this.sleep(id).catch(err => runtimeLog.warn('ConnectionManager', `idle sleep("${id}") failed: ${err.message}`));
|
|
199
|
-
}, this.idleTimeoutMs);
|
|
200
|
-
}
|
|
201
|
-
_resetTimer(id) {
|
|
202
|
-
const entry = this._active.get(id);
|
|
203
|
-
if (!entry)
|
|
204
|
-
return;
|
|
205
|
-
entry.lastActive = Date.now();
|
|
206
|
-
this._scheduleIdle(id);
|
|
207
|
-
}
|
|
208
|
-
async _evictLRU() {
|
|
209
|
-
// Pinned hosts are the last to go: pick the LRU unpinned one, and only
|
|
210
|
-
// fall back to a pinned one when every active host is pinned. maxActive
|
|
211
|
-
// stays a hard cap — a pin outranks other agents, not the capacity limit —
|
|
212
|
-
// but say so, because it means more agents were pinned than can run.
|
|
213
|
-
const oldestOf = (ids) => {
|
|
214
|
-
let oldest = null;
|
|
215
|
-
for (const id of ids) {
|
|
216
|
-
const entry = this._active.get(id);
|
|
217
|
-
const oldestEntry = oldest ? this._active.get(oldest) : null;
|
|
218
|
-
if (!entry)
|
|
219
|
-
continue;
|
|
220
|
-
if (!oldest || !oldestEntry || entry.lastActive < oldestEntry.lastActive)
|
|
221
|
-
oldest = id;
|
|
222
|
-
}
|
|
223
|
-
return oldest;
|
|
224
|
-
};
|
|
225
|
-
const active = [...this._active.keys()];
|
|
226
|
-
const victim = oldestOf(active.filter(id => !this._pinned.has(id))) ?? oldestOf(active);
|
|
227
|
-
if (!victim)
|
|
228
|
-
return;
|
|
229
|
-
if (this._pinned.has(victim)) {
|
|
230
|
-
runtimeLog.warn('ConnectionManager', `evicting pinned "${victim}": all ${active.length} active hosts are pinned and maxActive=${this.maxActive} is reached — pin fewer agents or raise maxActive`);
|
|
231
|
-
}
|
|
232
|
-
await this.sleep(victim);
|
|
233
|
-
}
|
|
234
105
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { claimOpenAgreement } from '../http/agreementFlows.js';
|
|
2
|
-
import { linkIsReachOnly
|
|
2
|
+
import { linkIsReachOnly } from './links.js';
|
|
3
3
|
import { fullCreds } from './types.js';
|
|
4
4
|
/**
|
|
5
5
|
* ZIG-1021 — the one claim verb. Quests, standing offers, and link invites
|
|
@@ -29,7 +29,7 @@ export const agreementClaimCapability = {
|
|
|
29
29
|
status: 'linked',
|
|
30
30
|
kind,
|
|
31
31
|
message: `Link invite claimed — you are now linked. ${linkIsReachOnly(env)}`,
|
|
32
|
-
agreement
|
|
32
|
+
agreement,
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
return {
|
|
@@ -12,8 +12,8 @@ export const openConversationCapability = {
|
|
|
12
12
|
key: 'chat_open',
|
|
13
13
|
names: { sdk: 'chat_open', mcp: 'ziggs_chat_open' },
|
|
14
14
|
descriptions: {
|
|
15
|
-
sdk: 'Open or reuse a chat with a user or agent participant. To reach an agent in ANOTHER org, establish a link first — agreement_propose with engagementKind "link" (if you have its agent id) or link_create_invite (if you do not), approved/claimed — otherwise this fails with AGENT_NOT_PUBLISHED. To list chats you can already read, use grant_list scopeKind=chat.',
|
|
16
|
-
mcp: 'Open or reuse a chat with a user or agent participant. To reach an agent in ANOTHER org, an unpublished delegate must establish a link first — ziggs_agreement_propose with engagementKind "link" (if you have its agent id) or ziggs_link_create_invite (if you do not), approved/claimed — otherwise this fails with AGENT_NOT_PUBLISHED.',
|
|
15
|
+
sdk: 'Open or reuse a chat with a user or agent participant. Calling it again for the same participant returns the SAME chat, so it is how you find the conversation you already have with someone — pass newChat only when this really is a separate subject. To reach an agent in ANOTHER org, establish a link first — agreement_propose with engagementKind "link" (if you have its agent id) or link_create_invite (if you do not), approved/claimed — otherwise this fails with AGENT_NOT_PUBLISHED. To list chats you can already read, use grant_list scopeKind=chat.',
|
|
16
|
+
mcp: 'Open or reuse a chat with a user or agent participant. Calling it again for the same participant returns the SAME chat; pass newChat only when this really is a separate subject. To reach an agent in ANOTHER org, an unpublished delegate must establish a link first — ziggs_agreement_propose with engagementKind "link" (if you have its agent id) or ziggs_link_create_invite (if you do not), approved/claimed — otherwise this fails with AGENT_NOT_PUBLISHED.',
|
|
17
17
|
},
|
|
18
18
|
annotation: 'write',
|
|
19
19
|
params: {
|
|
@@ -22,12 +22,22 @@ export const openConversationCapability = {
|
|
|
22
22
|
required: true,
|
|
23
23
|
description: 'User or agent id to converse with (from agent search — do not guess ids)',
|
|
24
24
|
},
|
|
25
|
+
newChat: {
|
|
26
|
+
type: 'boolean',
|
|
27
|
+
description: 'Open a separate chat even though one is already open with this participant. ' +
|
|
28
|
+
'For when the conversation is genuinely its own subject and would confuse an ' +
|
|
29
|
+
'existing thread. The separate chat does not become the main one, so a later ' +
|
|
30
|
+
'call without this still returns the original. Leave unset to continue where ' +
|
|
31
|
+
'you left off.',
|
|
32
|
+
},
|
|
25
33
|
},
|
|
26
34
|
needsAgentId: true,
|
|
27
35
|
handler: async (args, env) => {
|
|
28
36
|
if (!args['participantId'])
|
|
29
37
|
throw new Error('participantId is required');
|
|
30
|
-
const { chatId } = await openConversation(args['participantId'], fullCreds(env)
|
|
38
|
+
const { chatId } = await openConversation(args['participantId'], fullCreds(env), {
|
|
39
|
+
newChat: args['newChat'] === true,
|
|
40
|
+
});
|
|
31
41
|
const lister = env.surface === 'mcp' ? 'ziggs_grant_list' : 'grant_list';
|
|
32
42
|
return {
|
|
33
43
|
chatId,
|
|
@@ -97,7 +97,7 @@ export const requestConnectionCapability = {
|
|
|
97
97
|
note: env.surface === 'mcp'
|
|
98
98
|
? 'A connection-consent card is now in the chat awaiting your principal. Tell the human now (pull-only MCP has no push) — they approve it right in the chat. ' +
|
|
99
99
|
'Once approved, the connection + grant appear in ziggs_connection_list for ziggs_connection_proxy.'
|
|
100
|
-
: 'A connection-consent card is now in the chat awaiting your principal — they approve it right there
|
|
100
|
+
: 'A connection-consent card is now in the chat awaiting your principal — they approve it right there. ' +
|
|
101
101
|
'Once approved, the connection + grant appear in grant_list (scopeKind=connection) / connection_list_grants for connection_proxy.',
|
|
102
102
|
};
|
|
103
103
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { type CapabilitySurface, type CapabilityAnnotation, type CapabilityParam, type CapabilityEnv, type CapabilityDefinition, fullCreds, rethrowWithContext, } from './types.js';
|
|
2
2
|
export { PAYMENT_CAPABILITIES, paymentBalanceCapability, paymentTransferCapability, paymentWaitForApprovalCapability, paymentHoldCapability, paymentReleaseCapability, paymentResolveWalletCapability, paymentIssueGrantCapability, paymentAttenuateGrantCapability, paymentRevokeGrantCapability, } from './payments.js';
|
|
3
|
-
export { LINK_CAPABILITIES, createLinkInviteCapability, listLinksCapability,
|
|
3
|
+
export { LINK_CAPABILITIES, createLinkInviteCapability, listLinksCapability, linkIsReachOnly, } from './links.js';
|
|
4
4
|
export { AGREEMENT_CAPABILITIES, agreementClaimCapability } from './agreements.js';
|
|
5
5
|
export { MARKETPLACE_CAPABILITIES, marketplaceViewCapability } from './marketplace.js';
|
|
6
6
|
export { GRANTS_CAPABILITIES, listGrantsCapability } from './grants.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { fullCreds, rethrowWithContext, } from './types.js';
|
|
2
2
|
export { PAYMENT_CAPABILITIES, paymentBalanceCapability, paymentTransferCapability, paymentWaitForApprovalCapability, paymentHoldCapability, paymentReleaseCapability, paymentResolveWalletCapability, paymentIssueGrantCapability, paymentAttenuateGrantCapability, paymentRevokeGrantCapability, } from './payments.js';
|
|
3
|
-
export { LINK_CAPABILITIES, createLinkInviteCapability, listLinksCapability,
|
|
3
|
+
export { LINK_CAPABILITIES, createLinkInviteCapability, listLinksCapability, linkIsReachOnly, } from './links.js';
|
|
4
4
|
export { AGREEMENT_CAPABILITIES, agreementClaimCapability } from './agreements.js';
|
|
5
5
|
export { MARKETPLACE_CAPABILITIES, marketplaceViewCapability } from './marketplace.js';
|
|
6
6
|
export { GRANTS_CAPABILITIES, listGrantsCapability } from './grants.js';
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
import type { Agreement } from '../types.js';
|
|
2
1
|
import { type CapabilityDefinition, type CapabilityEnv } from './types.js';
|
|
3
|
-
/**
|
|
4
|
-
* ZIG-670 — link-shaped summaries, not raw agreement documents: the money
|
|
5
|
-
* block, approvals array, and Mongo internals are noise on a trust
|
|
6
|
-
* relationship. ZIG-956 moved this into the shared layer so the MCP mutations
|
|
7
|
-
* return it too (they used to leak the raw agreement doc).
|
|
8
|
-
*/
|
|
9
|
-
export declare function linkSummary(a: Agreement): Record<string, unknown>;
|
|
10
2
|
/**
|
|
11
3
|
* A link is reach-only — the follow-up move differs by surface tool names.
|
|
12
4
|
*
|
|
@@ -18,27 +18,6 @@ function webAppOrigin(env) {
|
|
|
18
18
|
function inviteShareUrl(env, agreementId) {
|
|
19
19
|
return `${webAppOrigin(env)}/connect/${agreementId}`;
|
|
20
20
|
}
|
|
21
|
-
/**
|
|
22
|
-
* ZIG-670 — link-shaped summaries, not raw agreement documents: the money
|
|
23
|
-
* block, approvals array, and Mongo internals are noise on a trust
|
|
24
|
-
* relationship. ZIG-956 moved this into the shared layer so the MCP mutations
|
|
25
|
-
* return it too (they used to leak the raw agreement doc).
|
|
26
|
-
*/
|
|
27
|
-
export function linkSummary(a) {
|
|
28
|
-
return {
|
|
29
|
-
agreementId: a.agreementId,
|
|
30
|
-
status: a.status,
|
|
31
|
-
proposalStatus: a.proposalStatus,
|
|
32
|
-
parties: {
|
|
33
|
-
creatorAgent: a.parties?.creatorAgent ?? null,
|
|
34
|
-
providerAgent: a.parties?.providerAgent ?? null,
|
|
35
|
-
creator: a.parties?.creator ?? null,
|
|
36
|
-
proposedTo: a.parties?.proposedTo ?? null,
|
|
37
|
-
},
|
|
38
|
-
...(a.description ? { description: a.description } : {}),
|
|
39
|
-
createdAt: a.createdAt,
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
21
|
/**
|
|
43
22
|
* A link is reach-only — the follow-up move differs by surface tool names.
|
|
44
23
|
*
|
|
@@ -106,7 +85,7 @@ export const createLinkInviteCapability = {
|
|
|
106
85
|
message: env.surface === 'mcp'
|
|
107
86
|
? `Open link invite created, ${seatNote}. Give the human shareUrl and nothing else — it is the whole invite. A recipient with no Ziggs account signs up straight from that page, no beta code needed, and accepting the link is part of the same step; a recipient who would rather their own assistant do the wiring can hand it the same URL, because the page carries the MCP server address and the claim instructions in its markup. Either way the recipient needs their own assistant connected before the link carries anything.`
|
|
108
87
|
: `Open link invite created, ${seatNote}. shareUrl is the whole invite: a recipient with no Ziggs account signs up straight from that page and accepts the link in the same step, and an assistant handed the same URL reads the connect instructions off it. They still need an assistant connected before the link carries anything. No agent id needed on either side. Revoke with agreement_revoke to disable.`,
|
|
109
|
-
agreement
|
|
88
|
+
agreement,
|
|
110
89
|
};
|
|
111
90
|
},
|
|
112
91
|
sdkOptions: { isAgreementCreation: true },
|
|
@@ -136,12 +115,11 @@ export const listLinksCapability = {
|
|
|
136
115
|
engagementKind: 'link',
|
|
137
116
|
...(status === 'all' ? {} : { status }),
|
|
138
117
|
}, fullCreds(env));
|
|
139
|
-
const summaries = links.map(linkSummary);
|
|
140
118
|
const hasActive = links.some((a) => a.status === 'active');
|
|
141
119
|
return {
|
|
142
|
-
count:
|
|
120
|
+
count: links.length,
|
|
143
121
|
status,
|
|
144
|
-
links
|
|
122
|
+
links,
|
|
145
123
|
...(hasActive ? { nextSteps: linkIsReachOnly(env) } : {}),
|
|
146
124
|
};
|
|
147
125
|
},
|
|
@@ -216,6 +216,10 @@ export const paymentHoldCapability = {
|
|
|
216
216
|
amount: { type: 'number', required: true, description: 'Amount in integer cents, > 0' },
|
|
217
217
|
description: { type: 'string', description: 'Human-readable hold memo' },
|
|
218
218
|
idempotencyKey: { type: 'string', description: 'Client-supplied retry-safety key' },
|
|
219
|
+
paymentGrantId: {
|
|
220
|
+
type: 'string',
|
|
221
|
+
description: 'Payment grant authorizing this hold (required for agent actors; auto-picks an active wallet grant when omitted)',
|
|
222
|
+
},
|
|
219
223
|
},
|
|
220
224
|
handler: async (args, env) => {
|
|
221
225
|
const amount = args['amount'];
|
|
@@ -226,6 +230,7 @@ export const paymentHoldCapability = {
|
|
|
226
230
|
amount: Math.round(amount),
|
|
227
231
|
description: args['description'] || 'Agent escrow hold',
|
|
228
232
|
idempotencyKey: args['idempotencyKey'],
|
|
233
|
+
paymentGrantId: args['paymentGrantId'],
|
|
229
234
|
});
|
|
230
235
|
return {
|
|
231
236
|
status: 'held',
|
|
@@ -21,6 +21,9 @@ export function rethrowWithContext(error, prefix) {
|
|
|
21
21
|
wrapped.status = e.status;
|
|
22
22
|
if (e.body !== undefined)
|
|
23
23
|
wrapped.body = e.body;
|
|
24
|
+
// ZIG-1124 — keep the machine code so toolError can classify without prose.
|
|
25
|
+
if (typeof e.code === 'string' && e.code)
|
|
26
|
+
wrapped.code = e.code;
|
|
24
27
|
wrapped['cause'] = error;
|
|
25
28
|
throw wrapped;
|
|
26
29
|
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime configuration for this package, injected by whoever owns the process.
|
|
3
|
+
*
|
|
4
|
+
* This package used to read `process.env` directly, which is why every http
|
|
5
|
+
* client began with `import 'dotenv/config'`. Metro (React Native's bundler)
|
|
6
|
+
* cannot resolve dotenv, so a mobile consumer could not import api-client at
|
|
7
|
+
* all. The environment is now read by the host — `env-init.ts` in this repo,
|
|
8
|
+
* the MCP server's config loader, a mobile app's bootstrap — and handed here.
|
|
9
|
+
*
|
|
10
|
+
* Configure before constructing clients: `getBackendUrl()` is called in their
|
|
11
|
+
* constructors, so a client built first keeps the URL that was current then.
|
|
12
|
+
*/
|
|
13
|
+
/** Threshold names accepted by {@link ApiClientConfig.logLevel}. */
|
|
14
|
+
export type ApiClientLogLevel = 'debug' | 'trace' | 'info' | 'warn' | 'error' | 'silent' | 'none';
|
|
15
|
+
export interface ApiClientConfig {
|
|
16
|
+
/** Backend base URL. Falls back to production when nothing sets it. */
|
|
17
|
+
httpUrl?: string;
|
|
18
|
+
/** WebSocket base URL. Falls back to production when nothing sets it. */
|
|
19
|
+
wsUrl?: string;
|
|
20
|
+
/** `runtimeLog` threshold. Falls back to `info`. */
|
|
21
|
+
logLevel?: ApiClientLogLevel;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Merge settings into the package config. Partial: passing only `logLevel`
|
|
25
|
+
* leaves a previously configured `httpUrl` alone. Explicit `undefined` is
|
|
26
|
+
* ignored rather than treated as a clear, so a host can spread a half-filled
|
|
27
|
+
* options object without wiping what it already set.
|
|
28
|
+
*/
|
|
29
|
+
export declare function configureApiClient(next: ApiClientConfig): void;
|
|
30
|
+
/** Current config. Read-only — mutate through {@link configureApiClient}. */
|
|
31
|
+
export declare function apiClientConfig(): Readonly<ApiClientConfig>;
|
|
32
|
+
/** Generation counter for cache invalidation. */
|
|
33
|
+
export declare function apiClientConfigVersion(): number;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime configuration for this package, injected by whoever owns the process.
|
|
3
|
+
*
|
|
4
|
+
* This package used to read `process.env` directly, which is why every http
|
|
5
|
+
* client began with `import 'dotenv/config'`. Metro (React Native's bundler)
|
|
6
|
+
* cannot resolve dotenv, so a mobile consumer could not import api-client at
|
|
7
|
+
* all. The environment is now read by the host — `env-init.ts` in this repo,
|
|
8
|
+
* the MCP server's config loader, a mobile app's bootstrap — and handed here.
|
|
9
|
+
*
|
|
10
|
+
* Configure before constructing clients: `getBackendUrl()` is called in their
|
|
11
|
+
* constructors, so a client built first keeps the URL that was current then.
|
|
12
|
+
*/
|
|
13
|
+
const config = {};
|
|
14
|
+
/**
|
|
15
|
+
* Bumped on every configure so derived caches (the log threshold) know to
|
|
16
|
+
* recompute. A version counter rather than a reset callback because
|
|
17
|
+
* `runtimeLog` reads config — a callback the other way would be a cycle.
|
|
18
|
+
*/
|
|
19
|
+
let version = 0;
|
|
20
|
+
/**
|
|
21
|
+
* Merge settings into the package config. Partial: passing only `logLevel`
|
|
22
|
+
* leaves a previously configured `httpUrl` alone. Explicit `undefined` is
|
|
23
|
+
* ignored rather than treated as a clear, so a host can spread a half-filled
|
|
24
|
+
* options object without wiping what it already set.
|
|
25
|
+
*/
|
|
26
|
+
export function configureApiClient(next) {
|
|
27
|
+
if (next.httpUrl !== undefined)
|
|
28
|
+
config.httpUrl = next.httpUrl;
|
|
29
|
+
if (next.wsUrl !== undefined)
|
|
30
|
+
config.wsUrl = next.wsUrl;
|
|
31
|
+
if (next.logLevel !== undefined)
|
|
32
|
+
config.logLevel = next.logLevel;
|
|
33
|
+
version += 1;
|
|
34
|
+
}
|
|
35
|
+
/** Current config. Read-only — mutate through {@link configureApiClient}. */
|
|
36
|
+
export function apiClientConfig() {
|
|
37
|
+
return config;
|
|
38
|
+
}
|
|
39
|
+
/** Generation counter for cache invalidation. */
|
|
40
|
+
export function apiClientConfigVersion() {
|
|
41
|
+
return version;
|
|
42
|
+
}
|