@ziggs-ai/agent-sdk 0.20.0 → 0.22.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 (61) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/Agent.d.ts +28 -7
  3. package/dist/Agent.js +189 -74
  4. package/dist/brain/ClaudeBrain.d.ts +11 -1
  5. package/dist/brain/ClaudeBrain.js +71 -18
  6. package/dist/brain/ZiggsBrain.d.ts +2 -0
  7. package/dist/brain/ZiggsBrain.js +1 -0
  8. package/dist/brain/claudeCognition.d.ts +1 -1
  9. package/dist/brain/claudeCognition.js +54 -7
  10. package/dist/brain/claudeSdkProtocol.d.ts +1 -1
  11. package/dist/brain/claudeSdkProtocol.js +5 -2
  12. package/dist/brain/claudeSdkTools.d.ts +5 -2
  13. package/dist/brain/claudeSdkTools.js +39 -10
  14. package/dist/brain/hireActivationNotice.d.ts +19 -0
  15. package/dist/brain/hireActivationNotice.js +79 -0
  16. package/dist/brain/standingOfferNotice.d.ts +15 -0
  17. package/dist/brain/standingOfferNotice.js +89 -0
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.js +1 -1
  20. package/dist/ingress/normalizeIncoming.d.ts +3 -0
  21. package/dist/ingress/normalizeIncoming.js +8 -0
  22. package/dist/mcp/createMcpAgent.d.ts +1 -1
  23. package/dist/mcp/createMcpAgent.js +1 -1
  24. package/dist/memory/MemoryStore.d.ts +18 -4
  25. package/dist/memory/MemoryStore.js +52 -12
  26. package/dist/platform/ConnectionPool.d.ts +9 -4
  27. package/dist/platform/ConnectionPool.js +16 -6
  28. package/dist/platform/StateTimers.d.ts +12 -6
  29. package/dist/platform/StateTimers.js +26 -6
  30. package/dist/platform/ZiggsEffectHandler.js +17 -3
  31. package/dist/platform/inboxLoop.d.ts +5 -7
  32. package/dist/platform/inboxLoop.js +36 -80
  33. package/dist/platform/polls/declaredPollRunner.d.ts +7 -1
  34. package/dist/platform/polls/declaredPollRunner.js +7 -9
  35. package/dist/platform/tasks/TaskService.d.ts +23 -1
  36. package/dist/platform/tasks/TaskService.js +8 -2
  37. package/dist/platform/tools/index.d.ts +3 -2
  38. package/dist/platform/tools/index.js +3 -2
  39. package/dist/platform/tools/tier1/protocolRunner.js +8 -1
  40. package/dist/platform/tools/tier1/protocolTools.js +3 -3
  41. package/dist/platform/tools/tier2/contextTools.d.ts +3 -0
  42. package/dist/platform/tools/tier2/contextTools.js +4 -1
  43. package/dist/platform/tools/tier2/planTools.d.ts +20 -0
  44. package/dist/platform/tools/tier2/planTools.js +64 -0
  45. package/dist/platform/tools/tier2/taskTools.d.ts +1 -0
  46. package/dist/platform/tools/tier2/taskTools.js +2 -1
  47. package/dist/pricing/fleetDefaults.js +4 -9
  48. package/dist/pricing/fleetTierByAgentId.gen.d.ts +1 -1
  49. package/dist/pricing/fleetTierByAgentId.gen.js +2 -752
  50. package/dist/runtime/AgentMachine.d.ts +3 -0
  51. package/dist/runtime/AgentMachine.js +6 -1
  52. package/dist/runtime/runTurn.d.ts +1 -0
  53. package/dist/runtime/runTurn.js +19 -15
  54. package/dist/runtime/wakeEffects.d.ts +35 -0
  55. package/dist/runtime/wakeEffects.js +121 -0
  56. package/dist/runtime/wakeOutcome.d.ts +33 -0
  57. package/dist/runtime/wakeOutcome.js +61 -6
  58. package/dist/types.d.ts +13 -0
  59. package/package.json +2 -2
  60. package/dist/pricing/fleetFreeTierA.gen.d.ts +0 -6
  61. package/dist/pricing/fleetFreeTierA.gen.js +0 -11
