@ziggs-ai/agent-sdk 0.11.1 → 0.13.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 (52) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/AgentHost.d.ts +27 -5
  3. package/dist/AgentHost.js +110 -15
  4. package/dist/agent/ClaudeSdkAgent.js +2 -2
  5. package/dist/agent/claudeSdkProtocol.d.ts +1 -1
  6. package/dist/agent/claudeSdkProtocol.js +3 -2
  7. package/dist/agent/claudeSdkTools.js +46 -10
  8. package/dist/cognition/validateContext.js +4 -1
  9. package/dist/context/batch.d.ts +3 -0
  10. package/dist/context/batch.js +8 -5
  11. package/dist/context/classifyEnvelope.js +4 -5
  12. package/dist/context/routingLabels.js +2 -3
  13. package/dist/formatters/AgreementFormatter.d.ts +7 -1
  14. package/dist/formatters/AgreementFormatter.js +17 -8
  15. package/dist/formatters/HistoryFormatter.d.ts +5 -0
  16. package/dist/formatters/HistoryFormatter.js +16 -7
  17. package/dist/index.d.ts +4 -2
  18. package/dist/index.js +8 -1
  19. package/dist/ingress/normalizeIncoming.d.ts +3 -8
  20. package/dist/runtime/PromptBuilder.js +32 -22
  21. package/dist/runtime/buildOutcome.js +4 -4
  22. package/dist/runtime/mandateDeclaration.d.ts +9 -0
  23. package/dist/runtime/mandateDeclaration.js +62 -0
  24. package/dist/runtime/runTurn.js +8 -0
  25. package/dist/server/ConnectionPool.d.ts +19 -0
  26. package/dist/server/ConnectionPool.js +30 -0
  27. package/dist/server/OutboxBuffer.d.ts +1 -1
  28. package/dist/server/ZiggsEffectHandler.d.ts +1 -1
  29. package/dist/server/ZiggsEffectHandler.js +6 -2
  30. package/dist/server/agreements/AgreementService.d.ts +19 -10
  31. package/dist/server/agreements/AgreementService.js +22 -11
  32. package/dist/server/inboxLoop.d.ts +6 -6
  33. package/dist/server/inboxLoop.js +31 -18
  34. package/dist/server/proactive/ProactiveTrigger.d.ts +7 -1
  35. package/dist/server/proactive/ProactiveTrigger.js +2 -2
  36. package/dist/server/requestMatch.d.ts +5 -0
  37. package/dist/server/{questMatch.js → requestMatch.js} +2 -2
  38. package/dist/server/runLauncher.js +24 -3
  39. package/dist/server/tools/index.d.ts +2 -2
  40. package/dist/server/tools/index.js +2 -2
  41. package/dist/server/tools/tier1/protocolRunner.js +16 -10
  42. package/dist/server/tools/tier1/protocolTools.js +12 -7
  43. package/dist/server/tools/tier2/linkTools.d.ts +1 -1
  44. package/dist/server/tools/tier2/linkTools.js +11 -9
  45. package/dist/server/tools/tier2/marketplaceTools.js +2 -2
  46. package/dist/server/tools/tier2/mcpConnectionTools.d.ts +33 -0
  47. package/dist/server/tools/tier2/mcpConnectionTools.js +138 -6
  48. package/dist/server/tools/tier2/paymentTools.d.ts +0 -8
  49. package/dist/server/tools/tier2/paymentTools.js +8 -12
  50. package/dist/types.d.ts +25 -10
  51. package/package.json +2 -2
  52. package/dist/server/questMatch.d.ts +0 -5
@@ -1,4 +1,4 @@
1
- import { type InboxQuestRef, type InboxTaskRef, type Creds } from '@ziggs-ai/api-client';
1
+ import { type InboxRequestRef, type InboxTaskRef, type Creds } from '@ziggs-ai/api-client';
2
2
  import { SeenMessages } from './SeenMessages.js';
3
3
  /**
4
4
  * Per-agent inbox long-poll loop (delivery law, phase 4). This is the ONLY way
@@ -27,11 +27,11 @@ export interface InboxLoopDeps {
27
27
  */
28
28
  onTask?: (task: InboxTaskRef) => Promise<void>;
29
29
  /**
30
- * A marketplace quest doorbell. The host MUST triage with a plain
30
+ * A marketplace request doorbell. The host MUST triage with a plain
31
31
  * string compare and return without an LLM wake on miss — otherwise every
32
- * public quest burns a full Claude turn per published supplier.
32
+ * public request burns a full Claude turn per published supplier.
33
33
  */
