@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.
Files changed (54) hide show
  1. package/dist/Agent.d.ts +28 -7
  2. package/dist/Agent.js +177 -72
  3. package/dist/brain/ClaudeBrain.d.ts +11 -1
  4. package/dist/brain/ClaudeBrain.js +71 -18
  5. package/dist/brain/ZiggsBrain.d.ts +2 -0
  6. package/dist/brain/ZiggsBrain.js +1 -0
  7. package/dist/brain/claudeCognition.d.ts +14 -1
  8. package/dist/brain/claudeCognition.js +106 -6
  9. package/dist/brain/claudeSdkProtocol.d.ts +1 -12
  10. package/dist/brain/claudeSdkProtocol.js +4 -1
  11. package/dist/brain/claudeSdkTools.d.ts +3 -0
  12. package/dist/brain/claudeSdkTools.js +4 -2
  13. package/dist/brain/hireActivationNotice.d.ts +19 -0
  14. package/dist/brain/hireActivationNotice.js +79 -0
  15. package/dist/brain/standingOfferNotice.d.ts +15 -0
  16. package/dist/brain/standingOfferNotice.js +89 -0
  17. package/dist/inboundDiscoveryPolicy.d.ts +2 -0
  18. package/dist/inboundDiscoveryPolicy.js +2 -0
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/ingress/normalizeIncoming.d.ts +3 -0
  22. package/dist/ingress/normalizeIncoming.js +8 -0
  23. package/dist/mcp/createMcpAgent.d.ts +1 -1
  24. package/dist/mcp/createMcpAgent.js +1 -1
  25. package/dist/memory/MemoryStore.d.ts +18 -4
  26. package/dist/memory/MemoryStore.js +52 -12
  27. package/dist/platform/ConnectionPool.d.ts +9 -4
  28. package/dist/platform/ConnectionPool.js +16 -6
  29. package/dist/platform/StateTimers.d.ts +12 -6
  30. package/dist/platform/StateTimers.js +26 -6
  31. package/dist/platform/ZiggsEffectHandler.js +5 -1
  32. package/dist/platform/inboxLoop.d.ts +5 -7
  33. package/dist/platform/inboxLoop.js +113 -96
  34. package/dist/platform/polls/declaredPollRunner.d.ts +7 -1
  35. package/dist/platform/polls/declaredPollRunner.js +7 -9
  36. package/dist/platform/tools/index.d.ts +1 -0
  37. package/dist/platform/tools/index.js +1 -0
  38. package/dist/platform/tools/tier2/contextTools.d.ts +2 -0
  39. package/dist/platform/tools/tier2/contextTools.js +3 -1
  40. package/dist/platform/tools/tier2/planTools.d.ts +20 -0
  41. package/dist/platform/tools/tier2/planTools.js +64 -0
  42. package/dist/pricing/fleetDefaults.js +4 -9
  43. package/dist/pricing/fleetTierByAgentId.gen.d.ts +1 -1
  44. package/dist/pricing/fleetTierByAgentId.gen.js +2 -752
  45. package/dist/runtime/AgentMachine.d.ts +3 -0
  46. package/dist/runtime/AgentMachine.js +6 -1
  47. package/dist/runtime/PromptBuilder.js +2 -0
  48. package/dist/runtime/runTurn.d.ts +1 -0
  49. package/dist/runtime/runTurn.js +19 -15
  50. package/dist/runtime/wakeEffects.d.ts +35 -0
  51. package/dist/runtime/wakeEffects.js +121 -0
  52. package/package.json +2 -2
  53. package/dist/pricing/fleetFreeTierA.gen.d.ts +0 -6
  54. 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 = new InboxClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl);
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 n = await deliverMessagesForChat(deps, news, earliest, reader, attempt);
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
- if (deps.seen.has(key))
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
- if (envelope.ackTo && (failures === 0 || opts.forceAck)) {
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
- // Assigned rows only: the server's bury-guard checks what was MINE to
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 probes = (async () => {
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
- await done.catch(() => { });
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
- /** Validate before starting any work, so a declared wake source cannot vanish. */
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
- /** Validate before starting any work, so a declared wake source cannot vanish. */
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
- if (FREE_TIER_A_AGENT_IDS.has(executorId))
34
- return true;
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) {
@@ -4,4 +4,4 @@
4
4
  */
5
5
  export type FleetPricingTier = 'A' | 'B' | 'C' | 'D';
6
6
  export declare const FLEET_TIER_BY_AGENT_ID: Record<string, FleetPricingTier>;
7
- export declare const FLEET_TIER_MAP_SIZE = 767;
7
+ export declare const FLEET_TIER_MAP_SIZE = 17;