@@ -5,6 +5,17 @@ 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;
@@ -244,7 +233,7 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
244
233
  for (const news of chatsInWakeOrder(envelope.chats ?? [])) {
245
234
  const earliest = earliestByChat.get(news.chatId) ?? news.latestAt;
246
235
  const failuresBefore = failures;
247
- const n = await deliverMessagesForChat(deps, news, earliest, reader, attempt);
236
+ const n = await deliverMessagesForChat(deps, news, earliest, reader, attempt, soleHireAgreementIdForChat(envelope.deliveries, news.chatId, deps.ownAgentId));
248
237
  messagesDelivered += n;
249
238
  if (n > 0)
250
239
  log(label, `chat ${news.chatId} delivered ${n} message(s)`);
@@ -409,16 +398,6 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
409
398
  * some defaults ship with. Lower it if your deployment sits behind a shorter one.
410
399
  */
411
400
  const DEFAULT_WAIT_SECONDS = 110;
412
- /**
413
- * Side-poll interval for store-publish `probe_*` rooms.
414
- *
415
- * Default **off**. A second GET /inbox beside the main long-poll burned the
416
- * shared poll ceiling: every hosted agent side-polled, probes got 429s, and
417
- * Register saw ALB HTML 504s. Main pass + probe-first ordering + pinned
418
- * `probe_*` deliveries are enough; set `probePollMs` only when you knowingly
419
- * need the extra loop.
420
- */
421
- const DEFAULT_PROBE_POLL_MS = 0;
422
401
  const ERROR_BACKOFF_BASE_MS = 1_000;
423
402
  const ERROR_BACKOFF_MAX_MS = 30_000;
424
403
  /**
@@ -455,7 +434,6 @@ function sleep(ms) {
455
434
  export function runInboxLoop(deps, opts = {}) {
456
435
  const label = deps.label ?? 'inboxLoop';
457
436
  const waitSeconds = opts.waitSeconds ?? DEFAULT_WAIT_SECONDS;
458
- const probePollMs = opts.probePollMs ?? DEFAULT_PROBE_POLL_MS;
459
437
  let running = true;
460
438
  let consecutiveErrors = 0;
461
439
  let heldPasses = 0;
@@ -501,32 +479,7 @@ export function runInboxLoop(deps, opts = {}) {
501
479
  }
502
480
  }
503
481
  })();
504
- const probes = (async () => {
505
- if (probePollMs <= 0)
506
- return;
507
- while (running) {
508
- const slice = 50;
509
- let left = probePollMs;
510
- while (left > 0 && running) {
511
- const step = Math.min(slice, left);
512
- await sleep(step);
513
- left -= step;
514
- }
515
- if (!running)
516
- break;
517
- try {
518
- const n = await answerPublishProbes(deps);
519
- if (n > 0)
520
- log(label, `publish-probe side poll delivered ${n} message(s)`);
521
- }
522
- catch (err) {
523
- if (!running)
524
- break;
525
- warn(label, `publish-probe side poll failed: ${err?.message ?? err}`);
526
- }
527
- }
528
- })();
529
- const done = Promise.all([main, probes]);
482
+ const done = main;
530
483
  return {
531
484
  get running() {
532
485
  return running;
@@ -539,7 +492,10 @@ export function runInboxLoop(deps, opts = {}) {
539
492
  },
540
493
  async stop() {
541
494
  running = false;
542
- 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(() => { });
543
499
  },
544
500
  };
545
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({
@@ -8,7 +8,29 @@ export declare class TaskService {
8
8
  getActiveTasksForChat(chatId: string): Promise<Task[]>;
9
9
  getSubtasks(parentTaskId: string): Promise<Task[]>;
10
10
  spawnUnderAgreement(agreementId: string, taskData: Omit<CreateTaskData, 'agreementId'>): Promise<Task>;
11
- updateState(taskId: string, state: TaskState, result?: unknown, idempotencyKey?: string): Promise<import("@ziggs-ai/api-client").TaskWriteConfirm>;
11
+ updateState(taskId: string, state: TaskState, result?: unknown, idempotencyKey?: string): Promise<{
12
+ ok: true;
13
+ taskId: string;
14
+ state: string;
15
+ outcome: import("@ziggs-ai/api-client").NextCallOutcome;
16
+ recorded: string;
17
+ handoff?: string;
18
+ delivery?: string;
19
+ metering?: string;
20
+ agreement?: string;
21
+ next: import("@ziggs-ai/api-client").NextCall[];
22
+ workContext?: import("@ziggs-ai/api-client").WorkContext;
23
+ agreementId?: string;
24
+ updatedAt?: string;
25
+ status?: string;
26
+ stepCount?: number;
27
+ structureChanged?: boolean;
28
+ stepId?: string;
29
+ stepStatus?: string;
30
+ patchedCount?: number;
31
+ resultRecorded?: boolean;
32
+ terminalState?: string;
33
+ }>;
12
34
  updatePlanStep(taskId: string, stepId: string, status: string, result?: unknown): Promise<import("@ziggs-ai/api-client").TaskWriteConfirm>;
13
35
  cancel(taskId: string): Promise<import("@ziggs-ai/api-client").TaskWriteConfirm>;
14
36
  report(taskId: string, message?: string): Promise<import("@ziggs-ai/api-client").TaskWriteConfirm>;
@@ -1,4 +1,4 @@
1
- import { createTask, getTask, getActiveTasksForAgent, getActiveTasksForChat, getSubtasks, updateTaskState, updateTaskPlanStep, cancelTask, reportTask, setTaskSatisfaction, countTasks, } from '@ziggs-ai/api-client';
1
+ import { createTask, getTask, getActiveTasksForAgent, getActiveTasksForChat, getSubtasks, updateTaskState, updateTaskPlanStep, cancelTask, reportTask, setTaskSatisfaction, countTasks, presentTaskOutcome, } from '@ziggs-ai/api-client';
2
2
  import { runtimeLog } from '../../shared/runtimeLog.js';
3
3
  function safeJsonStringify(value) {
4
4
  if (value === null || value === undefined)
@@ -65,7 +65,13 @@ export class TaskService {
65
65
  };
66
66
  const updated = await updateTaskState(taskId, state, data, this.creds);
67
67
  runtimeLog.info('TaskService', `Task state: ${taskId} → ${state}`);
68
- return updated;
68
+ // The same four facts the MCP surface reports, from the same
69
+ // presenter: what was stored, who was woken, what metering did, what the
70
+ // agreement is now. Both surfaces say it once, and say it the same way.
71
+ return {
72
+ ...updated,
73
+ ...presentTaskOutcome(updated, { creds: this.creds, surface: 'sdk' }),
74
+ };
69
75
  }
70
76
  catch (error) {
71
77
  runtimeLog.error('TaskService', `Task state update failed: ${error.message}`);
@@ -21,9 +21,10 @@ export { DISCOVERY_TOOLS, agentSearchTool, agentGetTool } from './tier2/discover
21
21
  export { MARKETPLACE_TOOLS, marketplaceViewTool } from './tier2/marketplaceTools.js';
22
22
  export { PAYMENT_TOOLS, paymentBalanceTool } from './tier2/paymentTools.js';
23
23
  export { CONNECTION_TOOLS, connectionProxyTool, connectionListGrantsTool, requestConnectionTool, } from './tier2/connectionTools.js';
24
- export { CONTEXT_TOOLS, contextReadTool, contextDelegateTool, contextExpandReachTool, contextDiscoverGrantableTool, } from './tier2/contextTools.js';
24
+ export { CONTEXT_TOOLS, contextReadTool, contextRequestTool, contextDelegateTool, contextExpandReachTool, contextDiscoverGrantableTool, } from './tier2/contextTools.js';
25
25
  export { GRANTS_TOOLS, listGrantsTool } from './tier2/grantsTools.js';
26
- export { TASK_TOOLS, listTasksTool } from './tier2/taskTools.js';
26
+ export { TASK_TOOLS, listTasksTool, cancelTaskTool } 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';
@@ -23,11 +23,12 @@ export { DISCOVERY_TOOLS, agentSearchTool, agentGetTool } from './tier2/discover
23
23
  export { MARKETPLACE_TOOLS, marketplaceViewTool } from './tier2/marketplaceTools.js';
24
24
  export { PAYMENT_TOOLS, paymentBalanceTool } from './tier2/paymentTools.js';
25
25
  export { CONNECTION_TOOLS, connectionProxyTool, connectionListGrantsTool, requestConnectionTool, } from './tier2/connectionTools.js';
26
- export { CONTEXT_TOOLS, contextReadTool, contextDelegateTool, contextExpandReachTool, contextDiscoverGrantableTool, } from './tier2/contextTools.js';
26
+ export { CONTEXT_TOOLS, contextReadTool, contextRequestTool, contextDelegateTool, contextExpandReachTool, contextDiscoverGrantableTool, } from './tier2/contextTools.js';
27
27
  // one grant lister across every rail (retires context_discover +
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
- export { TASK_TOOLS, listTasksTool } from './tier2/taskTools.js';
30
+ export { TASK_TOOLS, listTasksTool, cancelTaskTool } 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.
@@ -57,7 +57,7 @@ cachedAgreements) {
57
57
  case 'agreement-propose':
58
58
  return proposeAgreement(services, payload, chatId, agents, actorId, writeKeys);
59
59
  case 'agreement-subcontract':
60
- return subcontractAgreement(services, payload, chatId, agents, actorId, writeKeys, cachedAgreements);
60
+ return subcontractAgreement(services, payload, namedSubcontractChat(payload, chatId), agents, actorId, writeKeys, cachedAgreements);
61
61
  case 'agreement-respond':
62
62
  if (!payload.agreementId || !payload.action)
63
63
  return null;
@@ -323,6 +323,13 @@ function agActive(a) {
323
323
  function agIsActiveStatus(a) {
324
324
  return String(a.status ?? '') === 'active';
325
325
  }
326
+ /** A named room on the call wins; otherwise the wake's chat. Empty is omit. */
327
+ function namedSubcontractChat(payload, wakeChatId) {
328
+ const named = payload.chatId;
329
+ if (typeof named === 'string' && named.trim())
330
+ return named.trim();
331
+ return wakeChatId;
332
+ }
326
333
  async function subcontractAgreement(services, payload, chatId, _agents, actingAgentId, writeKeys = null, cachedAgreements) {
327
334
  if (!payload.description || !payload.executorId)
328
335
  return null;
@@ -1,7 +1,7 @@
1
1
  import { defineTool } from '../../../tools/defineTool.js';
2
2
  import { requireProtocolOperation } from '../../../tasks/protocolRegistry.js';
3
3
  import { dispatchProtocolOp } from './protocolRunner.js';
4
- import { AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION, agreementClaimCapability, } from '@ziggs-ai/api-client';
4
+ import { AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION, agreementClaimCapability, agreementSubcontractCapability, } from '@ziggs-ai/api-client';
5
5
  /** keep these enums in lockstep with MCP ziggs_agreement_* / ziggs_task_create. */
6
6
  const LIFECYCLE_ENUM = ['open', 'time-bound', 'count-bound'];
7
7
  const PLAN_REVIEW_TIMING_ENUM = ['with_proposal', 'before_execution'];
@@ -61,7 +61,7 @@ function handlerFor(toolName) {
61
61
  };
62
62
  }
63
63
  export const agreementProposeTool = defineTool('agreement_propose', { description: { type: 'string', required: true }, proposedTo: { type: 'string', required: true }, engagementKind: { type: 'string', enum: ['hire', 'service'], description: 'Contract type. Defaults to service. For a bilateral trust link, use link_propose.' }, parentAgreementId: 'string', providerId: { type: 'string', description: AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION }, price: { type: 'number', description: 'Price in POINTS, as an integer of hundredths — 500 means ϟ5.00. Convert both ways: a human asking for "ϟ2 per task" means 200, not 2. ϟ is the points symbol, not a hundredth.' }, lifecycle: lifecycleParam, expiresAt: 'string', maxExecutions: 'integer', billing: { type: 'string', enum: ['total', 'per_task'], description: "How price reads: 'total' (default, escrowed and paid at the end) or 'per_task' (a rate settled as each task completes — standing agreements only, and the default for a hire)." }, agreementDescription: 'string' }, handlerFor('agreement_propose'), { description: 'Propose an agreement — direct, broadcast, or link; there are no separate publish tools. Engagement precedence (posted-first): 1) REUSE an active agreement on matching terms, 2) CLAIM the counterparty\'s listing (marketplace_view → agreement_claim; never counter a listing), 3) POST a request broadcast here when nothing listed fits, 4) direct-propose ONLY for bespoke terms, renegotiation, or commissioning a named counterparty with no listing — most published agents are claim-only and refuse direct proposals with a pointer at their listing. Direction is stated, never guessed: providerId = your own id ("I work, you pay") or providerId = proposedTo ("you work, I pay") on direct proposals. Broadcast with proposedTo="everyone" (fully public) or "org" (your org only): no providerId publishes a REQUEST any agent may claim; providerId = your own id publishes a STANDING OFFER (claiming is agreement_claim; browsing is marketplace_view). For a bilateral trust link, use link_propose: it takes an email or an agent id, or neither for a share link. engagementKind defaults to service. Agreements are STANDING by default (no expiry, unlimited tasks under one agreement); set expiresAt or maxExecutions only when the engagement should end on its own.', isAgreementCreation: true });
64
- export const agreementSubcontractTool = defineTool('agreement_subcontract', { description: { type: 'string', required: true }, executorId: { type: 'string', required: true }, parentAgreementId: { type: 'string', required: true }, payerId: 'string', price: { type: 'number', description: 'Price in POINTS, as an integer of hundredths — 500 means ϟ5.00. Convert both ways: a human asking for "ϟ2 per task" means 200, not 2. ϟ is the points symbol, not a hundredth.' }, lifecycle: lifecycleParam, expiresAt: 'string', maxExecutions: 'integer', agreementDescription: 'string' }, handlerFor('agreement_subcontract'), { description: 'Delegate work to another agent under an existing parent agreement. Requires parentAgreementId.', isAgreementCreation: true });
64
+ export const agreementSubcontractTool = defineTool('agreement_subcontract', agreementSubcontractCapability.params, handlerFor('agreement_subcontract'), { description: agreementSubcontractCapability.descriptions.sdk, isAgreementCreation: true });
65
65
  export const agreementRespondTool = defineTool('agreement_respond', { agreementId: { type: 'string', required: true }, action: { type: 'string', required: true, enum: ['approve', 'reject'] } }, handlerFor('agreement_respond'), { description: 'Approve or reject a pending DIRECT agreement proposal addressed to you. Open broadcasts (requests, standing offers, link invites) have no personal approval slot — claim those with agreement_claim instead, or ignore them to pass. ONE exception: a hand-off that pins YOU as provider carries your pending approval even as an open broadcast — approving consents to serving whoever claims it (the row stays open for claims); rejecting cancels the hand-off. Ignoring it leaves the hand-off unclaimable, so always respond.' });
66
66
  export const agreementClaimTool = defineTool('agreement_claim', agreementClaimCapability.params, handlerFor('agreement_claim'), { description: agreementClaimCapability.descriptions.sdk });
67
67
  export const taskSetResultTool = defineTool('task_set_result', {
@@ -73,7 +73,7 @@ export const taskSetResultTool = defineTool('task_set_result', {
73
73
  type: 'object',
74
74
  description: 'Outcome payload (summary, status, links, …). This is what the next agent reads when it picks the work up from its own inbox.',
75
75
  },
76
- }, handlerFor('task_set_result'), { description: 'Transition a task to its terminal state — completed, failed, or cancelled — and record the result. Set the result here as an object ({ summary, status, links }): a task the parties opted into is closed by this call, whatever else you also sent them. Pass state (not status).' });
76
+ }, handlerFor('task_set_result'), { description: 'Transition a task to its terminal state — completed, failed, or cancelled — and record the result. Set the result here as an object ({ summary, status, links }): a task the parties opted into is closed by this call, whatever else you also sent them. Pass state (not status). This closes the task and not the engagement: a standing hire stays active for the next task, ending one is agreement_fulfill. The reply reports what actually happened — what was stored, who the completion was queued for (queued, not read), what per-task metering did — so report those and no more; a completed task is not by itself a paid one.' });
77
77
  export const agreementCounterTool = defineTool('agreement_counter', { agreementId: { type: 'string', required: true }, price: { type: 'number', description: 'Price in POINTS, as an integer of hundredths — 500 means ϟ5.00. Convert both ways: a human asking for "ϟ2 per task" means 200, not 2. ϟ is the points symbol, not a hundredth.' }, agreementDescription: 'string', expiresAt: 'string', lifecycle: lifecycleParam, maxExecutions: 'integer', description: 'string' }, handlerFor('agreement_counter'), { description: 'Counter a pending proposal with revised terms instead of approving or rejecting. Check status first with agreement_get.' });
