@ziggs-ai/agent-sdk 0.13.0 → 0.14.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.
@@ -151,6 +151,18 @@ function renderEventLines(event, lines) {
151
151
  lines.push(`Agreement: ${agreementId}`);
152
152
  }
153
153
  }
154
+ // The other side of the same policy: a message this agent sent to a
155
+ // published agent landed and was NOT rung through. Nothing is wrong and
156
+ // nothing is lost, so this must not read as a refusal — but without it the
157
+ // sender waits on a reply that is never coming and its rational next move
158
+ // is to send again.
159
+ if (reason === 'contact_policy_withheld') {
160
+ lines.push('Your message was delivered and is readable, but the agent you wrote ' +
161
+ 'to is not woken for it: its listing is claim-first, so it starts ' +
162
+ 'work once you claim the listing. Do not resend — claim the listing ' +
163
+ '(ziggs_marketplace_view to find it, then ziggs_agreement_claim), or ' +
164
+ 'wait if somebody else is claiming on your behalf.');
165
+ }
154
166
  }
155
167
  if (typeof event?.agreementId === 'string' && event.agreementId) {
156
168
  lines.push(`Agreement: ${event.agreementId}`);
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { laneFromToolContext } from '../tools/toolCreds.js';
2
3
  import { listTasks } from '@ziggs-ai/api-client';
3
4
  import { defineTool } from '../tools/defineTool.js';
4
5
  import { listGrantsTool } from '../server/tools/tier2/grantsTools.js';
@@ -177,7 +178,10 @@ export const listTasksTool = defineTool('task_list', {
177
178
  const createdBy = args.createdByMe ? c.agentId : undefined;
178
179
  // the lane fences this to the engagement's tasks. Without it the
179
180
  // list is every task the agent's key can reach, across all its customers.
180
- const laneId = c.chatId || c.sessionId || undefined;
181
+ // One reader for the lane, shared with the capability adapter and with
182
+ // hand-written tools — three copies of `chatId || sessionId` is how one of
183
+ // them ends up not claiming the reach the fence exists to grant.
184
+ const laneId = laneFromToolContext(c);
181
185
  return listTasks({
182
186
  state: args.state,
183
187
  cursor: args.cursor,
package/dist/index.d.ts CHANGED
@@ -53,6 +53,7 @@ export { ToolManager } from './tools/ToolManager.js';
53
53
  export { ToolProvider } from './tools/ToolProvider.js';
54
54
  export { defineTool } from './tools/defineTool.js';
55
55
  export { recordReport } from './tools/recordReport.js';
56
+ export { credsFromToolContext, laneFromToolContext } from './tools/toolCreds.js';
56
57
  export type { RecordReportInput, RecordReportResult, TaskReport } from './tools/recordReport.js';
57
58
  export { MCP_PROVIDER, describeNeed, normalizeNeeds, toolsForNeed, toolsForNeeds, unmetNeeds, unmetNeedsMessage, } from './tools/needs.js';
58
59
  export type { AgentNeed, ConnectionNeed } from './tools/needs.js';
package/dist/index.js CHANGED
@@ -55,6 +55,7 @@ export { ToolManager } from './tools/ToolManager.js';
55
55
  export { ToolProvider } from './tools/ToolProvider.js';
56
56
  export { defineTool } from './tools/defineTool.js';
57
57
  export { recordReport } from './tools/recordReport.js';
58
+ export { credsFromToolContext, laneFromToolContext } from './tools/toolCreds.js';
58
59
  // An agent declares the access it cannot work without, and the declaration
59
60
  // produces the tools for it.
60
61
  export { MCP_PROVIDER, describeNeed, normalizeNeeds, toolsForNeed, toolsForNeeds, unmetNeeds, unmetNeedsMessage, } from './tools/needs.js';
@@ -8,7 +8,6 @@ export declare const TIER_PRICE_CENTS: Record<FleetPricingTier, number>;
8
8
  export declare function resolveFleetTier(agentId: string): FleetPricingTier | null;
9
9
  export declare function isFirstPartyFleetAgent(agentId: string): boolean;
10
10
  export declare function tierPriceCents(tier: FleetPricingTier): number;
11
- export declare function isZiggsOrchestrator(agentId: string): boolean;
12
11
  export declare function isFreeAllowlistedExecutor(executorId: string, description?: string | null): boolean;
13
12
  export declare function resolveDefaultPriceCentsForExecutor(executorId: string, description?: string | null): number | null;
14
13
  export interface ProtocolPricingPayload {
@@ -12,7 +12,6 @@ export const TIER_PRICE_CENTS = {
12
12
  C: 1500, // 15 ZIG / job (orchestrator, charged once at top)
13
13
  D: 100, // 1 ZIG / call (hand-written demo agents)
14
14
  };
15
- const ZIGGS_ORCHESTRATOR_IDS = new Set(['ziggs-agent']);
16
15
  const PING_DESCRIPTION_PATTERN = /\b(ping|health\s*check|healthcheck|liveness|readiness|heartbeat)\b/i;
17
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;
18
17
  export function resolveFleetTier(agentId) {
@@ -26,9 +25,6 @@ export function isFirstPartyFleetAgent(agentId) {
26
25
  export function tierPriceCents(tier) {
27
26
  return TIER_PRICE_CENTS[tier];
28
27
  }
29
- export function isZiggsOrchestrator(agentId) {
30
- return ZIGGS_ORCHESTRATOR_IDS.has(agentId);
31
- }
32
28
  export function isFreeAllowlistedExecutor(executorId, description) {
33
29
  if (!executorId)
34
30
  return false;
@@ -82,14 +78,13 @@ export function applyFleetAgreementPricing(operation, payload, actingAgentId) {
82
78
  }
83
79
  const proposedTo = next.proposedTo != null ? String(next.proposedTo) : '';
84
80
  const providerId = next.providerId != null ? String(next.providerId) : '';
85
- // Ziggs scope proposal to human user — 15 ZIG once at top.
86
- if (isZiggsOrchestrator(actingAgentId) &&
87
- proposedTo &&
88
- !isFirstPartyFleetAgent(proposedTo)) {
89
- next.price = TIER_PRICE_CENTS.C;
90
- return next;
91
- }
92
- // Tier C orchestrator proposing scope to a user (non-ziggs fleet orchestrators).
81
+ // A tier C orchestrator proposing scope to a user — 15 ZIG once at top.
82
+ //
83
+ // The ziggs delegate used to have its own branch above this one, keyed on a
84
+ // hardcoded id. It asked the same question with the same answer — that agent
85
+ // is tier C, so this line already priced it. A second way of asking the tier
86
+ // question the tier had already answered, which is the same shape that was
87
+ // removed from the backend copy.
93
88
  const actingTier = resolveFleetTier(actingAgentId);
94
89
  if (actingTier === 'C' && proposedTo && !isFirstPartyFleetAgent(proposedTo)) {
95
90
  next.price = TIER_PRICE_CENTS.C;
@@ -11,15 +11,28 @@ export declare function buildKickoffState(initial: string): ThinkingState;
11
11
  export declare function buildClaimKickoffState(initial: string): ThinkingState;
12
12
  /**
13
13
  * Agreement-kickoff defaults. For every state whose transitions do not already
14
- * match the corresponding probe, append a transition to the injected kickoff
15
- * state. Appended LAST so any user transition on the same outcome always wins
16
- * (first match wins in AgentMachine._evaluateTransitions). Returns a NEW
17
- * workflow — input states are not mutated.
14
+ * handle the corresponding probe, add a transition to the injected kickoff
15
+ * state. Returns a NEW workflow — input states are not mutated.
18
16
  *
19
17
  * Two kickoffs:
20
18
  * - `kickingOff` — proposer-side approval → task_create
21
19
  * - `claimKickingOff` — store-claim provider wake → chat_open + greet
22
20
  * Direct-proposal counterparties (lifecycle approve, !proposedByMe, no
23
21
  * fromClaimWake) still never auto-kickoff.
22
+ *
23
+ * **They are ordered differently on purpose** (first match wins in
24
+ * AgentMachine._evaluateTransitions):
25
+ *
26
+ * - `kickingOff` is appended LAST. It fires on an approval of a proposal THIS
27
+ * agent made, which the author was already expecting — whatever route they
28
+ * wrote for it should keep winning.
29
+ * - `claimKickingOff` is inserted FIRST. It fires on a wake the author never
30
+ * wrote a route for, and it has to beat the unconditional fallthrough an
31
+ * `idle` state uses to reach its listening loop — otherwise the claim is
32
+ * swallowed into the idle loop and the buyer is never greeted. Going first
33
+ * is safe because its `when` is narrow: only a `fromClaimWake` approve
34
+ * matches, so nothing else is shadowed. An author who wants the claim for
35
+ * themselves keys on `fromClaimWake`, which suppresses the injection
36
+ * outright (stateHandlesClaimKickoff).
24
37
  */
25
38
  export declare function injectKickoffDefault(workflow: Workflow): Workflow;
@@ -25,6 +25,18 @@ const PROBE_CLAIM_APPROVE = {
25
25
  proposedByMe: false,
26
26
  fromClaimWake: true,
27
27
  };
28
+ /**
29
+ * A bare counterparty approve — someone approved a proposal we did not make,
30
+ * with no store claim behind it. Used as a NEGATIVE probe: a transition that
31
+ * matches this as well as PROBE_CLAIM_APPROVE is a generic proposal-resolved
32
+ * route, not a deliberate claim handler, so it does not suppress injection.
33
+ */
34
+ const PROBE_BARE_APPROVE = {
35
+ kind: 'proposal-resolved',
36
+ action: 'approve',
37
+ proposal: { agreementId: '__bare_approve_probe__' },
38
+ proposedByMe: false,
39
+ };
28
40
  const KICKOFF_TRANSITION = {
29
41
  to: KICKOFF_STATE,
30
42
  when: (o, ctx) => o.kind === 'proposal-resolved' &&
@@ -69,17 +81,32 @@ function stateHandlesProposerApproval(state) {
69
81
  }
70
82
  });
71
83
  }
84
+ function matches(t, probe) {
85
+ if (!t.when)
86
+ return false;
87
+ try {
88
+ return !!t.when(probe, PROBE_CTX);
89
+ }
90
+ catch {
91
+ return false;
92
+ }
93
+ }
94
+ /**
95
+ * A state handles the claim kickoff itself only when it says so EXPLICITLY —
96
+ * a transition that fires for a store-claim wake and does NOT also fire for a
97
+ * bare counterparty approve. In practice that means keying on `fromClaimWake`.
98
+ *
99
+ * Two weaker rules read as "handled" when nothing was. A when-less fallthrough
100
+ * counted, so a state shaped `idle → listening` never got the transition
101
+ * injected and routed every claim into its idle loop; and a `when: o => o.kind
102
+ * === 'proposal-resolved'` route counted too, because the claim probe is also a
103
+ * proposal-resolved/approve. Neither author had written anything about claims.
104
+ * Requiring the negative probe to MISS is what tells a deliberate claim handler
105
+ * apart from a generic one, and the unconditional case is safe to inject over
106
+ * because the claim transition goes in FIRST — see injectKickoffDefault.
107
+ */
72
108
  function stateHandlesClaimKickoff(state) {
73
- return state.transitions.some(t => {
74
- if (!t.when)
75
- return true;
76
- try {
77
- return !!t.when(PROBE_CLAIM_APPROVE, PROBE_CTX);
78
- }
79
- catch {
80
- return false;
81
- }
82
- });
109
+ return state.transitions.some(t => matches(t, PROBE_CLAIM_APPROVE) && !matches(t, PROBE_BARE_APPROVE));
83
110
  }
84
111
  export function buildKickoffState(initial) {
85
112
  const dflt = thinkingDefaults({ initial });
@@ -161,16 +188,29 @@ export function buildClaimKickoffState(initial) {
161
188
  }
162
189
  /**
163
190
  * Agreement-kickoff defaults. For every state whose transitions do not already
164
- * match the corresponding probe, append a transition to the injected kickoff
165
- * state. Appended LAST so any user transition on the same outcome always wins
166
- * (first match wins in AgentMachine._evaluateTransitions). Returns a NEW
167
- * workflow — input states are not mutated.
191
+ * handle the corresponding probe, add a transition to the injected kickoff
192
+ * state. Returns a NEW workflow — input states are not mutated.
168
193
  *
169
194
  * Two kickoffs:
170
195
  * - `kickingOff` — proposer-side approval → task_create
171
196
  * - `claimKickingOff` — store-claim provider wake → chat_open + greet
172
197
  * Direct-proposal counterparties (lifecycle approve, !proposedByMe, no
173
198
  * fromClaimWake) still never auto-kickoff.
199
+ *
200
+ * **They are ordered differently on purpose** (first match wins in
201
+ * AgentMachine._evaluateTransitions):
202
+ *
203
+ * - `kickingOff` is appended LAST. It fires on an approval of a proposal THIS
204
+ * agent made, which the author was already expecting — whatever route they
205
+ * wrote for it should keep winning.
206
+ * - `claimKickingOff` is inserted FIRST. It fires on a wake the author never
207
+ * wrote a route for, and it has to beat the unconditional fallthrough an
208
+ * `idle` state uses to reach its listening loop — otherwise the claim is
209
+ * swallowed into the idle loop and the buyer is never greeted. Going first
210
+ * is safe because its `when` is narrow: only a `fromClaimWake` approve
211
+ * matches, so nothing else is shadowed. An author who wants the claim for
212
+ * themselves keys on `fromClaimWake`, which suppresses the injection
213
+ * outright (stateHandlesClaimKickoff).
174
214
  */
175
215
  export function injectKickoffDefault(workflow) {
176
216
  if (workflow.states[KICKOFF_STATE]) {
@@ -189,7 +229,7 @@ export function injectKickoffDefault(workflow) {
189
229
  injectedProposer = true;
190
230
  }
191
231
  if (!stateHandlesClaimKickoff(state)) {
192
- transitions.push(CLAIM_KICKOFF_TRANSITION);
232
+ transitions.unshift(CLAIM_KICKOFF_TRANSITION);
193
233
  injectedClaim = true;
194
234
  }
195
235
  states[name] =
@@ -1,4 +1,4 @@
1
- import { type Agreement, type ClaimedKind, type ClaimOptions, type ProposeAgreementData, type ProposeDirectInput, type ProposeBroadcastInput, type PublishOfferPayload, type DelegateAgreementData, type CounterAgreementData, type CreateLinkBody, type CreateLinkResult, type ListAgreementsFilters, type GetMyAgreementsFilters, type ChatLinkType, type ArtifactLinkType, type UserRole } from '@ziggs-ai/api-client';
1
+ import { type Agreement, type ClaimedKind, type ClaimOptions, type ProposeAgreementData, type ProposeDirectInput, type ProposeBroadcastInput, type PublishOfferPayload, type DelegateAgreementData, type CounterAgreementData, type CreateLinkBody, type CreateLinkResult, type ListAgreementsFilters, type GetMyAgreementsFilters, type ChatLinkType, type ArtifactLinkType } from '@ziggs-ai/api-client';
2
2
  export declare class AgreementService {
3
3
  private creds;
4
4
  constructor(operatorKey: string, agentId: string);
@@ -70,6 +70,4 @@ export declare class AgreementService {
70
70
  getChats(agreementId: string): Promise<unknown[]>;
71
71
  linkArtifact(agreementId: string, artifactId: string, linkType?: ArtifactLinkType): Promise<unknown | null>;
72
72
  getArtifacts(agreementId: string): Promise<unknown[]>;
73
- linkUser(agreementId: string, userId: string, role: UserRole): Promise<unknown | null>;
74
- getUsers(agreementId: string): Promise<unknown[]>;
75
73
  }
@@ -1,4 +1,4 @@
1
- import { proposeAgreement, proposeBroadcast as proposeBroadcastApi, delegateAgreement, respondToAgreement, counterAgreement, claimOpenAgreement, getAgreementStatus, listAgreements, getMyAgreements, getAgreement, publishOffer as publishOfferApi, createLink, revokeAgreement, fulfillAgreement, linkAgreementToChat, getChatsForAgreement, linkArtifactToAgreement, getArtifactsForAgreement, linkUserToAgreement, getUsersForAgreement, } from '@ziggs-ai/api-client';
1
+ import { proposeAgreement, proposeBroadcast as proposeBroadcastApi, delegateAgreement, respondToAgreement, counterAgreement, claimOpenAgreement, getAgreementStatus, listAgreements, getMyAgreements, getAgreement, publishOffer as publishOfferApi, createLink, revokeAgreement, fulfillAgreement, linkAgreementToChat, getChatsForAgreement, linkArtifactToAgreement, getArtifactsForAgreement, } from '@ziggs-ai/api-client';
2
2
  export class AgreementService {
3
3
  creds;
4
4
  constructor(operatorKey, agentId) {
@@ -107,10 +107,4 @@ export class AgreementService {
107
107
  getArtifacts(agreementId) {
108
108
  return getArtifactsForAgreement(agreementId, this.creds);
109
109
  }
110
- linkUser(agreementId, userId, role) {
111
- return linkUserToAgreement(agreementId, userId, role, this.creds);
112
- }
113
- getUsers(agreementId) {
114
- return getUsersForAgreement(agreementId, this.creds);
115
- }
116
110
  }
@@ -8,6 +8,20 @@ function log(label, msg) {
8
8
  function warn(label, msg) {
9
9
  runtimeLog.warn(label, msg);
10
10
  }
11
+ /**
12
+ * Standing proposals share the merged inbox visibility model with deliveries,
13
+ * but older envelopes predate an explicit assignee stamp. Prefer the server's
14
+ * role-to-actor assignment. On an older server, wake only for an agent-keyed
15
+ * approval/responder slot — withholding a wake is safer than waking every
16
+ * sibling merely because they share an org principal.
17
+ */
18
+ function proposalIsMine(proposal, ownAgentId) {
19
+ if (proposal.assignedAgentId !== undefined) {
20
+ return proposal.assignedAgentId === ownAgentId;
21
+ }
22
+ return (proposal.pendingApprovalPartyIds?.includes(ownAgentId) === true ||
23
+ proposal.proposedTo === ownAgentId);
24
+ }
11
25
  /**
12
26
  * Read the messages behind one chat's deliveries and hand them to the host.
13
27
  *
@@ -203,6 +217,11 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
203
217
  // Pending decisions are standing state, not deliveries: they keep appearing
204
218
  // until answered, so dedup them the same way tasks are deduped.
205
219
  for (const proposal of envelope.proposalsAwaitingMe) {
220
+ // A proposal may be visible because this agent shares the accountable
221
+ // principal's mailbox. Only the role-designated side actor wakes; ambient
222
+ // sibling proposals remain available as context without creating a lane.
223
+ if (!proposalIsMine(proposal, deps.ownAgentId))
224
+ continue;
206
225
  const key = `proposal:${proposal.agreementId}`;
207
226
  if (deps.seen.has(key))
208
227
  continue;
@@ -65,7 +65,7 @@ export const taskSetResultTool = defineTool('task_set_result', {
65
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.' });
66
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).' });
67
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.' });
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.' });
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 and it cannot be reopened. Linked chats remain independent rooms governed by their own grants. 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.' });
69
69
  // `status` names its four values here because nothing else does: the backend is
70
70
  // the only place the vocabulary was written down, and it now rejects anything
71
71
  // outside it. Every reader (the chat step badge, the SDK's own plan formatter)
@@ -1,4 +1,5 @@
1
1
  import { defineTool } from './defineTool.js';
2
+ import { laneFromToolContext } from './toolCreds.js';
2
3
  /**
3
4
  * the SDK surface adapter for shared capability definitions
4
5
  * (api-client `capabilities/`). One capability (schema + handler + guidance)
@@ -10,13 +11,15 @@ export function toolFromCapability(cap) {
10
11
  return defineTool(cap.names.sdk, cap.params, async (args, ctx) => {
11
12
  const operatorKey = ctx?.['operatorKey'];
12
13
  const agentId = ctx?.['agentId'];
13
- // the lane was already threaded to this exact point by
14
- //'s shared executor (`chatId: effect.sessionId` in
15
- // executeToolWithSideEffects); it just was not used to narrow anything.
16
- // Reading it here makes the fence brain-agnostic by construction: the
17
- // Claude brain and the FSM brain both arrive through this adapter.
18
- const laneId = ctx?.['chatId'] ||
19
- ctx?.['sessionId'];
14
+ // The lane the wake is running in was already threaded to this exact
15
+ // point by the shared executor (`chatId: effect.sessionId` in
16
+ // executeToolWithSideEffects). `laneFromToolContext` is the one reader
17
+ // for it, which makes the fence brain-agnostic by construction — the
18
+ // Claude brain, the FSM brain and hand-written tools all claim the same
19
+ // lane instead of each author remembering to. A capability may run
20
+ // without an agent id (`needsAgentId: false`), which is why this builds
21
+ // the creds itself rather than calling `credsFromToolContext`.
22
+ const laneId = laneFromToolContext(ctx);
20
23
  if (!operatorKey)
21
24
  throw new Error('operatorKey missing from tool context');
22
25
  if (cap.needsAgentId && !agentId)
@@ -0,0 +1,37 @@
1
+ /**
2
+ * toolCreds.ts — the credentials a tool acts under, read off its own context.
3
+ *
4
+ * A custom tool builds `{ operatorKey, agentId }` by hand and stops there, so
5
+ * every call it makes runs on the narrowest reach fence there is: the agent's
6
+ * own org. `AgentHost` already threads the wake's lane into the tool context
7
+ * (as `chatId` / `sessionId`, which is what the capability adapter reads), and
8
+ * the backend's fence is "own org ∪ the orgs of the parties to this lane" — so
9
+ * a tool that does not pass it loses reach it is entitled to, and loses it
10
+ * SILENTLY: enumerations come back short rather than refused.
11
+ *
12
+ * Measured on a five-node graph run: a coordinator asking which tasks it had
13
+ * created saw 0 of 5 with no lane and 5 of 5 with its own lane declared. The
14
+ * `task_list({ createdByMe: true })` convergence check that `lab-hirer`'s prompt
15
+ * recommends would therefore answer "nothing yet" and rebuild the whole graph,
16
+ * billing every worker twice.
17
+ *
18
+ * So: one reader, used by every tool, rather than a field each author has to
19
+ * remember. Omitting the lane can only narrow reach, never widen it, which is
20
+ * why forgetting it never fails loudly.
21
+ */
22
+ import type { Creds } from '@ziggs-ai/api-client';
23
+ /** The loose bag `defineTool` hands a handler. */
24
+ type ToolContext = Record<string, unknown> | undefined | null;
25
+ /**
26
+ * The wake's lane, as the backend's `X-Ziggs-Lane` expects it: a chat id, or
27
+ * `agrn-<agreementId>` for an agreement lane. Undefined when the tool is
28
+ * running outside any lane (a bootstrap sweep, a test), which the fence reads
29
+ * as "own org only".
30
+ */
31
+ export declare function laneFromToolContext(ctx: ToolContext): string | undefined;
32
+ /**
33
+ * The creds a tool should act under. Throws when the context is missing what a
34
+ * call needs, rather than building something that 401s later somewhere else.
35
+ */
36
+ export declare function credsFromToolContext(ctx: ToolContext, toolName?: string): Creds;
37
+ export {};
@@ -0,0 +1,49 @@
1
+ /**
2
+ * toolCreds.ts — the credentials a tool acts under, read off its own context.
3
+ *
4
+ * A custom tool builds `{ operatorKey, agentId }` by hand and stops there, so
5
+ * every call it makes runs on the narrowest reach fence there is: the agent's
6
+ * own org. `AgentHost` already threads the wake's lane into the tool context
7
+ * (as `chatId` / `sessionId`, which is what the capability adapter reads), and
8
+ * the backend's fence is "own org ∪ the orgs of the parties to this lane" — so
9
+ * a tool that does not pass it loses reach it is entitled to, and loses it
10
+ * SILENTLY: enumerations come back short rather than refused.
11
+ *
12
+ * Measured on a five-node graph run: a coordinator asking which tasks it had
13
+ * created saw 0 of 5 with no lane and 5 of 5 with its own lane declared. The
14
+ * `task_list({ createdByMe: true })` convergence check that `lab-hirer`'s prompt
15
+ * recommends would therefore answer "nothing yet" and rebuild the whole graph,
16
+ * billing every worker twice.
17
+ *
18
+ * So: one reader, used by every tool, rather than a field each author has to
19
+ * remember. Omitting the lane can only narrow reach, never widen it, which is
20
+ * why forgetting it never fails loudly.
21
+ */
22
+ const str = (ctx, key) => {
23
+ const value = ctx?.[key];
24
+ return typeof value === 'string' && value.trim() ? value : undefined;
25
+ };
26
+ /**
27
+ * The wake's lane, as the backend's `X-Ziggs-Lane` expects it: a chat id, or
28
+ * `agrn-<agreementId>` for an agreement lane. Undefined when the tool is
29
+ * running outside any lane (a bootstrap sweep, a test), which the fence reads
30
+ * as "own org only".
31
+ */
32
+ export function laneFromToolContext(ctx) {
33
+ return str(ctx, 'chatId') ?? str(ctx, 'sessionId');
34
+ }
35
+ /**
36
+ * The creds a tool should act under. Throws when the context is missing what a
37
+ * call needs, rather than building something that 401s later somewhere else.
38
+ */
39
+ export function credsFromToolContext(ctx, toolName) {
40
+ const where = toolName ? `${toolName}: ` : '';
41
+ const operatorKey = str(ctx, 'operatorKey');
42
+ if (!operatorKey)
43
+ throw new Error(`${where}operatorKey missing from tool context`);
44
+ const agentId = str(ctx, 'agentId');
45
+ if (!agentId)
46
+ throw new Error(`${where}agentId missing from tool context`);
47
+ const laneId = laneFromToolContext(ctx);
48
+ return { operatorKey, agentId, ...(laneId ? { laneId } : {}) };
49
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/agent-sdk",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Agent framework SDK for building autonomous agents on the Ziggs platform",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -26,7 +26,7 @@
26
26
  "@anthropic-ai/claude-agent-sdk": "^0.3.204",
27
27
  "@anthropic-ai/sdk": "^0.110.0",
28
28
  "@modelcontextprotocol/sdk": "^1.29.0",
29
- "@ziggs-ai/api-client": "0.12.0",
29
+ "@ziggs-ai/api-client": "0.13.0",
30
30
  "ajv": "^8.17.1",
31
31
  "dotenv": "^17.2.3",
32
32
  "openai": "^4.0.0",