@ziggs-ai/agent-sdk 0.12.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.
Files changed (41) hide show
  1. package/dist/AgentHost.js +4 -1
  2. package/dist/agent/ClaudeSdkAgent.js +2 -2
  3. package/dist/agent/claudeCognition.js +12 -0
  4. package/dist/agent/claudeSdkTools.js +5 -1
  5. package/dist/cognition/validateContext.js +4 -1
  6. package/dist/context/batch.d.ts +3 -0
  7. package/dist/context/batch.js +8 -5
  8. package/dist/context/classifyEnvelope.js +4 -5
  9. package/dist/context/routingLabels.js +2 -3
  10. package/dist/formatters/AgreementFormatter.d.ts +7 -1
  11. package/dist/formatters/AgreementFormatter.js +17 -8
  12. package/dist/formatters/HistoryFormatter.d.ts +5 -0
  13. package/dist/formatters/HistoryFormatter.js +16 -7
  14. package/dist/index.d.ts +2 -1
  15. package/dist/index.js +2 -1
  16. package/dist/ingress/normalizeIncoming.d.ts +3 -8
  17. package/dist/pricing/fleetDefaults.d.ts +0 -1
  18. package/dist/pricing/fleetDefaults.js +7 -12
  19. package/dist/runtime/PromptBuilder.js +19 -21
  20. package/dist/runtime/buildOutcome.js +4 -4
  21. package/dist/runtime/kickoff.d.ts +17 -4
  22. package/dist/runtime/kickoff.js +55 -15
  23. package/dist/runtime/mandateDeclaration.d.ts +9 -0
  24. package/dist/runtime/mandateDeclaration.js +62 -0
  25. package/dist/runtime/runTurn.js +8 -0
  26. package/dist/server/agreements/AgreementService.d.ts +14 -12
  27. package/dist/server/agreements/AgreementService.js +15 -15
  28. package/dist/server/inboxLoop.js +19 -0
  29. package/dist/server/proactive/ProactiveTrigger.d.ts +7 -1
  30. package/dist/server/proactive/ProactiveTrigger.js +2 -2
  31. package/dist/server/tools/index.d.ts +1 -1
  32. package/dist/server/tools/index.js +1 -1
  33. package/dist/server/tools/tier1/protocolRunner.js +14 -8
  34. package/dist/server/tools/tier1/protocolTools.js +8 -3
  35. package/dist/server/tools/tier2/linkTools.d.ts +1 -1
  36. package/dist/server/tools/tier2/linkTools.js +11 -9
  37. package/dist/tools/capabilityAdapter.js +10 -7
  38. package/dist/tools/toolCreds.d.ts +37 -0
  39. package/dist/tools/toolCreds.js +49 -0
  40. package/dist/types.d.ts +2 -6
  41. package/package.json +2 -2
@@ -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] =
@@ -0,0 +1,9 @@
1
+ import type { ToolManager } from '../tools/ToolManager.js';
2
+ /**
3
+ * Add the wake's own mandate to a tool call that declares one and left it blank.
4
+ *
5
+ * Returns the args unchanged whenever there is nothing to say: the tool takes no
6
+ * declaration, the model already made one, or this wake is not running inside a
7
+ * job at all.
8
+ */
9
+ export declare function withDeclaredMandate(args: Record<string, unknown>, toolName: string | null | undefined, toolManager: ToolManager | undefined, activeAgreementId: string | null | undefined): Record<string, unknown>;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Fill in the job an agent is working inside, so it does not have to.
3
+ *
4
+ * The consent gate excuses an agreement an agent forms INSIDE a job its human
5
+ * already approved, and it verifies the claim server-side. What was missing was
6
+ * the claim itself: nothing on the agent surface said "I am doing this as part
7
+ * of job X", so every engagement an agent formed arrived as an outsider and
8
+ * waited on a human — including buying the help the job it was hired for needs.
9
+ *
10
+ * The declaration is made at the CALLER, never resolved on the server: the
11
+ * agreements module must not read the tasks module, and a guessed mandate is a
12
+ * false claim the termination cascade would act on destructively. Here the fact
13
+ * is free — the runtime already knows which task this wake is executing and
14
+ * which agreement that task runs under, which is exactly the answer.
15
+ *
16
+ * The model can still name a job itself (and does, when it is working on
17
+ * something other than its assigned task). An explicit value is never
18
+ * overwritten, including an explicit empty string, which reads as "this belongs
19
+ * to no job" — the difference between saying nothing and saying none.
20
+ */
21
+ /**
22
+ * The parameter a tool uses to carry the declaration.
23
+ *
24
+ * Two names, because the doors differ and each name is true at its own door: on
25
+ * a tool that CREATES an agreement the job becomes that row's parent, so the
26
+ * field is the parent id; on a tool that claims a row somebody else posted
27
+ * nothing is re-parented — the lineage stays with the publisher — and the job is
28
+ * only an authority. Listed together here so the runtime has one place that
29
+ * knows both, rather than a name test at each call site.
30
+ */
31
+ const MANDATE_PARAM_NAMES = ['mandateAgreementId', 'parentAgreementId'];
32
+ /** The mandate parameter this tool declares, if it declares one. */
33
+ function mandateParamOf(toolName, toolManager) {
34
+ if (!toolName)
35
+ return null;
36
+ const schema = toolManager?.getTool?.(toolName)?.schema;
37
+ const properties = schema?.function?.parameters?.properties;
38
+ if (!properties)
39
+ return null;
40
+ for (const name of MANDATE_PARAM_NAMES) {
41
+ if (Object.prototype.hasOwnProperty.call(properties, name))
42
+ return name;
43
+ }
44
+ return null;
45
+ }
46
+ /**
47
+ * Add the wake's own mandate to a tool call that declares one and left it blank.
48
+ *
49
+ * Returns the args unchanged whenever there is nothing to say: the tool takes no
50
+ * declaration, the model already made one, or this wake is not running inside a
51
+ * job at all.
52
+ */
53
+ export function withDeclaredMandate(args, toolName, toolManager, activeAgreementId) {
54
+ if (!activeAgreementId)
55
+ return args;
56
+ const param = mandateParamOf(toolName, toolManager);
57
+ if (!param)
58
+ return args;
59
+ if (args[param] !== undefined)
60
+ return args;
61
+ return { ...args, [param]: activeAgreementId };
62
+ }
@@ -1,6 +1,7 @@
1
1
  import { outcomeFromActionResult } from './buildOutcome.js';