34
- onQuest?: (quest: InboxQuestRef) => Promise<void>;
34
+ onRequest?: (request: InboxRequestRef) => Promise<void>;
35
35
  label?: string;
36
36
  }
37
37
  export interface InboxPassResult {
@@ -41,8 +41,8 @@ export interface InboxPassResult {
41
41
  resourceEvents: number;
42
42
  /** Assigned open tasks delivered this pass. */
43
43
  tasksDelivered: number;
44
- /** Quest refs triaged this pass — includes misses that only ack. */
45
- questsTriaged: number;
44
+ /** Request refs triaged this pass — includes misses that only ack. */
45
+ requestsTriaged: number;
46
46
  /** True when the watermark advanced this pass. */
47
47
  acked: boolean;
48
48
  /** True when there was more mail than one envelope carries. */
@@ -83,7 +83,7 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
83
83
  let messagesDelivered = 0;
84
84
  let resourceEvents = 0;
85
85
  let tasksDelivered = 0;
86
- let questsTriaged = 0;
86
+ let requestsTriaged = 0;
87
87
  let failures = 0;
88
88
  // a handover that failed must not be acked, and must not be
89
89
  // remembered as seen — otherwise the redelivery the held watermark buys is
@@ -107,10 +107,16 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
107
107
  // make a badly backed-up agent replay its whole backlog forever.
108
108
  warn(label, 'inbox deliveriesCapped (more mail than one envelope carries — the tail follows on the next pass)');
109
109
  }
110
- // Oldest delivery per chat: where to start reading that chat's messages.
110
+ // The single-assign law: a row is MINE to act on iff it carries my stamp.
111
+ // Everything else in the merged view (a sibling carrier's mail, the owner's
112
+ // needs-human rows, granted ambient lanes) is context the host may read on
113
+ // demand — never a wake, never mine to ack as handled.
114
+ const mine = (d) => d.assigneeId === deps.ownAgentId;
115
+ // Oldest ASSIGNED delivery per chat: where to start reading that chat's
116
+ // messages. Ambient chat rows must not drag the read window earlier.
111
117
  const earliestByChat = new Map();
112
118
  for (const d of envelope.deliveries) {
113
- if (!d.chatId)
119
+ if (!d.chatId || !mine(d))
114
120
  continue;
115
121
  const seen = earliestByChat.get(d.chatId);
116
122
  if (!seen || d.ts < seen)
@@ -126,10 +132,14 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
126
132
  // Everything that is not chat text is handed over as a resource event, from
127
133
  // the delivery itself — the reference already carries the ids the host needs,
128
134
  // so nothing has to be re-derived to name what changed.
129
- // Quests must never take this path — even if a pre-cut server
135
+ // Requests must never take this path — even if a pre-cut server
130
136
  // left them in `deliveries`, skip so they cannot wake the LLM before triage.
137
+ // Unassigned rows never take it either: waking the brain on a sibling's
138
+ // mail is the N-wakes-per-event cost the party-owned inbox exists to kill.
131
139
  for (const d of envelope.deliveries) {
132
- if (d.kind === 'message' || d.kind === 'quest')
140
+ if (d.kind === 'message' || d.kind === 'request')
141
+ continue;
142
+ if (!mine(d))
133
143
  continue;
134
144
  const key = `delivery:${d.kind}:${d.resourceId}:${d.ts}`;
135
145
  if (deps.seen.has(key))
@@ -169,26 +179,26 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
169
179
  if (envelope.truncatedTasks) {
170
180
  warn(label, `inbox truncatedTasks=${envelope.truncatedTasks} (more assigned tasks than one pass surfaces — they appear as earlier ones close)`);
171
181
  }
172
- // quest channel. Triage is the host's job (exact string compare);
182
+ // request channel. Triage is the host's job (exact string compare);
173
183
  // a miss must still count as handled so the watermark can advance with 0 tokens.
174
- for (const quest of envelope.questsAwaitingMe ?? []) {
175
- const key = `quest:${quest.agreementId}:${quest.ts}`;
184
+ for (const request of envelope.requestsAwaitingMe ?? []) {
185
+ const key = `request:${request.agreementId}:${request.ts}`;
176
186
  if (deps.seen.has(key))
177
187
  continue;
178
188
  deps.seen.mark(key);
179
- if (deps.onQuest) {
180
- const handled = await attempt(`quest ${quest.agreementId}`, () => deps.onQuest(quest), () => deps.seen.unmark(key));
189
+ if (deps.onRequest) {
190
+ const handled = await attempt(`request ${request.agreementId}`, () => deps.onRequest(request), () => deps.seen.unmark(key));
181
191
  if (handled)
182
- questsTriaged++;
192
+ requestsTriaged++;
183
193
  }
184
194
  else {
185
195
  // No handler → still count as handled (ack) so an old host cannot wedge
186
- // forever on quest mail it does not understand.
187
- questsTriaged++;
196
+ // forever on request mail it does not understand.
197
+ requestsTriaged++;
188
198
  }
189
199
  }
190
- if (envelope.truncatedQuests) {
191
- warn(label, `inbox truncatedQuests=${envelope.truncatedQuests} (more quests than one pass surfaces — the tail follows on the next pass)`);
200
+ if (envelope.truncatedRequests) {
201
+ warn(label, `inbox truncatedRequests=${envelope.truncatedRequests} (more requests than one pass surfaces — the tail follows on the next pass)`);
192
202
  }
193
203
  // Pending decisions are standing state, not deliveries: they keep appearing
194
204
  // until answered, so dedup them the same way tasks are deduped.
@@ -226,10 +236,13 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
226
236
  runtimeLog.error(label, `acking a window that failed ${failures} handover(s) — giving up on redelivery to keep the ` +
227
237
  `agent alive; this mail is lost (ackTo=${envelope.ackTo})`);
228
238
  }
239
+ // Assigned rows only: the server's bury-guard checks what was MINE to
240
+ // handle. Ambient rows slide past the cursor unlisted — they were never
241
+ // this agent's to act on, and the owner's seen mark tracks them.
229
242
  const handledResourceIds = [
230
243
  ...new Set([
231
- ...envelope.deliveries.map((d) => d.resourceId),
232
- ...(envelope.questsAwaitingMe ?? []).map((q) => q.agreementId),
244
+ ...envelope.deliveries.filter(mine).map((d) => d.resourceId),
245
+ ...(envelope.requestsAwaitingMe ?? []).map((q) => q.agreementId),
233
246
  ]),
234
247
  ].filter((id) => typeof id === 'string' && id.length > 0);
235
248
  await inbox.ack(envelope.ackTo, { handledResourceIds });
@@ -245,7 +258,7 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
245
258
  messagesDelivered,
246
259
  resourceEvents,
247
260
  tasksDelivered,
248
- questsTriaged,
261
+ requestsTriaged,
249
262
  acked,
250
263
  truncated: envelope.deliveriesCapped,
251
264
  failures,
@@ -5,7 +5,13 @@ interface TickContext {
5
5
  marketplace: {
6
6
  publishOffer: (payload: PublishOfferPayload) => Promise<unknown>;
7
7
  pullOffers: (options?: PullOffersOptions) => Promise<unknown[]>;
8
- claimOffer: (agreementId: string) => Promise<unknown>;
8
+ /**
9
+ * Claim any open broadcast. One verb: the backend routes on which side of
10
+ * the row is open, and a caller cannot work that out for itself — reading
11
+ * the agreement first is party-scoped, and a claimer is by definition not
12
+ * yet a party.
13
+ */
14
+ claimAgreement: (agreementId: string) => Promise<unknown>;
9
15
  };
10
16
  }
11
17
  interface ProactiveTriggerOptions {
@@ -1,4 +1,4 @@
1
- import { publishOffer, pullOffers, claimOffer } from '@ziggs-ai/api-client';
1
+ import { publishOffer, pullOffers, claimOpenAgreement } from '@ziggs-ai/api-client';
2
2
  import { runtimeLog } from '../../shared/runtimeLog.js';
3
3
  export class ProactiveTrigger {
4
4
  interval;
@@ -64,7 +64,7 @@ export class ProactiveTrigger {
64
64
  marketplace: {
65
65
  publishOffer: (payload) => publishOffer(payload, creds),
66
66
  pullOffers: (options) => pullOffers(options, creds),
67
- claimOffer: (agreementId) => claimOffer(agreementId, creds),
67
+ claimAgreement: (agreementId) => claimOpenAgreement(agreementId, creds),
68
68
  },
69
69
  };
70
70
  }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * exact-match triage for marketplace requests.
3
+ * Empty match never hits (token-safe default). Comparison is exact after trim.
4
+ */
5
+ export declare function requestExactMatch(match: string, tags: ReadonlyArray<string | null | undefined>): boolean;
@@ -1,8 +1,8 @@
1
1
  /**
2
- * exact-match triage for marketplace quests.
2
+ * exact-match triage for marketplace requests.
3
3
  * Empty match never hits (token-safe default). Comparison is exact after trim.
4
4
  */
5
- export function questExactMatch(match, tags) {
5
+ export function requestExactMatch(match, tags) {
6
6
  const needle = typeof match === 'string' ? match.trim() : '';
7
7
  if (!needle)
8
8
  return false;
@@ -5,6 +5,14 @@ import { boundKeyFleetWarning } from '../shared/operatorKeyClaims.js';
5
5
  import { runtimeLog } from '../shared/runtimeLog.js';
6
6
  import { PollRegistry } from './polls/PollKind.js';
7
7
  import { PollRunner } from './polls/PollRunner.js';
8
+ /**
9
+ * How long a shutdown waits for in-flight turns after it has stopped taking new
10
+ * work. An agent turn is an LLM call, so the old 30s was under the length of the
11
+ * work it was meant to protect; the container's stop timeout is what has to be
12
+ * larger still, or the drain is cut short by SIGKILL regardless (ECS
13
+ * `stopTimeout`, 120s on the fleet task definitions).
14
+ */
15
+ const DRAIN_TIMEOUT_MS = Number(process.env.FLEET_DRAIN_TIMEOUT_MS) || 90_000;
8
16
  /**
9
17
  * Collect the poll kinds the agents themselves ship and start a runner for
10
18
  * whatever they declared. This lives in the launcher, not in a caller's
@@ -57,7 +65,7 @@ async function runSingle(config, { operatorKey, label = 'agent', port = process.
57
65
  runtimeLog.info(label, 'SIGTERM — shutting down');
58
66
  pollRunner?.stop();
59
67
  agent.disconnect();
60
- await agent.drain(30_000);
68
+ await agent.drain(DRAIN_TIMEOUT_MS);
61
69
  try {
62
70
  await onShutdown?.();
63
71
  }
@@ -122,10 +130,23 @@ async function runFleet(configs, { operatorKey, port = process.env.PORT || 8080,
122
130
  });
123
131
  process.on('SIGTERM', async () => {
124
132
  runtimeLog.info(label, 'SIGTERM — shutting down');
133
+ // Stop taking new work everywhere first, then wait for what is running.
134
+ //
135
+ // This used to disconnect and drain only `primary` — which `run.ts` never
136
+ // sets, so in the deployed fleet the drain was a no-op on null and the
137
+ // process exited straight through `disconnectAll`, killing every in-flight
138
+ // turn. A buyer whose request landed mid-rollout got a host that stopped
139
+ // mid-sentence and never reported.
140
+ //
141
+ // Draining before stopping intake is the other half of the same bug: the
142
+ // wait is exactly the window in which a host on its way out keeps accepting
143
+ // deliveries the incoming host is also being handed.
125
144
  pollRunner?.stop();
126
145
  primary?.disconnect?.();
127
- await primary?.drain?.(30_000);
128
- await pool.disconnectAll();
146
+ await Promise.all([
147
+ pool.shutdown(DRAIN_TIMEOUT_MS),
148
+ primary?.drain?.(DRAIN_TIMEOUT_MS) ?? Promise.resolve(),
149
+ ]);
129
150
  try {
130
151
  await onShutdown?.();
131
152
  }
@@ -19,11 +19,11 @@ export { PROTOCOL_TOOLS, agreementProposeTool, agreementSubcontractTool, agreeme
19
19
  export { dispatchProtocolOp } from './tier1/protocolRunner.js';
20
20
  export { DISCOVERY_TOOLS, agentSearchTool, agentGetTool } from './tier2/discoveryTools.js';
21
21
  export { MARKETPLACE_TOOLS, marketplaceViewTool } from './tier2/marketplaceTools.js';
22
- export { PAYMENT_TOOLS, paymentBalanceTool, paymentTransferTool, paymentWaitForApprovalTool, paymentHoldTool, paymentReleaseTool, paymentResolveWalletTool, paymentIssueGrantTool, paymentAttenuateGrantTool, paymentRevokeGrantTool, } from './tier2/paymentTools.js';
22
+ export { PAYMENT_TOOLS, paymentBalanceTool } from './tier2/paymentTools.js';
23
23
  export { CONNECTION_TOOLS, connectionProxyTool, connectionListGrantsTool, requestConnectionTool, } from './tier2/connectionTools.js';
24
24
  export { CONTEXT_TOOLS, contextReadTool, contextDelegateTool, contextExpandReachTool, contextDiscoverGrantableTool, } from './tier2/contextTools.js';
25
25
  export { GRANTS_TOOLS, listGrantsTool } from './tier2/grantsTools.js';
26
- export { LINK_TOOLS, linkCreateInviteTool, linkListTool, } from './tier2/linkTools.js';
26
+ export { LINK_TOOLS, linkProposeTool, linkListTool, } from './tier2/linkTools.js';
27
27
  export { ARTIFACT_TOOLS, recordArtifactTool } from './tier2/artifactTools.js';
28
28
  export { MCP_CONNECTION_TOOLS, mcpToolCallTool, mcpToolsListTool, } from './tier2/mcpConnectionTools.js';
29
29
  export { CHAT_TOOLS, openConversationTool } from './tier2/chatTools.js';
@@ -21,7 +21,7 @@ export { dispatchProtocolOp } from './tier1/protocolRunner.js';
21
21
  // ── Tier 2: capability bundles ──────────────────────────────────────────────
22
22
  export { DISCOVERY_TOOLS, agentSearchTool, agentGetTool } from './tier2/discoveryTools.js';
23
23
  export { MARKETPLACE_TOOLS, marketplaceViewTool } from './tier2/marketplaceTools.js';
24
- export { PAYMENT_TOOLS, paymentBalanceTool, paymentTransferTool, paymentWaitForApprovalTool, paymentHoldTool, paymentReleaseTool, paymentResolveWalletTool, paymentIssueGrantTool, paymentAttenuateGrantTool, paymentRevokeGrantTool, } from './tier2/paymentTools.js';
24
+ export { PAYMENT_TOOLS, paymentBalanceTool } from './tier2/paymentTools.js';
25
25
  export { CONNECTION_TOOLS, connectionProxyTool, connectionListGrantsTool, requestConnectionTool, } from './tier2/connectionTools.js';
26
26
  export { CONTEXT_TOOLS, contextReadTool, contextDelegateTool, contextExpandReachTool, contextDiscoverGrantableTool, } from './tier2/contextTools.js';
27
27
  // one grant lister across every rail (retires context_discover +
@@ -30,7 +30,7 @@ export { GRANTS_TOOLS, listGrantsTool } from './tier2/grantsTools.js';
30
30
  // links (agreements with engagementKind 'link') — cross-org reach.
31
31
  ///1022: the rail is two tools; propose/claim/revoke ride the
32
32
  // agreement grammar.
33
- export { LINK_TOOLS, linkCreateInviteTool, linkListTool, } from './tier2/linkTools.js';
33
+ export { LINK_TOOLS, linkProposeTool, linkListTool, } from './tier2/linkTools.js';
34
34
  // deliberate artifact recording (the model decides "record this").
35
35
  export { ARTIFACT_TOOLS, recordArtifactTool } from './tier2/artifactTools.js';
36
36
  export { MCP_CONNECTION_TOOLS, mcpToolCallTool, mcpToolsListTool, } from './tier2/mcpConnectionTools.js';
@@ -1,4 +1,4 @@
1
- import { isBroadcastTarget, } from '@ziggs-ai/api-client';
1
+ import { isBroadcastTarget, partyActorIds, } from '@ziggs-ai/api-client';
2
2
  import { applyFleetCounterPricing, applyFleetPricingForProtocol, } from '../../../pricing/fleetDefaults.js';
3
3
  // the link-summary rule lives in AgreementClient, at the point the
4
4
  // document is parsed. Every verb here inherits it through the client instead of
@@ -63,10 +63,19 @@ cachedAgreements) {
63
63
  return null;
64
64
  return agreementService.respond(payload.agreementId, payload.action);
65
65
  case 'agreement-claim': {
66
- // the one claim verb — quest, standing offer, or link invite.
66
+ // the one claim verb — request, standing offer, or link invite.
67
67
  if (!payload.agreementId)
68
68
  throw new Error('agreement-claim requires `agreementId`');
69
- return await agreementService.claim(payload.agreementId);
69
+ // The job this claim is part of, when the caller named one
70
+ // (the runtime fills it from the task being executed). It authorizes the
71
+ // claim; it does not re-parent the row, which stays in the publisher's
72
+ // tree.
73
+ const declaredMandate = payload.mandateAgreementId;
74
+ return await agreementService.claim(payload.agreementId, {
75
+ ...(typeof declaredMandate === 'string' && declaredMandate
76
+ ? { mandateAgreementId: declaredMandate }
77
+ : {}),
78
+ });
70
79
  }
71
80
  case 'agreement-counter-proposal':
72
81
  if (!payload.agreementId)
@@ -217,7 +226,7 @@ async function proposeAgreement(services, payload, chatId, _agents, actingAgentI
217
226
  // no chat, no money; the target owner approves. Folds the old request_link.
218
227
  if (payload.engagementKind === 'link') {
219
228
  if (isBroadcast) {
220
- throw new Error('A broadcast link is an invite — use link_create_invite instead; propose links to one agent id.');
229
+ throw new Error('A broadcast link is an invite — use link_propose with no target instead; it returns a share link.');
221
230
  }
222
231
  return services.agreementService.requestLink(payload.proposedTo, payload.description);
223
232
  }
@@ -240,7 +249,7 @@ async function proposeAgreement(services, payload, chatId, _agents, actingAgentI
240
249
  audience: payload.proposedTo === 'org' ? 'org' : 'everyone',
241
250
  });
242
251
  }
243
- throw new Error('On a broadcast, providerId must be your own agent id (a standing offer) or omitted (a quest — the claiming agent fills the open side).');
252
+ throw new Error('On a broadcast, providerId must be your own agent id (a standing offer) or omitted (a request — the claiming agent fills the open side).');
244
253
  }
245
254
  const priced = applyFleetPricingForProtocol('agreement-propose', payload, actingAgentId);
246
255
  const { operation: _op, ...fields } = priced;
@@ -283,13 +292,10 @@ async function agreementsInChat(services, chatId, cachedAgreements) {
283
292
  }
284
293
  }
285
294
  function agIsProvider(a, agentId) {
286
- const p = a.parties ?? {};
287
- return p['provider'] === agentId || p['providerAgent'] === agentId || a['providerId'] === agentId;
295
+ return a.parties?.provider?.actor === agentId || a['providerId'] === agentId;
288
296
  }
289
297
  function agIsParty(a, agentId) {
290
- const p = a.parties ?? {};
291
- return ['provider', 'providerAgent', 'payer', 'proposedTo', 'creator', 'creatorAgent']
292
- .some((k) => p[k] === agentId);
298
+ return partyActorIds(a.parties).includes(agentId);
293
299
  }
294
300
  function agActive(a) {
295
301
  return !a.status || !TERMINAL_STATUS.has(String(a.status));
@@ -43,10 +43,15 @@ function handlerFor(toolName) {
43
43
  return result;
44
44
  };
45
45
  }
46
- 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: 'Amount in CENTS — 500 means $5.00, shown as ϟ5.00 in the UI. Convert both ways: a human asking for "ϟ2" or "$2 per task" means 200, not 2. ϟ is the currency symbol, not a cent.' }, 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 quest 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 QUEST 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 (or link_create_invite when you lack the agent id). 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 });
47
- 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: 'Amount in CENTS — 500 means $5.00, shown as ϟ5.00 in the UI. Convert both ways: a human asking for "ϟ2" or "$2 per task" means 200, not 2. ϟ is the currency symbol, not a cent.' }, 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 });
48
- 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 (quests, 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.' });
49
- export const agreementClaimTool = defineTool('agreement_claim', { agreementId: { type: 'string', required: true, description: 'The open agreement to claim (quest / offer / link invite id)' } }, handlerFor('agreement_claim'), { description: 'Claim an open broadcast agreement by id — the DEFAULT way to engage: terms are already posted, consent is the claim, activation is instant, no negotiation turns. Claims a quest (you do the work, the publisher pays), a standing offer (you buy, the publisher works), a hand-off (providerPinned: the publisher\'s hired agent works FOR you — you become the customer, and the payer when it is priced), or a link invite (bilateral trust forms immediately). You become the open party and the agreement activates. A hand-off is claimable only after its provider has accepted (409 until then). Find quests/offers with marketplace_view; listings are take-it-or-leave-it — never counter one. Direct proposals are approved with agreement_respond instead, not claimed.' });
46
+ 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 });
47
+ 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 });
48
+ 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.' });
49
+ export const agreementClaimTool = defineTool('agreement_claim', {
50
+ agreementId: { type: 'string', required: true, description: 'The open agreement to claim (request / offer / link invite id)' },
51
+ // The job this claim is part of. Filled in from the task this wake is
52
+ // executing when the agent does not name one itself.
53
+ mandateAgreementId: { type: 'string', description: 'The active agreement whose work this claim is part of — the job you are doing. Claiming for a job your human already approved needs no second approval from them, but you have to name the job: the server checks you are a party to it, and a wrong name simply earns nothing. Leave it out when the claim belongs to no job.' },
54
+ }, handlerFor('agreement_claim'), { description: 'Claim an open broadcast agreement by id — the DEFAULT way to engage: terms are already posted, consent is the claim, activation is instant, no negotiation turns. Claims a request (you do the work, the publisher pays), a standing offer (you buy, the publisher works), a hand-off (providerPinned: the publisher\'s hired agent works FOR you — you become the customer, and the payer when it is priced), or a link invite (bilateral trust forms immediately). You become the open party and the agreement activates. A hand-off is claimable only after its provider has accepted (409 until then). Find requests/offers with marketplace_view; listings are take-it-or-leave-it — never counter one. Direct proposals are approved with agreement_respond instead, not claimed.' });
50
55
  export const taskSetResultTool = defineTool('task_set_result', {
51
56
  taskId: { type: 'string', required: true },
52
57
  // same param name as MCP ziggs_task_set_result / PATCH /tasks/:id/state.
@@ -54,10 +59,10 @@ export const taskSetResultTool = defineTool('task_set_result', {
54
59
  // Same object contract as MCP ({ summary, status, links, … }).
55
60
  result: {
56
61
  type: 'object',
57
- description: 'Outcome payload (summary, status, links, …). Finished work is the task result — never a chat message.',
62
+ description: 'Outcome payload (summary, status, links, …). This is what the next agent reads when it picks the work up from its own inbox.',
58
63
  },
59
- }, handlerFor('task_set_result'), { description: 'Transition a task to its terminal state — completed, failed, or cancelled — and record the result. Finished work is the task result: set it here as an object ({ summary, status, links }), never as a chat message. Pass state (not status).' });
60
- export const agreementCounterTool = defineTool('agreement_counter', { agreementId: { type: 'string', required: true }, price: { type: 'number', description: 'Amount in CENTS — 500 means $5.00, shown as ϟ5.00 in the UI. Convert both ways: a human asking for "ϟ2" or "$2 per task" means 200, not 2. ϟ is the currency symbol, not a cent.' }, 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.' });
64
+ }, 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).' });
65
+ 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.' });
61
66
  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).' });
62
67
  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.' });
63
68
  export const agreementFulfillTool = defineTool('agreement_fulfill', { agreementId: { type: 'string', required: true } }, handlerFor('agreement_fulfill'), { description: 'END an agreement you provide — permanently. Fulfilling terminates the whole relationship, not one deliverable: every grant it conferred is revoked, its shared space is torn down, and it cannot be reopened. Finished WORK is reported via task_set_result (the task closes; the agreement stays standing for the next task). Only fulfill when the engagement\'s full scope is delivered and nothing more is expected — never a standing hire that just finished a task.' });
@@ -1,4 +1,4 @@
1
1
  import type { ToolDefinition } from '../../../tools/defineTool.js';
2
- export declare const linkCreateInviteTool: ToolDefinition;
2
+ export declare const linkProposeTool: ToolDefinition;
3
3
  export declare const linkListTool: ToolDefinition;
4
4
  export declare const LINK_TOOLS: ToolDefinition[];
@@ -1,13 +1,15 @@
1
- import { LINK_CAPABILITIES, createLinkInviteCapability, listLinksCapability, } from '@ziggs-ai/api-client';
1
+ import { LINK_CAPABILITIES, listLinksCapability, proposeLinkCapability, } from '@ziggs-ai/api-client';
2
2
  import { toolFromCapability, toolsFromCapabilities } from '../../../tools/capabilityAdapter.js';
3
- // links are agreements with engagementKind 'link':
4
- // bilateral agent-to-agent trust that gives any unpublished agent cross-org
5
- // reach. Defined once in api-client's shared capability layer (including the
6
- // linkSummary shaping); this file only adapts them onto the SDK.
3
+ // links are agreements with engagementKind 'link': bilateral trust between two
4
+ // PEOPLE that gives either side reach to the other. Defined once in api-client's
5
+ // shared capability layer (including the linkSummary shaping); this file only
6
+ // adapts them onto the SDK.
7
7
  //
8
- ///1022 shrank the rail to two tools. The agreement verbs carry the
9
- // rest: request a direct link with agreement_propose (engagementKind 'link'),
10
- // claim an invite with agreement_claim, end a link with agreement_revoke.
11
- export const linkCreateInviteTool = toolFromCapability(createLinkInviteCapability);
8
+ // Two tools. There were three: `link_create_invite` was `link_propose` with a
9
+ // different way of naming the other side, so a caller had to choose between them
10
+ // before knowing which applied. One verb takes an email, an agent id, or
11
+ // nothing. The agreement verbs carry the rest: claim an invite with
12
+ // agreement_claim, end a link with agreement_revoke.
13
+ export const linkProposeTool = toolFromCapability(proposeLinkCapability);
12
14
  export const linkListTool = toolFromCapability(listLinksCapability);
13
15
  export const LINK_TOOLS = toolsFromCapabilities(LINK_CAPABILITIES);
@@ -1,9 +1,9 @@
1
1
  import { MARKETPLACE_CAPABILITIES, marketplaceViewCapability } from '@ziggs-ai/api-client';
2
2
  import { toolFromCapability, toolsFromCapabilities } from '../../../tools/capabilityAdapter.js';
3
- // the marketplace read (quests AND standing offers),
3
+ // the marketplace read (requests AND standing offers),
4
4
  // defined once in api-client's shared capability layer. Publishing is NOT
5
5
  // here: it is the tier-1 verb agreement_propose with proposedTo
6
- // "everyone" | "org" (providerId = own id for an offer, omitted for a quest);
6
+ // "everyone" | "org" (providerId = own id for an offer, omitted for a request);
7
7
  // claiming is agreement_claim.
8
8
  export const marketplaceViewTool = toolFromCapability(marketplaceViewCapability);
9
9
  export const MARKETPLACE_TOOLS = toolsFromCapabilities(MARKETPLACE_CAPABILITIES);
@@ -1,4 +1,37 @@
1
1
  import { type ToolDefinition } from '../../../tools/defineTool.js';
2
2
  export declare const mcpToolsListTool: ToolDefinition;
3
+ /**
4
+ * How much of one MCP tool result a wake will carry.
5
+ *
6
+ * A remote MCP server hands back the upstream API's payload verbatim, and those
7
+ * are sized for programs, not for a context window: GitHub's `list_pull_requests`
8
+ * with no paging argument answered ~150,000 characters. Past the runtime's limit
9
+ * the result stops being a result — the Claude SDK spills an oversized tool
10
+ * result to a file, and a hosted agent has no filesystem tool to read it with, so
11
+ * the wake ends in "the answer was too big and I cannot see it". Same class of
12
+ * problem as an MCP server that ships a 30K-character tool description, one layer
13
+ * later.
14
+ *
15
+ * A bounded answer with a marker beats an unreadable one: the model can act on
16
+ * "truncated, narrow the call", and it cannot act on a file path.
17
+ */
18
+ export declare const MAX_MCP_RESULT_CHARS = 24000;
19
+ /**
20
+ * Tool schemas are bigger than tool results and are read once per wake, so they
21
+ * get their own, larger ceiling — a schema cut in half is worse than useless,
22
+ * because the half that survives looks complete.
23
+ */
24
+ export declare const MAX_MCP_TOOLS_LIST_CHARS = 60000;
25
+ /**
26
+ * Bound one MCP result, telling the model plainly what was cut.
27
+ *
28
+ * Text blocks are cut individually rather than the JSON being sliced, because a
29
+ * sliced JSON string is invalid and a model handed invalid JSON reports the
30
+ * server as broken.
31
+ */
32
+ export declare function boundMcpResult(result: unknown, max?: number): {
33
+ result: unknown;
34
+ truncated?: Record<string, unknown>;
35
+ };
3
36
  export declare const mcpToolCallTool: ToolDefinition;
4
37
  export declare const MCP_CONNECTION_TOOLS: ToolDefinition[];