@ziggs-ai/agent-sdk 0.19.0 → 0.21.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/Agent.d.ts +28 -7
- package/dist/Agent.js +177 -72
- package/dist/brain/ClaudeBrain.d.ts +11 -1
- package/dist/brain/ClaudeBrain.js +71 -18
- package/dist/brain/ZiggsBrain.d.ts +2 -0
- package/dist/brain/ZiggsBrain.js +1 -0
- package/dist/brain/claudeCognition.d.ts +14 -1
- package/dist/brain/claudeCognition.js +106 -6
- package/dist/brain/claudeSdkProtocol.d.ts +1 -12
- package/dist/brain/claudeSdkProtocol.js +4 -1
- package/dist/brain/claudeSdkTools.d.ts +3 -0
- package/dist/brain/claudeSdkTools.js +4 -2
- package/dist/brain/hireActivationNotice.d.ts +19 -0
- package/dist/brain/hireActivationNotice.js +79 -0
- package/dist/brain/standingOfferNotice.d.ts +15 -0
- package/dist/brain/standingOfferNotice.js +89 -0
- package/dist/inboundDiscoveryPolicy.d.ts +2 -0
- package/dist/inboundDiscoveryPolicy.js +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/ingress/normalizeIncoming.d.ts +3 -0
- package/dist/ingress/normalizeIncoming.js +8 -0
- package/dist/mcp/createMcpAgent.d.ts +1 -1
- package/dist/mcp/createMcpAgent.js +1 -1
- package/dist/memory/MemoryStore.d.ts +18 -4
- package/dist/memory/MemoryStore.js +52 -12
- package/dist/platform/ConnectionPool.d.ts +9 -4
- package/dist/platform/ConnectionPool.js +16 -6
- package/dist/platform/StateTimers.d.ts +12 -6
- package/dist/platform/StateTimers.js +26 -6
- package/dist/platform/ZiggsEffectHandler.js +5 -1
- package/dist/platform/inboxLoop.d.ts +5 -7
- package/dist/platform/inboxLoop.js +113 -96
- package/dist/platform/polls/declaredPollRunner.d.ts +7 -1
- package/dist/platform/polls/declaredPollRunner.js +7 -9
- package/dist/platform/tools/index.d.ts +1 -0
- package/dist/platform/tools/index.js +1 -0
- package/dist/platform/tools/tier2/contextTools.d.ts +2 -0
- package/dist/platform/tools/tier2/contextTools.js +3 -1
- package/dist/platform/tools/tier2/planTools.d.ts +20 -0
- package/dist/platform/tools/tier2/planTools.js +64 -0
- package/dist/pricing/fleetDefaults.js +4 -9
- package/dist/pricing/fleetTierByAgentId.gen.d.ts +1 -1
- package/dist/pricing/fleetTierByAgentId.gen.js +2 -752
- package/dist/runtime/AgentMachine.d.ts +3 -0
- package/dist/runtime/AgentMachine.js +6 -1
- package/dist/runtime/PromptBuilder.js +2 -0
- package/dist/runtime/runTurn.d.ts +1 -0
- package/dist/runtime/runTurn.js +19 -15
- package/dist/runtime/wakeEffects.d.ts +35 -0
- package/dist/runtime/wakeEffects.js +121 -0
- package/package.json +2 -2
- package/dist/pricing/fleetFreeTierA.gen.d.ts +0 -6
- package/dist/pricing/fleetFreeTierA.gen.js +0 -11
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
import { ContextReadClient, InboxClient, } from '@ziggs-ai/api-client';
|
|
1
|
+
import { ContextReadClient, InboxClient, planInboxAck, planPartialInboxAck, } from '@ziggs-ai/api-client';
|
|
2
2
|
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
3
3
|
import { isRateLimited } from '@ziggs-ai/api-client';
|
|
4
4
|
const READ_PAGE = 100;
|
|
5
5
|
function log(label, msg) {
|
|
6
6
|
runtimeLog.info(label, msg);
|
|
7
7
|
}
|
|
8
|
+
function inboxClient(deps) {
|
|
9
|
+
return new InboxClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl, deps.creds.instanceId);
|
|
10
|
+
}
|
|
11
|
+
async function releaseInboxHostBestEffort(deps) {
|
|
12
|
+
try {
|
|
13
|
+
await inboxClient(deps).releaseHost();
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
warn(deps.label ?? 'inboxLoop', `inbox host release failed: ${err?.message ?? err}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
8
19
|
function warn(label, msg) {
|
|
9
20
|
runtimeLog.warn(label, msg);
|
|
10
21
|
}
|
|
@@ -25,45 +36,6 @@ export function chatsInWakeOrder(chats) {
|
|
|
25
36
|
function isPublishProbeChat(chatId) {
|
|
26
37
|
return chatId.startsWith('probe_');
|
|
27
38
|
}
|
|
28
|
-
/**
|
|
29
|
-
* Store publish waits on a chat reply, not inbox ack. The ordinary pass can
|
|
30
|
-
* already be inside leftover mail when that room appears, so a side read
|
|
31
|
-
* answers `probe_*` chats without waiting for the current pass to finish.
|
|
32
|
-
* It never acks — the main pass still owns the watermark.
|
|
33
|
-
*/
|
|
34
|
-
export async function answerPublishProbes(deps) {
|
|
35
|
-
const inbox = new InboxClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl);
|
|
36
|
-
const reader = new ContextReadClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl);
|
|
37
|
-
const envelope = await inbox.getInbox({});
|
|
38
|
-
const probes = (envelope.chats ?? []).filter((c) => isPublishProbeChat(c.chatId));
|
|
39
|
-
if (probes.length === 0)
|
|
40
|
-
return 0;
|
|
41
|
-
const mine = (d) => d.assigneeId === deps.ownAgentId;
|
|
42
|
-
const earliestByChat = new Map();
|
|
43
|
-
for (const d of envelope.deliveries) {
|
|
44
|
-
if (!d.chatId || !mine(d) || !isPublishProbeChat(d.chatId))
|
|
45
|
-
continue;
|
|
46
|
-
const seen = earliestByChat.get(d.chatId);
|
|
47
|
-
if (!seen || d.ts < seen)
|
|
48
|
-
earliestByChat.set(d.chatId, d.ts);
|
|
49
|
-
}
|
|
50
|
-
const attempt = async (what, handover) => {
|
|
51
|
-
try {
|
|
52
|
-
await handover();
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
catch (err) {
|
|
56
|
-
warn(deps.label ?? 'inboxLoop', `publish-probe ${what} was not handled: ${err?.message ?? err}`);
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
let delivered = 0;
|
|
61
|
-
for (const news of probes) {
|
|
62
|
-
const earliest = earliestByChat.get(news.chatId) ?? news.latestAt;
|
|
63
|
-
delivered += await deliverMessagesForChat(deps, news, earliest, reader, attempt);
|
|
64
|
-
}
|
|
65
|
-
return delivered;
|
|
66
|
-
}
|
|
67
39
|
/**
|
|
68
40
|
* Standing proposals share the merged inbox visibility model with deliveries,
|
|
69
41
|
* but older envelopes predate an explicit assignee stamp. Prefer the server's
|
|
@@ -78,6 +50,22 @@ function proposalIsMine(proposal, ownAgentId) {
|
|
|
78
50
|
return (proposal.pendingApprovalPartyIds?.includes(ownAgentId) === true ||
|
|
79
51
|
proposal.proposedTo === ownAgentId);
|
|
80
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* The one hire a chat's assigned deliveries name, or null when the room
|
|
55
|
+
* has none or more than one. Memory (and acting-for) treat "several
|
|
56
|
+
* customers in one room" as the shared section.
|
|
57
|
+
*/
|
|
58
|
+
export function soleHireAgreementIdForChat(deliveries, chatId, ownAgentId) {
|
|
59
|
+
const ids = new Set();
|
|
60
|
+
for (const d of deliveries) {
|
|
61
|
+
if (d.chatId !== chatId || !d.agreementId)
|
|
62
|
+
continue;
|
|
63
|
+
if (d.assigneeId && d.assigneeId !== ownAgentId)
|
|
64
|
+
continue;
|
|
65
|
+
ids.add(d.agreementId);
|
|
66
|
+
}
|
|
67
|
+
return ids.size === 1 ? [...ids][0] : null;
|
|
68
|
+
}
|
|
81
69
|
/**
|
|
82
70
|
* Read the messages behind one chat's deliveries and hand them to the host.
|
|
83
71
|
*
|
|
@@ -85,7 +73,7 @@ function proposalIsMine(proposal, ownAgentId) {
|
|
|
85
73
|
* reads through `/context/read`, where the agent's own grants fence what it is
|
|
86
74
|
* allowed to see. A delivery reference is an address, never an authorisation.
|
|
87
75
|
*/
|
|
88
|
-
async function deliverMessagesForChat(deps, news, earliestTs, reader, attempt) {
|
|
76
|
+
async function deliverMessagesForChat(deps, news, earliestTs, reader, attempt, hireAgreementId) {
|
|
89
77
|
let delivered = 0;
|
|
90
78
|
let remaining = news.count;
|
|
91
79
|
// `after` is exclusive, so step back a millisecond to include the oldest
|
|
@@ -135,6 +123,7 @@ async function deliverMessagesForChat(deps, news, earliestTs, reader, attempt) {
|
|
|
135
123
|
// leaving it made ClaudeBrain treat the wake as notice-only.
|
|
136
124
|
receiverId: deps.ownAgentId,
|
|
137
125
|
receiver: { id: deps.ownAgentId },
|
|
126
|
+
...(hireAgreementId ? { agreementId: hireAgreementId } : {}),
|
|
138
127
|
}));
|
|
139
128
|
if (handled)
|
|
140
129
|
delivered++;
|
|
@@ -155,7 +144,7 @@ async function deliverMessagesForChat(deps, news, earliestTs, reader, attempt) {
|
|
|
155
144
|
*/
|
|
156
145
|
export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
157
146
|
const label = deps.label ?? 'inboxLoop';
|
|
158
|
-
const inbox =
|
|
147
|
+
const inbox = inboxClient(deps);
|
|
159
148
|
const reader = new ContextReadClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl);
|
|
160
149
|
const envelope = await inbox.getInbox(waitSeconds > 0 ? { waitSeconds } : {});
|
|
161
150
|
let messagesDelivered = 0;
|
|
@@ -190,22 +179,72 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
190
179
|
// needs-human rows, granted ambient lanes) is context the host may read on
|
|
191
180
|
// demand — never a wake, never mine to ack as handled.
|
|
192
181
|
const mine = (d) => d.assigneeId === deps.ownAgentId;
|
|
182
|
+
// Give the mark back in steps instead of once at the end of the pass.
|
|
183
|
+
//
|
|
184
|
+
// A pass that acks only when it finishes freezes the watermark for its whole
|
|
185
|
+
// length, and the server hands an unanswered row to the person's stand-in
|
|
186
|
+
// after 90s. Measured, a single turn is ~19s but an uninterrupted stretch is
|
|
187
|
+
// ~86s median, so the pass — not the turn — is what crosses the window. Each
|
|
188
|
+
// row now carries a token for its own position; `planPartialInboxAck` says
|
|
189
|
+
// how far those tokens can safely be taken given what is finished.
|
|
190
|
+
//
|
|
191
|
+
// A mark is global, so it can only be released in timestamp order, while
|
|
192
|
+
// chats are worked newest-first. One chat in the pass — the ordinary case,
|
|
193
|
+
// and the one the window actually bites — releases as soon as it is done.
|
|
194
|
+
// Several, and the oldest is worked last, so the release waits for it: no
|
|
195
|
+
// worse than acking at the end, which is what this replaced.
|
|
196
|
+
const handledSoFar = new Set();
|
|
197
|
+
let lastAckedTo = null;
|
|
198
|
+
const ackWhatIsFinished = async () => {
|
|
199
|
+
const step = planPartialInboxAck(envelope, handledSoFar, {
|
|
200
|
+
ownAgentId: deps.ownAgentId,
|
|
201
|
+
failures,
|
|
202
|
+
});
|
|
203
|
+
if (!step || step.ackTo === lastAckedTo)
|
|
204
|
+
return;
|
|
205
|
+
try {
|
|
206
|
+
await inbox.ack(step.ackTo, { handledResourceIds: step.handledResourceIds });
|
|
207
|
+
lastAckedTo = step.ackTo;
|
|
208
|
+
log(label, `acked up to ${step.ackTo} (mid-pass)`);
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
// Never fatal: the end-of-pass ack is still coming, and the watermark is
|
|
212
|
+
// monotonic, so a lost step costs nothing but the exposure it would have
|
|
213
|
+
// closed.
|
|
214
|
+
warn(label, `mid-pass ack did not land: ${err?.message ?? err}`);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
193
217
|
// Oldest ASSIGNED delivery per chat: where to start reading that chat's
|
|
194
218
|
// messages. Ambient chat rows must not drag the read window earlier.
|
|
195
219
|
const earliestByChat = new Map();
|
|
220
|
+
const assignedRowsByChat = new Map();
|
|
196
221
|
for (const d of envelope.deliveries) {
|
|
197
222
|
if (!d.chatId || !mine(d))
|
|
198
223
|
continue;
|
|
199
224
|
const seen = earliestByChat.get(d.chatId);
|
|
200
225
|
if (!seen || d.ts < seen)
|
|
201
226
|
earliestByChat.set(d.chatId, d.ts);
|
|
227
|
+
const rows = assignedRowsByChat.get(d.chatId);
|
|
228
|
+
if (rows)
|
|
229
|
+
rows.push(d.resourceId);
|
|
230
|
+
else
|
|
231
|
+
assignedRowsByChat.set(d.chatId, [d.resourceId]);
|
|
202
232
|
}
|
|
203
233
|
for (const news of chatsInWakeOrder(envelope.chats ?? [])) {
|
|
204
234
|
const earliest = earliestByChat.get(news.chatId) ?? news.latestAt;
|
|
205
|
-
const
|
|
235
|
+
const failuresBefore = failures;
|
|
236
|
+
const n = await deliverMessagesForChat(deps, news, earliest, reader, attempt, soleHireAgreementIdForChat(envelope.deliveries, news.chatId, deps.ownAgentId));
|
|
206
237
|
messagesDelivered += n;
|
|
207
238
|
if (n > 0)
|
|
208
239
|
log(label, `chat ${news.chatId} delivered ${n} message(s)`);
|
|
240
|
+
// A fold that dropped a handover has not finished its rows, and the
|
|
241
|
+
// watermark must wait for the redelivery it is holding open.
|
|
242
|
+
if (failures === failuresBefore) {
|
|
243
|
+
for (const id of assignedRowsByChat.get(news.chatId) ?? []) {
|
|
244
|
+
handledSoFar.add(id);
|
|
245
|
+
}
|
|
246
|
+
await ackWhatIsFinished();
|
|
247
|
+
}
|
|
209
248
|
}
|
|
210
249
|
// Everything that is not chat text is handed over as a resource event, from
|
|
211
250
|
// the delivery itself — the reference already carries the ids the host needs,
|
|
@@ -220,8 +259,12 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
220
259
|
if (!mine(d))
|
|
221
260
|
continue;
|
|
222
261
|
const key = `delivery:${d.kind}:${d.resourceId}:${d.ts}`;
|
|
223
|
-
|
|
262
|
+
// Handled on an earlier pass — it still settles this instant, exactly as
|
|
263
|
+
// it does in the end-of-pass list.
|
|
264
|
+
if (deps.seen.has(key)) {
|
|
265
|
+
handledSoFar.add(d.resourceId);
|
|
224
266
|
continue;
|
|
267
|
+
}
|
|
225
268
|
deps.seen.mark(key);
|
|
226
269
|
const handled = await attempt(`${d.kind} ${d.resourceId}`, () => deps.onResourceEvent({
|
|
227
270
|
kind: d.kind,
|
|
@@ -236,9 +279,13 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
236
279
|
...(d.actorId ? { actorId: d.actorId } : {}),
|
|
237
280
|
change: 'created',
|
|
238
281
|
}), () => deps.seen.unmark(key));
|
|
239
|
-
if (handled)
|
|
282
|
+
if (handled) {
|
|
240
283
|
resourceEvents++;
|
|
284
|
+
handledSoFar.add(d.resourceId);
|
|
285
|
+
}
|
|
241
286
|
}
|
|
287
|
+
// An older row here may have been holding the mark back behind the chats.
|
|
288
|
+
await ackWhatIsFinished();
|
|
242
289
|
// the work channel. An open assigned task is redelivered on every
|
|
243
290
|
// pass until it closes (that IS the durable-wait contract), so dedup on
|
|
244
291
|
// taskId+updatedAt — a genuinely touched task wakes again, an untouched one
|
|
@@ -261,18 +308,23 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
261
308
|
// a miss must still count as handled so the watermark can advance with 0 tokens.
|
|
262
309
|
for (const request of envelope.openRequestsAwaitingMe ?? []) {
|
|
263
310
|
const key = `request:${request.agreementId}:${request.ts}`;
|
|
264
|
-
if (deps.seen.has(key))
|
|
311
|
+
if (deps.seen.has(key)) {
|
|
312
|
+
handledSoFar.add(request.agreementId);
|
|
265
313
|
continue;
|
|
314
|
+
}
|
|
266
315
|
deps.seen.mark(key);
|
|
267
316
|
if (deps.onRequest) {
|
|
268
317
|
const handled = await attempt(`request ${request.agreementId}`, () => deps.onRequest(request), () => deps.seen.unmark(key));
|
|
269
|
-
if (handled)
|
|
318
|
+
if (handled) {
|
|
270
319
|
requestsTriaged++;
|
|
320
|
+
handledSoFar.add(request.agreementId);
|
|
321
|
+
}
|
|
271
322
|
}
|
|
272
323
|
else {
|
|
273
324
|
// No handler → still count as handled (ack) so an old host cannot wedge
|
|
274
325
|
// forever on request mail it does not understand.
|
|
275
326
|
requestsTriaged++;
|
|
327
|
+
handledSoFar.add(request.agreementId);
|
|
276
328
|
}
|
|
277
329
|
}
|
|
278
330
|
if (envelope.truncatedRequests) {
|
|
@@ -308,22 +360,20 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
308
360
|
// the watermark so the server redelivers the window — unless `forceAck` says
|
|
309
361
|
// this window has failed too many times in a row and is now poison, in which
|
|
310
362
|
// case it is dropped loudly rather than wedging the agent forever.
|
|
363
|
+
// One function, shared with the MCP inbox tool — do not restate the list
|
|
364
|
+
// or the allow-gate here.
|
|
311
365
|
let acked = false;
|
|
312
|
-
|
|
366
|
+
const ack = planInboxAck(envelope, {
|
|
367
|
+
ownAgentId: deps.ownAgentId,
|
|
368
|
+
failures,
|
|
369
|
+
forceAck: opts.forceAck,
|
|
370
|
+
});
|
|
371
|
+
if (ack.allowed) {
|
|
313
372
|
if (failures > 0) {
|
|
314
373
|
runtimeLog.error(label, `acking a window that failed ${failures} handover(s) — giving up on redelivery to keep the ` +
|
|
315
374
|
`agent alive; this mail is lost (ackTo=${envelope.ackTo})`);
|
|
316
375
|
}
|
|
317
|
-
|
|
318
|
-
// handle. Ambient rows slide past the cursor unlisted — they were never
|
|
319
|
-
// this agent's to act on, and the owner's seen mark tracks them.
|
|
320
|
-
const handledResourceIds = [
|
|
321
|
-
...new Set([
|
|
322
|
-
...envelope.deliveries.filter(mine).map((d) => d.resourceId),
|
|
323
|
-
...(envelope.openRequestsAwaitingMe ?? []).map((q) => q.agreementId),
|
|
324
|
-
]),
|
|
325
|
-
].filter((id) => typeof id === 'string' && id.length > 0);
|
|
326
|
-
await inbox.ack(envelope.ackTo, { handledResourceIds });
|
|
376
|
+
await inbox.ack(envelope.ackTo, { handledResourceIds: ack.handledResourceIds });
|
|
327
377
|
acked = true;
|
|
328
378
|
log(label, `acked up to ${envelope.ackTo}`);
|
|
329
379
|
}
|
|
@@ -348,16 +398,6 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
348
398
|
* some defaults ship with. Lower it if your deployment sits behind a shorter one.
|
|
349
399
|
*/
|
|
350
400
|
const DEFAULT_WAIT_SECONDS = 110;
|
|
351
|
-
/**
|
|
352
|
-
* Side-poll interval for store-publish `probe_*` rooms.
|
|
353
|
-
*
|
|
354
|
-
* Default **off**. A second GET /inbox beside the main long-poll burned the
|
|
355
|
-
* shared poll ceiling: every hosted agent side-polled, probes got 429s, and
|
|
356
|
-
* Register saw ALB HTML 504s. Main pass + probe-first ordering + pinned
|
|
357
|
-
* `probe_*` deliveries are enough; set `probePollMs` only when you knowingly
|
|
358
|
-
* need the extra loop.
|
|
359
|
-
*/
|
|
360
|
-
const DEFAULT_PROBE_POLL_MS = 0;
|
|
361
401
|
const ERROR_BACKOFF_BASE_MS = 1_000;
|
|
362
402
|
const ERROR_BACKOFF_MAX_MS = 30_000;
|
|
363
403
|
/**
|
|
@@ -394,7 +434,6 @@ function sleep(ms) {
|
|
|
394
434
|
export function runInboxLoop(deps, opts = {}) {
|
|
395
435
|
const label = deps.label ?? 'inboxLoop';
|
|
396
436
|
const waitSeconds = opts.waitSeconds ?? DEFAULT_WAIT_SECONDS;
|
|
397
|
-
const probePollMs = opts.probePollMs ?? DEFAULT_PROBE_POLL_MS;
|
|
398
437
|
let running = true;
|
|
399
438
|
let consecutiveErrors = 0;
|
|
400
439
|
let heldPasses = 0;
|
|
@@ -440,32 +479,7 @@ export function runInboxLoop(deps, opts = {}) {
|
|
|
440
479
|
}
|
|
441
480
|
}
|
|
442
481
|
})();
|
|
443
|
-
const
|
|
444
|
-
if (probePollMs <= 0)
|
|
445
|
-
return;
|
|
446
|
-
while (running) {
|
|
447
|
-
const slice = 50;
|
|
448
|
-
let left = probePollMs;
|
|
449
|
-
while (left > 0 && running) {
|
|
450
|
-
const step = Math.min(slice, left);
|
|
451
|
-
await sleep(step);
|
|
452
|
-
left -= step;
|
|
453
|
-
}
|
|
454
|
-
if (!running)
|
|
455
|
-
break;
|
|
456
|
-
try {
|
|
457
|
-
const n = await answerPublishProbes(deps);
|
|
458
|
-
if (n > 0)
|
|
459
|
-
log(label, `publish-probe side poll delivered ${n} message(s)`);
|
|
460
|
-
}
|
|
461
|
-
catch (err) {
|
|
462
|
-
if (!running)
|
|
463
|
-
break;
|
|
464
|
-
warn(label, `publish-probe side poll failed: ${err?.message ?? err}`);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
})();
|
|
468
|
-
const done = Promise.all([main, probes]);
|
|
482
|
+
const done = main;
|
|
469
483
|
return {
|
|
470
484
|
get running() {
|
|
471
485
|
return running;
|
|
@@ -478,7 +492,10 @@ export function runInboxLoop(deps, opts = {}) {
|
|
|
478
492
|
},
|
|
479
493
|
async stop() {
|
|
480
494
|
running = false;
|
|
481
|
-
|
|
495
|
+
// Drop the lease immediately. Do not wait out a held poll — SIGTERM
|
|
496
|
+
// then process.exit cannot sit on the 110s wait.
|
|
497
|
+
await releaseInboxHostBestEffort(deps);
|
|
498
|
+
void done.catch(() => { });
|
|
482
499
|
},
|
|
483
500
|
};
|
|
484
501
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { AgentOptions } from '../../Agent.js';
|
|
2
2
|
import { PollRunner, type WakeTarget } from './PollRunner.js';
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Build the runner that executes this agent's declared wakes.
|
|
5
|
+
*
|
|
6
|
+
* A malformed `polls` value is a programming error and still throws. A
|
|
7
|
+
* binding that names an unknown kind or has no chatId is not: PollRunner
|
|
8
|
+
* warns and skips it, so one agent's invalid poll cannot refuse the host.
|
|
9
|
+
*/
|
|
4
10
|
export declare function createDeclaredPollRunner(options: AgentOptions, target: WakeTarget): PollRunner | null;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { PollRegistry } from './PollKind.js';
|
|
2
2
|
import { PollRunner } from './PollRunner.js';
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Build the runner that executes this agent's declared wakes.
|
|
5
|
+
*
|
|
6
|
+
* A malformed `polls` value is a programming error and still throws. A
|
|
7
|
+
* binding that names an unknown kind or has no chatId is not: PollRunner
|
|
8
|
+
* warns and skips it, so one agent's invalid poll cannot refuse the host.
|
|
9
|
+
*/
|
|
4
10
|
export function createDeclaredPollRunner(options, target) {
|
|
5
11
|
const registry = new PollRegistry();
|
|
6
12
|
for (const kind of options.pollKinds ?? [])
|
|
@@ -9,14 +15,6 @@ export function createDeclaredPollRunner(options, target) {
|
|
|
9
15
|
return null;
|
|
10
16
|
if (!Array.isArray(options.polls))
|
|
11
17
|
throw new Error('Agent: polls must be an array');
|
|
12
|
-
for (const binding of options.polls) {
|
|
13
|
-
if (!binding || !registry.get(binding.kind)) {
|
|
14
|
-
throw new Error(`Agent ${options.agentId}: unknown poll kind "${binding?.kind}"; declare its implementation in pollKinds`);
|
|
15
|
-
}
|
|
16
|
-
if (typeof binding.chatId !== 'string' || !binding.chatId.trim()) {
|
|
17
|
-
throw new Error(`Agent ${options.agentId}: poll "${binding.kind}" requires a chatId`);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
18
|
if (!options.polls.length)
|
|
21
19
|
return null;
|
|
22
20
|
return new PollRunner({
|
|
@@ -24,6 +24,7 @@ export { CONNECTION_TOOLS, connectionProxyTool, connectionListGrantsTool, reques
|
|
|
24
24
|
export { CONTEXT_TOOLS, contextReadTool, contextDelegateTool, contextExpandReachTool, contextDiscoverGrantableTool, } from './tier2/contextTools.js';
|
|
25
25
|
export { GRANTS_TOOLS, listGrantsTool } from './tier2/grantsTools.js';
|
|
26
26
|
export { TASK_TOOLS, listTasksTool } from './tier2/taskTools.js';
|
|
27
|
+
export { PLAN_TOOLS, taskReplacePlanTool } from './tier2/planTools.js';
|
|
27
28
|
export { LINK_TOOLS, linkProposeTool, linkListTool, } from './tier2/linkTools.js';
|
|
28
29
|
export { INTRODUCTION_TOOLS, introductionMintTool, introductionRedeemTool, introductionListTool, introductionRevokeTool, } from './tier2/introductionTools.js';
|
|
29
30
|
export { ARTIFACT_TOOLS, recordArtifactTool } from './tier2/artifactTools.js';
|
|
@@ -28,6 +28,7 @@ export { CONTEXT_TOOLS, contextReadTool, contextDelegateTool, contextExpandReach
|
|
|
28
28
|
// payment_list_grants). Always-available on the hosted agent — see CLAUDE_SDK_TOOLS.
|
|
29
29
|
export { GRANTS_TOOLS, listGrantsTool } from './tier2/grantsTools.js';
|
|
30
30
|
export { TASK_TOOLS, listTasksTool } from './tier2/taskTools.js';
|
|
31
|
+
export { PLAN_TOOLS, taskReplacePlanTool } from './tier2/planTools.js';
|
|
31
32
|
// links (agreements with engagementKind 'link') — cross-org reach.
|
|
32
33
|
///1022: the rail is two tools; propose/claim/revoke ride the
|
|
33
34
|
// agreement grammar.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ToolDefinition } from '../../../tools/defineTool.js';
|
|
2
2
|
export declare const contextReadTool: ToolDefinition;
|
|
3
|
+
export declare const openTool: ToolDefinition;
|
|
4
|
+
export declare const accessExplainTool: ToolDefinition;
|
|
3
5
|
export declare const contextDelegateTool: ToolDefinition;
|
|
4
6
|
export declare const contextExpandReachTool: ToolDefinition;
|
|
5
7
|
export declare const contextDiscoverGrantableTool: ToolDefinition;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { CONTEXT_CAPABILITIES, contextReadCapability, contextDelegateCapability, contextExpandReachCapability, contextDiscoverGrantableCapability, } from '@ziggs-ai/api-client';
|
|
1
|
+
import { CONTEXT_CAPABILITIES, contextReadCapability, openCapability, accessExplainCapability, contextDelegateCapability, contextExpandReachCapability, contextDiscoverGrantableCapability, } from '@ziggs-ai/api-client';
|
|
2
2
|
import { toolFromCapability, toolsFromCapabilities } from '../../../tools/capabilityAdapter.js';
|
|
3
3
|
// context grant tools ride the same api-client
|
|
4
4
|
// clients as ziggs-mcp; the whole definition (schema + handler) is now shared
|
|
5
5
|
// via the capability layer, so the two surfaces cannot fork.
|
|
6
6
|
export const contextReadTool = toolFromCapability(contextReadCapability);
|
|
7
|
+
export const openTool = toolFromCapability(openCapability);
|
|
8
|
+
export const accessExplainTool = toolFromCapability(accessExplainCapability);
|
|
7
9
|
export const contextDelegateTool = toolFromCapability(contextDelegateCapability);
|
|
8
10
|
export const contextExpandReachTool = toolFromCapability(contextExpandReachCapability);
|
|
9
11
|
export const contextDiscoverGrantableTool = toolFromCapability(contextDiscoverGrantableCapability);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ToolDefinition } from '../../../tools/defineTool.js';
|
|
2
|
+
/**
|
|
3
|
+
* `task_replace_plan` — write the checklist of a task you hold.
|
|
4
|
+
*
|
|
5
|
+
* The hosted tool set could only UPDATE a step that already existed
|
|
6
|
+
* (`task_update_plan_step` takes a stepId), and a task created without a plan
|
|
7
|
+
* has none, so an agent had no way to record anything about its own work on
|
|
8
|
+
* the task row. That mattered the moment one agent served two customers: from
|
|
9
|
+
* the second customer's room it could not find the approval request it had
|
|
10
|
+
* sent under the first customer's hire, and told them it had never been sent
|
|
11
|
+
* (store-opening lab, 2026-09-15). The MCP surface has had
|
|
12
|
+
* `ziggs_task_replace_plan` all along; this is the same client call for the
|
|
13
|
+
* hosted brain.
|
|
14
|
+
*
|
|
15
|
+
* The plan is the agent's own notebook on the task: step results can carry
|
|
16
|
+
* ids (an approval agreement, an artifact), and `task_list` reads the plan back
|
|
17
|
+
* from any room the agent is woken in.
|
|
18
|
+
*/
|
|
19
|
+
export declare const taskReplacePlanTool: ToolDefinition;
|
|
20
|
+
export declare const PLAN_TOOLS: ToolDefinition[];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { replaceTaskPlan } from '@ziggs-ai/api-client';
|
|
2
|
+
import { defineTool } from '../../../tools/defineTool.js';
|
|
3
|
+
import { credsFromToolContext } from '../../../tools/toolCreds.js';
|
|
4
|
+
/**
|
|
5
|
+
* `task_replace_plan` — write the checklist of a task you hold.
|
|
6
|
+
*
|
|
7
|
+
* The hosted tool set could only UPDATE a step that already existed
|
|
8
|
+
* (`task_update_plan_step` takes a stepId), and a task created without a plan
|
|
9
|
+
* has none, so an agent had no way to record anything about its own work on
|
|
10
|
+
* the task row. That mattered the moment one agent served two customers: from
|
|
11
|
+
* the second customer's room it could not find the approval request it had
|
|
12
|
+
* sent under the first customer's hire, and told them it had never been sent
|
|
13
|
+
* (store-opening lab, 2026-09-15). The MCP surface has had
|
|
14
|
+
* `ziggs_task_replace_plan` all along; this is the same client call for the
|
|
15
|
+
* hosted brain.
|
|
16
|
+
*
|
|
17
|
+
* The plan is the agent's own notebook on the task: step results can carry
|
|
18
|
+
* ids (an approval agreement, an artifact), and `task_list` reads the plan back
|
|
19
|
+
* from any room the agent is woken in.
|
|
20
|
+
*/
|
|
21
|
+
export const taskReplacePlanTool = defineTool('task_replace_plan', {
|
|
22
|
+
taskId: { type: 'string', required: true, minLength: 1, pattern: '\\S' },
|
|
23
|
+
steps: {
|
|
24
|
+
type: 'array',
|
|
25
|
+
required: true,
|
|
26
|
+
items: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
stepId: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'Stable id for this step. Omit to let the server assign step-<n>.',
|
|
32
|
+
},
|
|
33
|
+
description: { type: 'string', minLength: 1, pattern: '\\S' },
|
|
34
|
+
order: {
|
|
35
|
+
type: 'integer',
|
|
36
|
+
description: 'Position in the plan. Omit to use the array index.',
|
|
37
|
+
},
|
|
38
|
+
status: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
enum: ['pending', 'in_progress', 'completed', 'skipped'],
|
|
41
|
+
description: 'Step progress. Omit for pending.',
|
|
42
|
+
},
|
|
43
|
+
result: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
description: 'What this step produced, as an object — e.g. { "agreementId": "agr_…" } for an approval you requested, or { "artifactId": "art_…" } for a pack you recorded. Stored with the step and read back by task_list.',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
required: ['description'],
|
|
49
|
+
},
|
|
50
|
+
description: 'The FULL ordered step list. Existing steps are replaced wholesale, not appended to — read the current plan with task_list first and send it back with your changes.',
|
|
51
|
+
},
|
|
52
|
+
}, async (args, ctx) => {
|
|
53
|
+
const creds = credsFromToolContext(ctx, 'task_replace_plan');
|
|
54
|
+
const taskId = typeof args.taskId === 'string' ? args.taskId.trim() : '';
|
|
55
|
+
if (!taskId)
|
|
56
|
+
throw new Error('task_replace_plan: taskId is required');
|
|
57
|
+
const steps = args.steps;
|
|
58
|
+
if (!Array.isArray(steps))
|
|
59
|
+
throw new Error('task_replace_plan: steps must be an array');
|
|
60
|
+
return replaceTaskPlan(taskId, steps, creds);
|
|
61
|
+
}, {
|
|
62
|
+
description: 'Replace the plan of a task you hold with the full ordered step list. Use it to write your own checklist on a task that arrived without one, and to record what a step produced (an approval agreement id, an artifact id) so a later wake — in any room — can find it with task_list. Steps you do not send are removed; mark finished steps completed so closing the task does not rewrite them as skipped.',
|
|
63
|
+
});
|
|
64
|
+
export const PLAN_TOOLS = [taskReplacePlanTool];
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* Prices are in wallet cents (1 ZIG = 100 cents).
|
|
4
4
|
*/
|
|
5
5
|
import { FLEET_TIER_BY_AGENT_ID } from './fleetTierByAgentId.gen.js';
|
|
6
|
-
import { FREE_TIER_A_AGENT_IDS } from './fleetFreeTierA.gen.js';
|
|
7
6
|
import { isEvalFreeExecutor } from './fleetEvalFree.js';
|
|
8
7
|
export const ZIGGS_OPERATOR_CUT_BPS = 1000; // 10%
|
|
9
8
|
export const TIER_PRICE_CENTS = {
|
|
@@ -13,7 +12,6 @@ export const TIER_PRICE_CENTS = {
|
|
|
13
12
|
D: 100, // 1 ZIG / call (hand-written demo agents)
|
|
14
13
|
};
|
|
15
14
|
const PING_DESCRIPTION_PATTERN = /\b(ping|health\s*check|healthcheck|liveness|readiness|heartbeat)\b/i;
|
|
16
|
-
const TRIVIAL_LOOKUP_DESCRIPTION_PATTERN = /\b(what\s+time|current\s+time|local\s+time|world\s+clock|timezone|time\s+zone|weather|forecast|temperature|uv\s+index)\b/i;
|
|
17
15
|
export function resolveFleetTier(agentId) {
|
|
18
16
|
if (!agentId || typeof agentId !== 'string')
|
|
19
17
|
return null;
|
|
@@ -30,15 +28,12 @@ export function isFreeAllowlistedExecutor(executorId, description) {
|
|
|
30
28
|
return false;
|
|
31
29
|
if (isEvalFreeExecutor(executorId))
|
|
32
30
|
return true;
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
// A health check does not bill, whatever tier the executor is in. This is
|
|
32
|
+
// the only description-based exemption left: an agent whose work should cost
|
|
33
|
+
// nothing says so with a posted price of 0 on its own listing, where the
|
|
34
|
+
// buyer can see it, rather than through a rule inferred here.
|
|
35
35
|
if (description && PING_DESCRIPTION_PATTERN.test(description))
|
|
36
36
|
return true;
|
|
37
|
-
if (description &&
|
|
38
|
-
TRIVIAL_LOOKUP_DESCRIPTION_PATTERN.test(description) &&
|
|
39
|
-
FREE_TIER_A_AGENT_IDS.has(executorId)) {
|
|
40
|
-
return true;
|
|
41
|
-
}
|
|
42
37
|
return false;
|
|
43
38
|
}
|
|
44
39
|
export function resolveDefaultPriceCentsForExecutor(executorId, description) {
|