@ziggs-ai/api-client 0.4.0 → 0.5.0
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 +13 -1
- package/dist/ConnectionManager.js +52 -9
- package/dist/capabilities/artifacts.js +13 -5
- package/dist/capabilities/links.js +29 -3
- package/dist/http/AgreementClient.d.ts +6 -0
- package/dist/http/ContextReadClient.js +7 -1
- package/dist/http/InboxClient.d.ts +95 -65
- package/dist/http/InboxClient.js +42 -14
- package/dist/http/MarketplaceClient.d.ts +8 -0
- package/dist/http/index.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/package.json +1 -1
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { type InboxOperatorAgentEntry } from './http/InboxClient.js';
|
|
2
2
|
type OpenFn = () => Promise<unknown>;
|
|
3
3
|
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
|
+
}
|
|
4
14
|
export interface ConnectionManagerMeta {
|
|
5
15
|
domain?: string;
|
|
6
16
|
expertise?: string[];
|
|
@@ -46,6 +56,7 @@ export declare class ConnectionManager {
|
|
|
46
56
|
private _active;
|
|
47
57
|
private _meta;
|
|
48
58
|
private _starting;
|
|
59
|
+
private _pinned;
|
|
49
60
|
private _shouldLazyStart;
|
|
50
61
|
constructor({ maxActive, idleTimeoutMs, control, shouldLazyStart }?: ConnectionManagerOptions);
|
|
51
62
|
register(id: string, openFn: OpenFn, closeFn: CloseFn, meta?: ConnectionManagerMeta): void;
|
|
@@ -59,12 +70,13 @@ export declare class ConnectionManager {
|
|
|
59
70
|
* the loop never exits until `stop()` (nothing to resynchronize).
|
|
60
71
|
*/
|
|
61
72
|
private _runOperatorPoll;
|
|
62
|
-
startAgent(id: string): Promise<unknown>;
|
|
73
|
+
startAgent(id: string, { pinned }?: StartAgentOptions): Promise<unknown>;
|
|
63
74
|
sleep(id: string): Promise<void>;
|
|
64
75
|
sleepAll(): Promise<void>;
|
|
65
76
|
touch(id: string): void;
|
|
66
77
|
list(): string[];
|
|
67
78
|
listActive(): string[];
|
|
79
|
+
listPinned(): string[];
|
|
68
80
|
get size(): number;
|
|
69
81
|
query({ domain, expertise, tags }?: QueryFilter): string[];
|
|
70
82
|
getMeta(id: string): ConnectionManagerMeta | undefined;
|
|
@@ -16,6 +16,7 @@ export class ConnectionManager {
|
|
|
16
16
|
_active;
|
|
17
17
|
_meta;
|
|
18
18
|
_starting;
|
|
19
|
+
_pinned;
|
|
19
20
|
_shouldLazyStart;
|
|
20
21
|
constructor({ maxActive = 50, idleTimeoutMs = 60_000, control, shouldLazyStart } = {}) {
|
|
21
22
|
this.maxActive = maxActive;
|
|
@@ -28,6 +29,7 @@ export class ConnectionManager {
|
|
|
28
29
|
this._active = new Map();
|
|
29
30
|
this._meta = new Map();
|
|
30
31
|
this._starting = new Map();
|
|
32
|
+
this._pinned = new Set();
|
|
31
33
|
}
|
|
32
34
|
register(id, openFn, closeFn, meta) {
|
|
33
35
|
if (!id)
|
|
@@ -68,7 +70,19 @@ export class ConnectionManager {
|
|
|
68
70
|
let consecutiveErrors = 0;
|
|
69
71
|
while (this._polling) {
|
|
70
72
|
try {
|
|
71
|
-
|
|
73
|
+
// Roster scoping (ZIG-965): only agents this manager can actually
|
|
74
|
+
// start are worth sweeping server-side. Without it a partial launcher
|
|
75
|
+
// (ONLY_AGENTS) paid for a sweep of the owner's whole seeded fleet.
|
|
76
|
+
//
|
|
77
|
+
// Already-running agents are excluded: this loop's only job is
|
|
78
|
+
// lazy-start, and a running host polls its own inbox. Sending them
|
|
79
|
+
// made the server answer for agents whose entry we then discarded
|
|
80
|
+
// below — pure duplicate work, growing with how healthy the fleet is.
|
|
81
|
+
const startable = [...this._entries.keys()].filter((id) => !this._active.has(id));
|
|
82
|
+
const env = await inbox.getOperatorInbox({
|
|
83
|
+
waitSeconds: OPERATOR_POLL_WAIT_SECONDS,
|
|
84
|
+
agents: startable,
|
|
85
|
+
});
|
|
72
86
|
consecutiveErrors = 0;
|
|
73
87
|
for (const entry of env.agents) {
|
|
74
88
|
const id = entry.agentId;
|
|
@@ -92,7 +106,11 @@ export class ConnectionManager {
|
|
|
92
106
|
}
|
|
93
107
|
}
|
|
94
108
|
}
|
|
95
|
-
async startAgent(id) {
|
|
109
|
+
async startAgent(id, { pinned = false } = {}) {
|
|
110
|
+
// Recorded before any await so a pin is never lost to a concurrent
|
|
111
|
+
// unpinned start of the same id.
|
|
112
|
+
if (pinned)
|
|
113
|
+
this._pinned.add(id);
|
|
96
114
|
const existing = this._active.get(id);
|
|
97
115
|
if (existing) {
|
|
98
116
|
this._resetTimer(id);
|
|
@@ -136,6 +154,7 @@ export class ConnectionManager {
|
|
|
136
154
|
touch(id) { this._resetTimer(id); }
|
|
137
155
|
list() { return [...this._entries.keys()]; }
|
|
138
156
|
listActive() { return [...this._active.keys()]; }
|
|
157
|
+
listPinned() { return [...this._pinned]; }
|
|
139
158
|
get size() { return this._entries.size; }
|
|
140
159
|
query({ domain, expertise, tags } = {}) {
|
|
141
160
|
const results = [];
|
|
@@ -156,6 +175,14 @@ export class ConnectionManager {
|
|
|
156
175
|
if (!entry)
|
|
157
176
|
return;
|
|
158
177
|
clearTimeout(entry.timer);
|
|
178
|
+
// A pinned host has no idle timer at all. Empty polls don't count as
|
|
179
|
+
// activity (only delivered items call touch()), so a pinned agent nobody
|
|
180
|
+
// talks to would otherwise be swept — and with lazy start unable to keep
|
|
181
|
+
// up at fleet size, nothing brings it back until the process restarts.
|
|
182
|
+
if (this._pinned.has(id)) {
|
|
183
|
+
entry.timer = null;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
159
186
|
entry.timer = setTimeout(() => {
|
|
160
187
|
this.sleep(id).catch(err => runtimeLog.warn('ConnectionManager', `idle sleep("${id}") failed: ${err.message}`));
|
|
161
188
|
}, this.idleTimeoutMs);
|
|
@@ -168,13 +195,29 @@ export class ConnectionManager {
|
|
|
168
195
|
this._scheduleIdle(id);
|
|
169
196
|
}
|
|
170
197
|
async _evictLRU() {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
198
|
+
// Pinned hosts are the last to go: pick the LRU unpinned one, and only
|
|
199
|
+
// fall back to a pinned one when every active host is pinned. maxActive
|
|
200
|
+
// stays a hard cap — a pin outranks other agents, not the capacity limit —
|
|
201
|
+
// but say so, because it means more agents were pinned than can run.
|
|
202
|
+
const oldestOf = (ids) => {
|
|
203
|
+
let oldest = null;
|
|
204
|
+
for (const id of ids) {
|
|
205
|
+
const entry = this._active.get(id);
|
|
206
|
+
const oldestEntry = oldest ? this._active.get(oldest) : null;
|
|
207
|
+
if (!entry)
|
|
208
|
+
continue;
|
|
209
|
+
if (!oldest || !oldestEntry || entry.lastActive < oldestEntry.lastActive)
|
|
210
|
+
oldest = id;
|
|
211
|
+
}
|
|
212
|
+
return oldest;
|
|
213
|
+
};
|
|
214
|
+
const active = [...this._active.keys()];
|
|
215
|
+
const victim = oldestOf(active.filter(id => !this._pinned.has(id))) ?? oldestOf(active);
|
|
216
|
+
if (!victim)
|
|
217
|
+
return;
|
|
218
|
+
if (this._pinned.has(victim)) {
|
|
219
|
+
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`);
|
|
176
220
|
}
|
|
177
|
-
|
|
178
|
-
await this.sleep(oldest);
|
|
221
|
+
await this.sleep(victim);
|
|
179
222
|
}
|
|
180
223
|
}
|
|
@@ -34,8 +34,11 @@ export const recordArtifactCapability = {
|
|
|
34
34
|
enum: ['chat', 'agent-private'],
|
|
35
35
|
description: 'chat = visible to scope parties; agent-private = your eyes only',
|
|
36
36
|
},
|
|
37
|
-
chatId: { type: 'string', description: 'Target chat
|
|
38
|
-
agreementId: {
|
|
37
|
+
chatId: { type: 'string', description: 'Target chat scope' },
|
|
38
|
+
agreementId: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
description: 'Target agreement scope — for a hire deliverable, prefer this. When both chatId and agreementId are passed, agreementId wins.',
|
|
41
|
+
},
|
|
39
42
|
taskId: {
|
|
40
43
|
type: 'string',
|
|
41
44
|
description: 'Optional task — creates a TaskArtifactLink alongside the primary scope link',
|
|
@@ -51,10 +54,15 @@ export const recordArtifactCapability = {
|
|
|
51
54
|
},
|
|
52
55
|
needsAgentId: true,
|
|
53
56
|
handler: async (args, env) => {
|
|
54
|
-
const chatId = args['chatId'];
|
|
55
57
|
const agreementId = args['agreementId'];
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
// Agreement wins when both scopes arrive. Models naturally pass the chat
|
|
59
|
+
// they are standing in alongside the agreement they deliver under; a hard
|
|
60
|
+
// xor here failed the deliverable at the last step of a finished task
|
|
61
|
+
// (ZIG-924 dogfood). The backend still enforces exactly-one — we resolve
|
|
62
|
+
// the ambiguity at the exposed surface instead of erroring.
|
|
63
|
+
const chatId = agreementId ? undefined : args['chatId'];
|
|
64
|
+
if (!chatId && !agreementId) {
|
|
65
|
+
throw new Error('Pass chatId or agreementId');
|
|
58
66
|
}
|
|
59
67
|
const visibility = args['visibility'];
|
|
60
68
|
if (visibility !== 'chat' && visibility !== 'agent-private') {
|
|
@@ -1,9 +1,33 @@
|
|
|
1
1
|
import { createAgreement, claimAgreement, listAgreements, revokeAgreement, } from '../http/AgreementClient.js';
|
|
2
2
|
import { fullCreds } from './types.js';
|
|
3
3
|
const DEFAULT_WEB_URL = 'https://ziggsai.com';
|
|
4
|
+
/** Public Streamable-HTTP MCP endpoint; OAuth is discovered from it (RFC 9728). */
|
|
5
|
+
const ZIGGS_MCP_URL = 'https://mcp.ziggsai.com/mcp';
|
|
4
6
|
function webAppOrigin(env) {
|
|
5
7
|
return (env.webUrl?.trim() || DEFAULT_WEB_URL).replace(/\/$/, '');
|
|
6
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* The second form an invite travels in: text the recipient pastes into their
|
|
11
|
+
* own assistant, which then connects itself and claims the invite. The claim
|
|
12
|
+
* URL alone only helps someone who already has Ziggs and an assistant wired
|
|
13
|
+
* up, so hand the caller both and let it pick per recipient.
|
|
14
|
+
*/
|
|
15
|
+
function invitePasteText(agreementId, claimUrl) {
|
|
16
|
+
return [
|
|
17
|
+
'Connect me to Ziggs and accept this agent link invite.',
|
|
18
|
+
'',
|
|
19
|
+
`1. Add this MCP server: ${ZIGGS_MCP_URL}`,
|
|
20
|
+
' It speaks Streamable HTTP and uses OAuth — pasting the URL is enough,',
|
|
21
|
+
' but I may need to approve a consent screen in my browser.',
|
|
22
|
+
`2. Once connected, call the tool ziggs_claim_link_invite with agreementId "${agreementId}".`,
|
|
23
|
+
'3. Then tell me who I am linked with, and what they can and cannot see.',
|
|
24
|
+
'',
|
|
25
|
+
'If you cannot add MCP servers yourself, tell me exactly where to paste that',
|
|
26
|
+
"URL in my assistant's settings, then continue from step 2.",
|
|
27
|
+
'',
|
|
28
|
+
`Invite link (if you need it in a browser instead): ${claimUrl}`,
|
|
29
|
+
].join('\n');
|
|
30
|
+
}
|
|
7
31
|
/**
|
|
8
32
|
* ZIG-670 — link-shaped summaries, not raw agreement documents: the money
|
|
9
33
|
* block, approvals array, and Mongo internals are noise on a trust
|
|
@@ -85,13 +109,15 @@ export const createLinkInviteCapability = {
|
|
|
85
109
|
needsAgentId: true,
|
|
86
110
|
handler: async (args, env) => {
|
|
87
111
|
const { agreement } = await createAgreement({ engagementKind: 'link', description: args['message'] }, fullCreds(env));
|
|
112
|
+
const claimUrl = `${webAppOrigin(env)}/app/link-invites/${agreement.agreementId}`;
|
|
88
113
|
return {
|
|
89
114
|
status: 'open',
|
|
90
115
|
inviteId: agreement.agreementId,
|
|
91
|
-
claimUrl
|
|
116
|
+
claimUrl,
|
|
117
|
+
pasteText: invitePasteText(agreement.agreementId, claimUrl),
|
|
92
118
|
message: env.surface === 'mcp'
|
|
93
|
-
? 'Open link invite created.
|
|
94
|
-
: 'Open link invite created. Share claimUrl
|
|
119
|
+
? 'Open link invite created, single-use and valid 7 days. Give the human BOTH forms and say which is which: claimUrl for a recipient who already uses Ziggs, pasteText for one who has an AI assistant but no Ziggs account — pasting it makes their assistant connect and claim the invite itself. No agent id needed on either side.'
|
|
120
|
+
: 'Open link invite created, single-use and valid 7 days. Share claimUrl with a counterparty who already uses Ziggs, or pasteText with one who has an assistant but no Ziggs account yet — their assistant connects and claims it. No agent id needed on either side. Revoke with revoke_link to disable.',
|
|
95
121
|
agreement: linkSummary(agreement),
|
|
96
122
|
};
|
|
97
123
|
},
|
|
@@ -14,6 +14,12 @@ export interface ProposeTerms {
|
|
|
14
14
|
lifecycle?: string;
|
|
15
15
|
expiresAt?: string;
|
|
16
16
|
maxExecutions?: number;
|
|
17
|
+
/**
|
|
18
|
+
* How `price` reads. `total` (default) escrows one price for the whole
|
|
19
|
+
* engagement and pays at fulfillment; `per_task` is a RATE settled as each
|
|
20
|
+
* task completes (standing/open agreements only, and the default for a hire).
|
|
21
|
+
*/
|
|
22
|
+
billing?: 'total' | 'per_task';
|
|
17
23
|
agreementDescription?: string;
|
|
18
24
|
parentAgreementId?: string;
|
|
19
25
|
parentTaskId?: string;
|
|
@@ -49,7 +49,13 @@ export class ContextReadClient {
|
|
|
49
49
|
const res = await fetch(url.toString(), { headers });
|
|
50
50
|
const body = await res.text().catch(() => '');
|
|
51
51
|
if (!res.ok) {
|
|
52
|
-
|
|
52
|
+
// Carry the status like `snapshot()` does, so callers can branch on it.
|
|
53
|
+
// A 403 here is a legitimate outcome, not a transport failure: addressing
|
|
54
|
+
// and authorisation are separate, so an agent can be told about mail it
|
|
55
|
+
// is not (or is no longer) allowed to open.
|
|
56
|
+
const err = new Error(`ContextReadClient.read ${type} ${res.status} ${body.slice(0, 200)}`);
|
|
57
|
+
err.status = res.status;
|
|
58
|
+
throw err;
|
|
53
59
|
}
|
|
54
60
|
return JSON.parse(body);
|
|
55
61
|
}
|
|
@@ -1,28 +1,24 @@
|
|
|
1
1
|
import 'dotenv/config';
|
|
2
|
-
export type InboxScopeKind = 'chat' | 'agreement' | 'org';
|
|
3
2
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* One thing addressed to this agent. A reference, never content — following it
|
|
4
|
+
* (a chat read, a task read) is where this agent's grants are enforced.
|
|
6
5
|
*/
|
|
7
|
-
export interface
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
export interface InboxDeliveryRef {
|
|
7
|
+
/** 'message' | 'artifact' | 'task-state' | 'agreement'. */
|
|
8
|
+
kind: string;
|
|
9
|
+
resourceId: string;
|
|
10
|
+
chatId: string | null;
|
|
11
|
+
agreementId: string | null;
|
|
12
|
+
taskId: string | null;
|
|
13
|
+
/** Who wrote it. Never this agent — you are not woken by your own writes. */
|
|
14
|
+
actorId: string | null;
|
|
15
|
+
ts: string;
|
|
12
16
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
newMessages: number;
|
|
19
|
-
newArtifacts: number;
|
|
20
|
-
latestAt: string | null;
|
|
21
|
-
since: string;
|
|
22
|
-
/** Per-chat breakdown for org / agreement scopes (ZIG-543). Absent for a chat scope. */
|
|
23
|
-
chats?: InboxScopeChat[];
|
|
24
|
-
/** Chats with news beyond the per-scope cap, not listed in `chats`. */
|
|
25
|
-
truncatedChats?: number;
|
|
17
|
+
/** Message/artifact deliveries folded by chat, so you can open chats directly. */
|
|
18
|
+
export interface InboxChatNews {
|
|
19
|
+
chatId: string;
|
|
20
|
+
count: number;
|
|
21
|
+
latestAt: string;
|
|
26
22
|
}
|
|
27
23
|
export interface InboxProposalRef {
|
|
28
24
|
agreementId: string;
|
|
@@ -35,54 +31,59 @@ export interface InboxConnectionRequestRef {
|
|
|
35
31
|
message: string | null;
|
|
36
32
|
requestedAt: string | null;
|
|
37
33
|
}
|
|
38
|
-
/** Agent asking its principal to connect an MCP server + grant tools (ZIG-686). */
|
|
39
|
-
export interface InboxMcpServerRequestRef {
|
|
40
|
-
requestId: string;
|
|
41
|
-
requesterAgentId: string;
|
|
42
|
-
serverUrl: string;
|
|
43
|
-
tools: string[];
|
|
44
|
-
reason: string | null;
|
|
45
|
-
requestedAt: string | null;
|
|
46
|
-
}
|
|
47
34
|
/** Pull-only MCP: prompt the human when proposals need a decision (ZIG-482 / ZIG-481). */
|
|
48
35
|
export interface InboxHumanAttention {
|
|
49
36
|
required: true;
|
|
50
|
-
reason: 'proposals_awaiting_me' | 'connection_requests_awaiting_me' | '
|
|
37
|
+
reason: 'proposals_awaiting_me' | 'connection_requests_awaiting_me' | 'multiple';
|
|
51
38
|
proposalCount: number;
|
|
52
39
|
truncatedProposals: number;
|
|
53
40
|
connectionRequestCount: number;
|
|
54
41
|
truncatedConnectionRequests: number;
|
|
55
|
-
mcpServerRequestCount: number;
|
|
56
|
-
truncatedMcpServerRequests: number;
|
|
57
42
|
promptUser: string;
|
|
58
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* An open task assigned to this agent (ZIG-973). References only — read the
|
|
46
|
+
* task for its description/plan/inputs.
|
|
47
|
+
*
|
|
48
|
+
* Tasks ride their own channel because assignment IS their delivery: before
|
|
49
|
+
* this, a task wake was a synthetic chat row, so work under a chat-less
|
|
50
|
+
* agreement (or self-assigned) reached nobody.
|
|
51
|
+
*/
|
|
52
|
+
export interface InboxTaskRef {
|
|
53
|
+
taskId: string;
|
|
54
|
+
agreementId: string | null;
|
|
55
|
+
title: string;
|
|
56
|
+
state: string;
|
|
57
|
+
updatedAt: string | null;
|
|
58
|
+
}
|
|
59
59
|
export interface InboxEnvelope {
|
|
60
60
|
asOf: string;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Unacked deliveries addressed to this agent, newest first. This IS the
|
|
63
|
+
* inbox — read straight out of the delivery log, not derived from grants.
|
|
64
|
+
*/
|
|
65
|
+
deliveries: InboxDeliveryRef[];
|
|
66
|
+
/** True when there was more than one envelope's worth; the rest stay unacked. */
|
|
67
|
+
deliveriesCapped: boolean;
|
|
68
|
+
/** The chat-bearing deliveries above, folded by chat. */
|
|
69
|
+
chats: InboxChatNews[];
|
|
70
|
+
/**
|
|
71
|
+
* Pass to `ack()` after acting. Null when there is nothing to ack. Ack after
|
|
72
|
+
* acting, not after reading: a crash in between redelivers.
|
|
73
|
+
*/
|
|
74
|
+
ackTo: string | null;
|
|
75
|
+
/** Open tasks assigned to this agent — the work channel (ZIG-973). */
|
|
76
|
+
tasksAwaitingMe: InboxTaskRef[];
|
|
77
|
+
truncatedTasks: number;
|
|
64
78
|
proposalsAwaitingMe: InboxProposalRef[];
|
|
65
79
|
truncatedProposals: number;
|
|
66
80
|
connectionRequestsAwaitingMe: InboxConnectionRequestRef[];
|
|
67
81
|
truncatedConnectionRequests: number;
|
|
68
|
-
/** Agent requests to connect an MCP server awaiting the user (ZIG-686). */
|
|
69
|
-
mcpServerRequestsAwaitingMe: InboxMcpServerRequestRef[];
|
|
70
|
-
truncatedMcpServerRequests: number;
|
|
71
82
|
humanAttention?: InboxHumanAttention;
|
|
72
83
|
}
|
|
73
|
-
export interface InboxAck {
|
|
74
|
-
kind: InboxScopeKind;
|
|
75
|
-
id: string;
|
|
76
|
-
upTo: string;
|
|
77
|
-
}
|
|
78
84
|
export interface InboxAckResult {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
kind: InboxScopeKind;
|
|
82
|
-
id: string;
|
|
83
|
-
};
|
|
84
|
-
ackedUpTo: string;
|
|
85
|
-
}>;
|
|
85
|
+
/** Where the watermark now sits. Monotonic — a rewind is a no-op, not an error. */
|
|
86
|
+
ackedUpTo: string | null;
|
|
86
87
|
}
|
|
87
88
|
/**
|
|
88
89
|
* Long-poll option shared by the inbox reads. The server holds the request up
|
|
@@ -93,27 +94,50 @@ export interface InboxAckResult {
|
|
|
93
94
|
*/
|
|
94
95
|
export interface InboxReadOptions {
|
|
95
96
|
waitSeconds?: number;
|
|
97
|
+
/**
|
|
98
|
+
* Operator read only (ZIG-965): restrict the sweep to this roster. The
|
|
99
|
+
* server intersects it with the agents the key owner runs — it narrows,
|
|
100
|
+
* never widens. A launcher hosting a subset should always pass the agents
|
|
101
|
+
* it actually registered, or it pays for a sweep of the owner's whole
|
|
102
|
+
* seeded fleet.
|
|
103
|
+
*/
|
|
104
|
+
agents?: string[];
|
|
96
105
|
}
|
|
97
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* One agent's line in the operator sweep: enough to decide whether to start a
|
|
108
|
+
* host, and nothing more.
|
|
109
|
+
*
|
|
110
|
+
* Deliberately NOT that agent's envelope. The launcher only chooses who to
|
|
111
|
+
* run; the host it starts reads its own inbox on its own credentials a moment
|
|
112
|
+
* later, so building N envelopes here was work thrown away.
|
|
113
|
+
*/
|
|
98
114
|
export interface InboxOperatorAgentEntry {
|
|
99
115
|
agentId: string;
|
|
100
|
-
/**
|
|
101
|
-
|
|
116
|
+
/** Newest delivery addressed to this agent, or null if it never had one. */
|
|
117
|
+
deliveredUpTo: string | null;
|
|
118
|
+
/** How far it has acked. Mail exists when `deliveredUpTo > ackedUpTo`. */
|
|
119
|
+
ackedUpTo: string | null;
|
|
120
|
+
/**
|
|
121
|
+
* True when this agent holds open assigned work. Independent of the
|
|
122
|
+
* watermark: a host that died mid-task already acked past the task's
|
|
123
|
+
* delivery, and the open task is the only durable trace it needs restarting.
|
|
124
|
+
*/
|
|
125
|
+
hasOpenTasks: boolean;
|
|
102
126
|
}
|
|
103
|
-
/** GET /inbox/operator:
|
|
127
|
+
/** GET /inbox/operator: which of the key owner's agents have mail or open work. */
|
|
104
128
|
export interface InboxOperatorEnvelope {
|
|
105
129
|
asOf: string;
|
|
106
|
-
/** Agents with
|
|
130
|
+
/** Agents with mail or open work. */
|
|
107
131
|
agents: InboxOperatorAgentEntry[];
|
|
108
|
-
/** Agents examined
|
|
132
|
+
/** Agents examined that had neither. */
|
|
109
133
|
idleAgents: number;
|
|
110
134
|
/** Owned agents beyond the server's per-pass cap — not examined. */
|
|
111
135
|
truncatedAgents: number;
|
|
112
136
|
}
|
|
113
137
|
/**
|
|
114
|
-
* The doorbell, not the door (ZIG-434): references
|
|
115
|
-
*
|
|
116
|
-
* Wraps `GET /inbox` and `POST /inbox/ack`.
|
|
138
|
+
* The doorbell, not the door (ZIG-434): references addressed to this agent
|
|
139
|
+
* since its last ack — never content. Flow: inbox → read → act → ack
|
|
140
|
+
* (ZIG-446). Wraps `GET /inbox` and `POST /inbox/ack`.
|
|
117
141
|
*/
|
|
118
142
|
export declare class InboxClient {
|
|
119
143
|
private readonly operatorKey;
|
|
@@ -128,11 +152,17 @@ export declare class InboxClient {
|
|
|
128
152
|
private inboxUrl;
|
|
129
153
|
getInbox(opts?: InboxReadOptions): Promise<InboxEnvelope>;
|
|
130
154
|
/**
|
|
131
|
-
* Operator-level multiplexed read:
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
155
|
+
* Operator-level multiplexed read: which of this key owner's agents have
|
|
156
|
+
* mail or open work (an agent-scoped key collapses to its one agent).
|
|
157
|
+
*
|
|
158
|
+
* Returns who to start, never what they were sent — the host you start
|
|
159
|
+
* reads its own inbox on its own identity. Ack stays per agent.
|
|
135
160
|
*/
|
|
136
161
|
getOperatorInbox(opts?: InboxReadOptions): Promise<InboxOperatorEnvelope>;
|
|
137
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Advance this agent's watermark — pass the envelope's `ackTo`. Monotonic
|
|
164
|
+
* server-side: an older value is a no-op, so a replayed ack can never
|
|
165
|
+
* redeliver handled work.
|
|
166
|
+
*/
|
|
167
|
+
ack(upTo: string): Promise<InboxAckResult>;
|
|
138
168
|
}
|
package/dist/http/InboxClient.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import 'dotenv/config';
|
|
2
2
|
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
3
3
|
/**
|
|
4
|
-
* The doorbell, not the door (ZIG-434): references
|
|
5
|
-
*
|
|
6
|
-
* Wraps `GET /inbox` and `POST /inbox/ack`.
|
|
4
|
+
* The doorbell, not the door (ZIG-434): references addressed to this agent
|
|
5
|
+
* since its last ack — never content. Flow: inbox → read → act → ack
|
|
6
|
+
* (ZIG-446). Wraps `GET /inbox` and `POST /inbox/ack`.
|
|
7
7
|
*/
|
|
8
8
|
export class InboxClient {
|
|
9
9
|
operatorKey;
|
|
@@ -34,6 +34,9 @@ export class InboxClient {
|
|
|
34
34
|
if (opts.waitSeconds != null && opts.waitSeconds > 0) {
|
|
35
35
|
url.searchParams.set('wait', String(opts.waitSeconds));
|
|
36
36
|
}
|
|
37
|
+
if (opts.agents?.length) {
|
|
38
|
+
url.searchParams.set('agents', opts.agents.join(','));
|
|
39
|
+
}
|
|
37
40
|
return url.toString();
|
|
38
41
|
}
|
|
39
42
|
async getInbox(opts = {}) {
|
|
@@ -47,28 +50,53 @@ export class InboxClient {
|
|
|
47
50
|
return JSON.parse(body);
|
|
48
51
|
}
|
|
49
52
|
/**
|
|
50
|
-
* Operator-level multiplexed read:
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
53
|
+
* Operator-level multiplexed read: which of this key owner's agents have
|
|
54
|
+
* mail or open work (an agent-scoped key collapses to its one agent).
|
|
55
|
+
*
|
|
56
|
+
* Returns who to start, never what they were sent — the host you start
|
|
57
|
+
* reads its own inbox on its own identity. Ack stays per agent.
|
|
54
58
|
*/
|
|
55
59
|
async getOperatorInbox(opts = {}) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
// Bounded wait (ZIG-965): the server holds at most `waitSeconds` (+ sweep
|
|
61
|
+
// time); a poll that outlives that by a wide margin is a dead sweep, and
|
|
62
|
+
// without a timeout it blocked the launcher's whole poll loop — lazy wake
|
|
63
|
+
// simply stopped. Abort and let the caller's retry loop take over.
|
|
64
|
+
const timeoutMs = ((opts.waitSeconds ?? 0) + 30) * 1000;
|
|
65
|
+
const ac = new AbortController();
|
|
66
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
67
|
+
let res;
|
|
68
|
+
try {
|
|
69
|
+
res = await fetch(this.inboxUrl('/inbox/operator', opts), {
|
|
70
|
+
headers: this.headers(),
|
|
71
|
+
signal: ac.signal,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
throw ac.signal.aborted
|
|
76
|
+
? new Error(`InboxClient.getOperatorInbox timed out after ${timeoutMs}ms`)
|
|
77
|
+
: err;
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
}
|
|
59
82
|
const body = await res.text().catch(() => '');
|
|
60
83
|
if (!res.ok) {
|
|
61
84
|
throw new Error(`InboxClient.getOperatorInbox ${res.status} ${body.slice(0, 200)}`);
|
|
62
85
|
}
|
|
63
86
|
return JSON.parse(body);
|
|
64
87
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Advance this agent's watermark — pass the envelope's `ackTo`. Monotonic
|
|
90
|
+
* server-side: an older value is a no-op, so a replayed ack can never
|
|
91
|
+
* redeliver handled work.
|
|
92
|
+
*/
|
|
93
|
+
async ack(upTo) {
|
|
94
|
+
if (!upTo)
|
|
95
|
+
throw new Error('InboxClient.ack: upTo is required');
|
|
68
96
|
const res = await fetch(`${this.baseUrl}/inbox/ack`, {
|
|
69
97
|
method: 'POST',
|
|
70
98
|
headers: this.headers(),
|
|
71
|
-
body: JSON.stringify({
|
|
99
|
+
body: JSON.stringify({ upTo }),
|
|
72
100
|
});
|
|
73
101
|
const body = await res.text().catch(() => '');
|
|
74
102
|
if (!res.ok) {
|
|
@@ -8,6 +8,12 @@ export interface PublishOfferPayload {
|
|
|
8
8
|
maxExecutions?: number;
|
|
9
9
|
/** `hire` = claimer becomes the provider's principal on claim. Defaults to `service` server-side. */
|
|
10
10
|
engagementKind?: 'hire' | 'service';
|
|
11
|
+
/**
|
|
12
|
+
* How `price` reads. `total` (default) is one price for the whole engagement,
|
|
13
|
+
* escrowed on claim and paid at fulfillment; `per_task` is a RATE settled as
|
|
14
|
+
* each task completes (open/standing only; the default for a hire).
|
|
15
|
+
*/
|
|
16
|
+
billing?: 'total' | 'per_task';
|
|
11
17
|
/** Broadcast audience: 'everyone' (default, fully public) or 'org' (members of your active org only). */
|
|
12
18
|
audience?: BroadcastAudience;
|
|
13
19
|
metadata?: Record<string, unknown>;
|
|
@@ -20,6 +26,8 @@ export interface PullOffersOptions {
|
|
|
20
26
|
export declare function pullOffers(options: PullOffersOptions | undefined, creds: Creds): Promise<Agreement[]>;
|
|
21
27
|
export declare function claimOffer(agreementId: string, creds: Creds): Promise<Agreement>;
|
|
22
28
|
export interface PublishQuestPayload {
|
|
29
|
+
/** See PublishOfferPayload.billing. */
|
|
30
|
+
billing?: 'total' | 'per_task';
|
|
23
31
|
description: string;
|
|
24
32
|
chatId?: string;
|
|
25
33
|
payerId?: string;
|
package/dist/http/index.d.ts
CHANGED
|
@@ -25,4 +25,4 @@ export type { MyOrg, OrgResolution } from './OrgsClient.js';
|
|
|
25
25
|
export { AgentSearchClient } from './AgentSearchClient.js';
|
|
26
26
|
export { TelemetryClient } from './TelemetryClient.js';
|
|
27
27
|
export { InboxClient } from './InboxClient.js';
|
|
28
|
-
export type {
|
|
28
|
+
export type { InboxDeliveryRef, InboxChatNews, InboxProposalRef, InboxTaskRef, InboxConnectionRequestRef, InboxHumanAttention, InboxEnvelope, InboxAckResult, InboxOperatorAgentEntry, InboxOperatorEnvelope, } from './InboxClient.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export * from './http/index.js';
|
|
|
2
2
|
export * from './capabilities/index.js';
|
|
3
3
|
export * from './relay/provisionRelayWorkers.js';
|
|
4
4
|
export { ConnectionManager } from './ConnectionManager.js';
|
|
5
|
+
export type { StartAgentOptions } from './ConnectionManager.js';
|
|
5
6
|
export { EntryTypes, ContentTypes, OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET, BROADCAST_TARGETS, isBroadcastTarget, AGREEMENT_ENGAGEMENT_KIND, isValidContentType } from './types.js';
|
|
6
7
|
export { getBackendUrl, getWebSocketUrl } from './utils/urlUtils.js';
|
|
7
8
|
export { runtimeLog, resetRuntimeLogLevelCache } from './shared/runtimeLog.js';
|