2
2
  import { runtimeLog } from '../shared/runtimeLog.js';
3
3
  import { safeParseJSON } from '../utils/jsonExtractor.js';
4
+ import { withDeclaredMandate } from './mandateDeclaration.js';
4
5
  const MAX_TOOL_LOOP_ITERS = 20;
5
6
  const MAX_MSG_CHARS = 3500;
6
7
  /**
@@ -260,6 +261,9 @@ export async function runTurn(input) {
260
261
  args = JSON.parse(tc.function.arguments || '{}');
261
262
  }
262
263
  catch { }
264
+ // Say which job this is part of, when the tool asks and the model did
265
+ // not. The runtime knows; the model would have to be told.
266
+ args = withDeclaredMandate(args, toolName, toolManager, ctx.activeAgreementId);
263
267
  const decision = { action: matchingAction, tool: toolName, args };
264
268
  const result = await executeDecision(decision, actionDef, effWithTrace, sessionId, memory, null, addToolUsage);
265
269
  toolCallResults.push({ tc, matchingAction, result, args, actionDef });
@@ -311,6 +315,10 @@ export async function runTurn(input) {
311
315
  const decision = parsed.decision;
312
316
  const chosenAction = decision.action;
313
317
  const actionDef = actions[chosenAction];
318
+ // Same mandate declaration as the tool-call path above. Both routes reach
319
+ // the same tools, so filling it on only one would make the excuse depend on
320
+ // which shape the model happened to answer in.
321
+ decision.args = withDeclaredMandate((decision.args ?? {}), actionDef?.tool, toolManager, ctx.activeAgreementId);
314
322
  if (decision.thought)
315
323
  await emitThought(decision.thought);
316
324
  // 2-call streaming: if this is a respond action (has receiverId, no bound tool),
@@ -1,4 +1,4 @@
1
- import { type Agreement, type ClaimedKind, type ProposeAgreementData, type ProposeDirectInput, type ProposeBroadcastInput, type PublishOfferPayload, type DelegateAgreementData, type CounterAgreementData, type CreateLinkBody, 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);
@@ -25,15 +25,22 @@ export declare class AgreementService {
25
25
  * the shared flow; `kind` is the api-client ClaimedKind so this signature
26
26
  * can't drift from the flow again (a 'hand-off' addition broke the build).
27
27
  */