78
78
  export const agreementGetTool = defineTool('agreement_get', { agreementId: { type: 'string', required: true } }, handlerFor('agreement_get'), { description: 'Read the current proposal status for an agreement (pending, approved, rejected, countered, expired).' });
79
79
  export const agreementRevokeTool = defineTool('agreement_revoke', { agreementId: { type: 'string', required: true } }, handlerFor('agreement_revoke'), { description: 'Revoke a pending proposal you created so it can no longer be approved. Use when the user has clearly moved on to a different request before responding to the previous proposal card.' });
@@ -1,5 +1,8 @@
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;
5
+ export declare const contextRequestTool: ToolDefinition;
3
6
  export declare const contextDelegateTool: ToolDefinition;
4
7
  export declare const contextExpandReachTool: ToolDefinition;
5
8
  export declare const contextDiscoverGrantableTool: ToolDefinition;
@@ -1,9 +1,12 @@
1
- import { CONTEXT_CAPABILITIES, contextReadCapability, contextDelegateCapability, contextExpandReachCapability, contextDiscoverGrantableCapability, } from '@ziggs-ai/api-client';
1
+ import { CONTEXT_CAPABILITIES, contextReadCapability, openCapability, accessExplainCapability, contextDelegateCapability, contextExpandReachCapability, contextDiscoverGrantableCapability, contextRequestCapability, } 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);
9
+ export const contextRequestTool = toolFromCapability(contextRequestCapability);
7
10
  export const contextDelegateTool = toolFromCapability(contextDelegateCapability);
8
11
  export const contextExpandReachTool = toolFromCapability(contextExpandReachCapability);
9
12
  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];
@@ -1,3 +1,4 @@
1
1
  import type { ToolDefinition } from '../../../tools/defineTool.js';
2
2
  export declare const listTasksTool: ToolDefinition;
3
+ export declare const cancelTaskTool: ToolDefinition;
3
4
  export declare const TASK_TOOLS: ToolDefinition[];
@@ -1,6 +1,7 @@
1
- import { listTasksCapability, TASK_CAPABILITIES } from '@ziggs-ai/api-client';
1
+ import { cancelTaskCapability, listTasksCapability, TASK_CAPABILITIES } from '@ziggs-ai/api-client';
2
2
  import { toolFromCapability, toolsFromCapabilities } from '../../../tools/capabilityAdapter.js';
3
3
  // GET /tasks, defined once in api-client. MCP ziggs_task_list and this
4
4
  // hosted-Claude tool used to re-declare the same filters.
5
5
  export const listTasksTool = toolFromCapability(listTasksCapability);
6
+ export const cancelTaskTool = toolFromCapability(cancelTaskCapability);
6
7
  export const TASK_TOOLS = toolsFromCapabilities(TASK_CAPABILITIES);
@@ -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;