28
- claim(agreementId: string): Promise<{
28
+ claim(agreementId: string, opts?: ClaimOptions): Promise<{
29
29
  agreement: Agreement;
30
30
  kind: ClaimedKind;
31
31
  }>;
32
32
  /**
33
- * a link proposal is an agreement (engagementKind 'link') to one
34
- * agent; the target owner approves. Folds the old request_link tool.
33
+ * Connect with the person an agent answers for.
34
+ *
35
+ * `to` is an email or an agent id. A link is with the PERSON either way: an id
36
+ * is only a way to find its owner, and one that answers to an org is refused.
37
+ *
38
+ * The reply says only that the invitation went. Handing back the agreement for
39
+ * an id target while an address target got a bare acknowledgement told the
40
+ * caller more about one kind of target than the other, which made the id form
41
+ * a way to check who has an account.
35
42
  */
36
- requestLink(providerId: string, description?: string): Promise<Agreement>;
43
+ requestLink(to: string, description?: string): Promise<CreateLinkResult>;
37
44
  counter(agreementId: string, counter: CounterAgreementData): Promise<Agreement>;
38
45
  getStatus(agreementId: string): Promise<unknown | null>;
39
46
  list(filters?: ListAgreementsFilters): Promise<Agreement[]>;
@@ -46,14 +53,11 @@ export declare class AgreementService {
46
53
  mine(filters?: GetMyAgreementsFilters): Promise<Agreement[]>;
47
54
  get(agreementId: string): Promise<Agreement | null>;
48
55
  /**
49
- * Create a trust link — direct to one agent, or an open invite. Work
56
+ * Connect with someone — by email, by agent id, or with a share link. Work
50
57
  * agreements are created by `propose` / the marketplace claim routes, which
51
58
  * run the term normalisation and consent gates this rail never had.
52
59
  */
53
- createLink(body: CreateLinkBody): Promise<{
54
- ok: boolean;
55
- agreement: Agreement;
56
- }>;
60
+ createLink(body: CreateLinkBody): Promise<CreateLinkResult>;
57
61
  revoke(agreementId: string): Promise<{
58
62
  ok: boolean;
59
63
  agreement: Agreement;
@@ -66,6 +70,4 @@ export declare class AgreementService {
66
70
  getChats(agreementId: string): Promise<unknown[]>;
67
71
  linkArtifact(agreementId: string, artifactId: string, linkType?: ArtifactLinkType): Promise<unknown | null>;
68
72
  getArtifacts(agreementId: string): Promise<unknown[]>;
69
- linkUser(agreementId: string, userId: string, role: UserRole): Promise<unknown | null>;
70
- getUsers(agreementId: string): Promise<unknown[]>;
71
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) {
@@ -43,16 +43,22 @@ export class AgreementService {
43
43
  * the shared flow; `kind` is the api-client ClaimedKind so this signature
44
44
  * can't drift from the flow again (a 'hand-off' addition broke the build).
45
45
  */
46
- claim(agreementId) {
47
- return claimOpenAgreement(agreementId, this.creds);
46
+ claim(agreementId, opts = {}) {
47
+ return claimOpenAgreement(agreementId, this.creds, opts);
48
48
  }
49
49
  /**
50
- * a link proposal is an agreement (engagementKind 'link') to one
51
- * agent; the target owner approves. Folds the old request_link tool.
50
+ * Connect with the person an agent answers for.
51
+ *
52
+ * `to` is an email or an agent id. A link is with the PERSON either way: an id
53
+ * is only a way to find its owner, and one that answers to an org is refused.
54
+ *
55
+ * The reply says only that the invitation went. Handing back the agreement for
56
+ * an id target while an address target got a bare acknowledgement told the
57
+ * caller more about one kind of target than the other, which made the id form
58
+ * a way to check who has an account.
52
59
  */
53
- async requestLink(providerId, description) {
54
- const { agreement } = await createLink({ targetAgentId: providerId, ...(description ? { description } : {}) }, this.creds);
55
- return agreement;
60
+ async requestLink(to, description) {
61
+ return createLink({ to, ...(description ? { description } : {}) }, this.creds);
56
62
  }
57
63
  counter(agreementId, counter) {
58
64
  return counterAgreement(agreementId, counter || {}, this.creds);
@@ -76,7 +82,7 @@ export class AgreementService {
76
82
  return getAgreement(agreementId, this.creds);
77
83
  }
78
84
  /**
79
- * Create a trust link — direct to one agent, or an open invite. Work
85
+ * Connect with someone — by email, by agent id, or with a share link. Work
80
86
  * agreements are created by `propose` / the marketplace claim routes, which
81
87
  * run the term normalisation and consent gates this rail never had.
82
88
  */
@@ -101,10 +107,4 @@ export class AgreementService {
101
107
  getArtifacts(agreementId) {
102
108
  return getArtifactsForAgreement(agreementId, this.creds);
103
109
  }
104
- linkUser(agreementId, userId, role) {
105
- return linkUserToAgreement(agreementId, userId, role, this.creds);
106
- }
107
- getUsers(agreementId) {
108
- return getUsersForAgreement(agreementId, this.creds);
109
- }
110
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;
@@ -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
  }
@@ -23,7 +23,7 @@ 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';
@@ -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
@@ -66,7 +66,16 @@ cachedAgreements) {
66
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
  }
@@ -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: '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 (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 });
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
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
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', { agreementId: { type: 'string', required: true, description: 'The open agreement to claim (request / 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 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.' });
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.
@@ -60,7 +65,7 @@ export const taskSetResultTool = defineTool('task_set_result', {
60
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
- 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.' });
64
69
  // `status` names its four values here because nothing else does: the backend is
65
70
  // the only place the vocabulary was written down, and it now rejects anything
66
71
  // outside it. Every reader (the chat step badge, the SDK's own plan formatter)
@@ -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,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 {};