@ziggs-ai/api-client 0.37.19 → 0.37.21

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.37.21
6
+
7
+ - A room lane can name the agreement the wake works under: `laneUnderAgreement(chatId, agreementId)` builds it, and the lane header sends it as `X-Ziggs-Agreement` beside `X-Ziggs-Lane`. The backend takes it only for an agreement the agent has mail under in that room.
8
+ - A search row names an active hire of that agent: it says already hired and carries the agreement id and the workspace the payer bought it in. An agent with no such hire has no such field.
9
+
10
+ ## 0.37.20
11
+
12
+ - Shorter parameter text on the agreement verbs and the artifact record capability. Every rule stays.
13
+
5
14
  ## 0.37.19
6
15
 
7
16
  - Hosted-agent agreement tools take commercial terms from the same definition as the other agreement verbs, including the interface.
@@ -0,0 +1,19 @@
1
+ import { type Agreement } from '../types.js';
2
+ /** One live hire of an agent, as a search row names it. */
3
+ export interface ActiveHire {
4
+ agreementId: string;
5
+ /** The workspace the payer bought it in. Null when the row records none. */
6
+ workspace: string | null;
7
+ }
8
+ type HireRow = Agreement & {
9
+ payerLane?: string | null;
10
+ };
11
+ /**
12
+ * Active hires whose provider is this agent.
13
+ *
14
+ * A listing is not a hire: its payer is still a broadcast. A link and a
15
+ * membership are not hires either. `service` stays, because older hires
16
+ * were written as services with a named payer.
17
+ */
18
+ export declare function activeHiresForAgent(agentId: string, agreements: readonly HireRow[]): ActiveHire[];
19
+ export {};
@@ -0,0 +1,31 @@
1
+ import { AGREEMENT_ENGAGEMENT_KIND, isBroadcastTarget, } from '../types.js';
2
+ /**
3
+ * Active hires whose provider is this agent.
4
+ *
5
+ * A listing is not a hire: its payer is still a broadcast. A link and a
6
+ * membership are not hires either. `service` stays, because older hires
7
+ * were written as services with a named payer.
8
+ */
9
+ export function activeHiresForAgent(agentId, agreements) {
10
+ if (!agentId)
11
+ return [];
12
+ const hires = [];
13
+ for (const row of agreements) {
14
+ if (row.status !== 'active')
15
+ continue;
16
+ if (row.engagementKind !== AGREEMENT_ENGAGEMENT_KIND.HIRE &&
17
+ row.engagementKind !== AGREEMENT_ENGAGEMENT_KIND.SERVICE) {
18
+ continue;
19
+ }
20
+ if (row.parties?.provider?.actor !== agentId)
21
+ continue;
22
+ const payer = row.parties?.payer?.principal;
23
+ if (typeof payer === 'string' && isBroadcastTarget(payer))
24
+ continue;
25
+ hires.push({
26
+ agreementId: row.agreementId,
27
+ workspace: row.payerLane ?? null,
28
+ });
29
+ }
30
+ return hires;
31
+ }
@@ -33,12 +33,12 @@ export const AGREEMENT_TERMS_PARAMS = {
33
33
  },
34
34
  price: {
35
35
  type: 'number',
36
- description: 'Price in POINTS, as an integer of hundredths — 500 means ϟ5.00. Recording a price here does not itself move any. Convert BOTH ways or you are off by 100x: a human who says "ϟ2" means 200, and quoting 500 back as "ϟ500" is the same mistake inverted.',
36
+ description: 'Points in hundredths: 500 is ϟ5.00, and a person\'s "ϟ2" is 200. Convert both ways. Recording a price moves no points.',
37
37
  },
38
38
  billing: {
39
39
  type: 'string',
40
40
  enum: ['total', 'per_task'],
41
- description: 'How price reads. total (default) is one price for the whole engagement, settled at fulfilment; per_task is a rate settled as each task completes (standing agreements only, and the default for a hire).',
41
+ description: 'total (default): one price, settled at fulfilment. per_task: a rate per completed task (standing agreements; the default for a hire).',
42
42
  },
43
43
  engagementKind: {
44
44
  type: 'string',
@@ -48,14 +48,14 @@ export const AGREEMENT_TERMS_PARAMS = {
48
48
  lifecycle: {
49
49
  type: 'string',
50
50
  enum: ['open', 'time-bound', 'count-bound'],
51
- description: 'open (default) is standing: no expiry, unlimited tasks. Set this only when the engagement should end on its own.',
51
+ description: 'open (default): no expiry, unlimited tasks. Set it only when this should end on its own.',
52
52
  },
53
53
  expiresAt: {
54
54
  type: 'string',
55
55
  description: 'ISO timestamp after which this can no longer be acted on.',
56
56
  },
57
57
  interactionMode: { type: 'string', enum: ['messages', 'tasks', 'both'], description: 'Accepted interface. Task-only services need no chat or negotiation.' },
58
- conversationAllowance: { type: 'number', description: 'Shared incoming-message limit across covered rooms. A helper hired inside this job draws on this limit and has none of its own. Omit for unlimited messages on a new agreement; on a counter, omission preserves the existing term.' },
58
+ conversationAllowance: { type: 'number', description: 'Incoming messages allowed across covered rooms, shared with any helper hired under this job. Omit for unlimited; on a counter, omit to keep the current limit.' },
59
59
  maxExecutions: {
60
60
  type: 'number',
61
61
  description: 'End the engagement after this many tasks.',
@@ -90,7 +90,7 @@ function engagementKindFrom(args) {
90
90
  /** The declared job governs the work; hiring consent uses grants or agreement approval. */
91
91
  const MANDATE_PARAM = {
92
92
  type: 'string',
93
- description: "The active agreement whose work this is part of — the job you are doing. It becomes this engagement's parent, so it ends when the job ends, but does not itself authorize a hire. Agent-initiated hires require a covering approval-authority grant or approval on the agreement. Name only an agreement you are actually a party to; the server checks, and a wrong name simply earns nothing. Leave it out for work that belongs to no job. It never names who you act for — the server reads that off the room or contract that woke you.",
93
+ description: 'The job you are doing, if this work is part of it: an active agreement you are a party to. This work ends when that job ends. It does not authorize the hire by itself, and never says who you act for.',
94
94
  };
95
95
  /** Pull the declared mandate off a validated arg bag, as the wire field. */
96
96
  function mandateFrom(args) {
@@ -140,7 +140,7 @@ const COUNTERPARTY_PARAM = {
140
140
  const CHAT_PARAM = {
141
141
  type: 'string',
142
142
  required: false,
143
- description: 'The room this is proposed in, when there is one — the counterparty can read and discuss the terms there. Omit it for an agreement between parties with no conversation; the proposal still reaches them, it just has no room to be discussed in.',
143
+ description: 'The room to discuss the terms in, if any. Without one the proposal still reaches them.',
144
144
  };
145
145
  /** Marketplace follow-up: a published listing is claimed, never countered. */
146
146
  function publishedNext(env) {
@@ -162,7 +162,7 @@ export const agreementBuyCapability = {
162
162
  chatId: CHAT_PARAM,
163
163
  ...AGREEMENT_TERMS_PARAMS,
164
164
  mandateAgreementId: MANDATE_PARAM,
165
- parentAgreementId: { type: 'string', description: 'The job this work is part of; ending that job ends this work. Never a link: a link is who can reach whom, so paid work cannot be placed under one. This placement does not grant spending consent.' },
165
+ parentAgreementId: { type: 'string', description: 'The job this work is part of; ending it ends this work. Not a link, and no spending consent.' },
166
166
  },
167
167
  needsAgentId: true,
168
168
  handler: async (args, env) => {
@@ -193,7 +193,7 @@ export const agreementBidCapability = {
193
193
  chatId: CHAT_PARAM,
194
194
  ...AGREEMENT_TERMS_PARAMS,
195
195
  mandateAgreementId: MANDATE_PARAM,
196
- parentAgreementId: { type: 'string', description: 'The job this work is part of; ending that job ends this work. Never a link: a link is who can reach whom, so paid work cannot be placed under one. This placement does not grant spending consent.' },
196
+ parentAgreementId: { type: 'string', description: 'The job this work is part of; ending it ends this work. Not a link, and no spending consent.' },
197
197
  },
198
198
  needsAgentId: true,
199
199
  handler: async (args, env) => {
@@ -232,7 +232,7 @@ export const agreementBrokerCapability = {
232
232
  chatId: CHAT_PARAM,
233
233
  ...AGREEMENT_TERMS_PARAMS,
234
234
  mandateAgreementId: MANDATE_PARAM,
235
- parentAgreementId: { type: 'string', description: 'The job this work is part of; ending that job ends this work. Never a link: a link is who can reach whom, so paid work cannot be placed under one. This placement does not grant spending consent.' },
235
+ parentAgreementId: { type: 'string', description: 'The job this work is part of; ending it ends this work. Not a link, and no spending consent.' },
236
236
  },
237
237
  needsAgentId: true,
238
238
  handler: async (args, env) => {
@@ -262,7 +262,7 @@ export const agreementRequestCapability = {
262
262
  ...AGREEMENT_TERMS_PARAMS,
263
263
  match: MATCH_PARAM,
264
264
  mandateAgreementId: MANDATE_PARAM,
265
- parentAgreementId: { type: 'string', description: 'The job this work is part of; ending that job ends this work. Never a link: a link is who can reach whom, so paid work cannot be placed under one. This placement does not grant spending consent.' },
265
+ parentAgreementId: { type: 'string', description: 'The job this work is part of; ending it ends this work. Not a link, and no spending consent.' },
266
266
  },
267
267
  needsAgentId: true,
268
268
  handler: async (args, env) => {
@@ -210,8 +210,7 @@ export const recordArtifactCapability = {
210
210
  },
211
211
  idempotencyKey: {
212
212
  type: 'string',
213
- description: 'Optional dedup key: a redelivered record with the same key no-ops and returns the original artifact. Derive it deterministically (e.g. from the source event + step) — not a random value — so a crash-replay reproduces it. ' +
214
- 'A task-bound result artifact is one deliverable per task: recordReport keys it `task-result:<taskId>`, so a retry after a partial failure reuses that row. A later revision of the same task still returns the original artifact — change the key if you mean a new deliverable.',
213
+ description: 'Dedup key: the same key returns the original artifact instead of a new one. Derive it from the event and step, never at random. A new revision needs a new key.',
215
214
  },
216
215
  externalRef: {
217
216
  type: 'object',
@@ -1,4 +1,6 @@
1
1
  import { AgentSearchClient } from '../http/AgentSearchClient.js';
2
+ import { getMyAgreements } from '../http/AgreementClient.js';
3
+ import { activeHiresForAgent } from './activeHire.js';
2
4
  import { RESOURCE_HIDDEN_MESSAGE } from './context.js';
3
5
  import { fullCreds } from './types.js';
4
6
  export const agentSearchCapability = {
@@ -6,7 +8,7 @@ export const agentSearchCapability = {
6
8
  names: { sdk: 'agent_search', mcp: 'ziggs_agent_search' },
7
9
  title: 'Search for agents',
8
10
  descriptions: {
9
- sdk: 'Search for agents by capability, name, or description. Returns ranked results with relevance scores. A keyword/natural-language query searches the published store, your org\'s own agents and your own agents, and it matches your teammates by name: a teammate comes back under `people` as a person to message (chat_open with their userId), never as a list of their agents. Someone else\'s agent is reached through the person it answers for, inside your org and outside it: you message the person, which of their agents answers is theirs to decide, and their agents become known to you by turning up in rooms. A link works the same way: reach a linked person through the link itself (link_list gives their addressable peer id, then chat_open). Passing an EXACT agent id resolves that one agent when you can reach it. Each row carries `doors` — its engagement doors: doors.listingAgreementId is a live listing to CLAIM (agreement_claim — the default, one hop from here) and doors.acceptsProposals says whether a direct proposal would even be accepted (most published agents are claim-only). Use returned agentId in grant/issue tools — do not guess ids.',
11
+ sdk: 'Search for agents by capability, name, or description. Returns ranked results with relevance scores. A keyword/natural-language query searches the published store, your org\'s own agents and your own agents, and it matches your teammates by name: a teammate comes back under `people` as a person to message (chat_open with their userId), never as a list of their agents. Someone else\'s agent is reached through the person it answers for, inside your org and outside it: you message the person, which of their agents answers is theirs to decide, and their agents become known to you by turning up in rooms. A link works the same way: reach a linked person through the link itself (link_list gives their addressable peer id, then chat_open). Passing an EXACT agent id resolves that one agent when you can reach it. Each row carries `doors` — its engagement doors: doors.listingAgreementId is a live listing to CLAIM (agreement_claim — the default, one hop from here) and doors.acceptsProposals says whether a direct proposal would even be accepted (most published agents are claim-only). Use returned agentId in grant/issue tools — do not guess ids. When the caller already hires an agent, that row says already hired and names the agreement id and workspace.',
10
12
  },
11
13
  annotation: 'read-only',
12
14
  params: {
@@ -42,7 +44,9 @@ export const agentSearchCapability = {
42
44
  peopleNote: `These are teammates whose name matched. Message the person with ${chatOpen} participantId=<userId>; which of their agents answers is theirs to decide.`,
43
45
  }
44
46
  : {};
45
- if (!result.agents?.length) {
47
+ const hired = await hireNotes(result.agents ?? [], env);
48
+ const agents = (result.agents ?? []).map((agent) => withHire(agent, hired));
49
+ if (!agents.length) {
46
50
  // a bare {count: 0} reads as "discovery is down" to LLM
47
51
  // callers — say what was searched and how to recover instead.
48
52
  return {
@@ -67,7 +71,12 @@ export const agentSearchCapability = {
67
71
  `not their agents. Take their peer id from ${linkList} and open a chat with the person instead.`,
68
72
  };
69
73
  }
70
- return { count: result.agents.length, agents: result.agents, ...messageThem };
74
+ return {
75
+ count: agents.length,
76
+ agents,
77
+ ...(hired.checked ? {} : { hireCheck: 'unavailable' }),
78
+ ...messageThem,
79
+ };
71
80
  },
72
81
  sdkOptions: { isGenericFallback: true },
73
82
  };
@@ -106,3 +115,27 @@ export const DISCOVERY_CAPABILITIES = [
106
115
  agentSearchCapability,
107
116
  agentGetCapability,
108
117
  ];
118
+ function withHire(agent, hired) {
119
+ const hires = hired.byAgent.get(agent.agentId);
120
+ if (!hires?.length)
121
+ return agent;
122
+ return { ...agent, alreadyHired: 'already hired', hires };
123
+ }
124
+ /** One reachable-agreement read for the whole result. A failure is said, so a row with no hire field was checked. */
125
+ async function hireNotes(agents, env) {
126
+ const byAgent = new Map();
127
+ if (agents.length === 0)
128
+ return { checked: true, byAgent };
129
+ try {
130
+ const agreements = await getMyAgreements({}, fullCreds(env));
131
+ for (const agent of agents) {
132
+ const hires = activeHiresForAgent(agent.agentId, agreements);
133
+ if (hires.length)
134
+ byAgent.set(agent.agentId, hires);
135
+ }
136
+ return { checked: true, byAgent };
137
+ }
138
+ catch {
139
+ return { checked: false, byAgent };
140
+ }
141
+ }
@@ -59,7 +59,7 @@ const BRIEFS = {
59
59
  },
60
60
  // Agreements
61
61
  ziggs_agreement_list: {
62
- does: 'Lists agreements you are a party to. scope reachable adds every agreement your grants can read.',
62
+ does: 'Lists the agreements you can reach, including hires. scope mine keeps only agreements you are a party to.',
63
63
  },
64
64
  ziggs_agreement_get: {
65
65
  does: 'Reads one agreement: parties, terms, status, and approvalAction (who may decide a pending slot).',
@@ -223,7 +223,7 @@ const BRIEFS = {
223
223
  },
224
224
  // Agents, links and introductions
225
225
  ziggs_agent_search: {
226
- does: 'Finds agents in the store, your workspaces and your own; matching teammates come back as people. Each row says whether to claim a listing or propose.',
226
+ does: 'Finds agents in the store, your workspaces and your own; teammates come back as people. A row you already hire says already hired and names that agreement and workspace.',
227
227
  },
228
228
  ziggs_agent_get: {
229
229
  does: 'Reads one agent’s profile by exact id, with how to hire it: listingAgreementId to claim, acceptsProposals for a direct proposal.',
@@ -33,5 +33,5 @@ export { TelemetryClient } from './TelemetryClient.js';
33
33
  export { InboxClient } from './InboxClient.js';
34
34
  export { planInboxAck, planPartialInboxAck, inboxAckAllowed, inboxAckHandledIds, } from '../inboxAck.js';
35
35
  export type { InboxAckDecision, InboxAckStep, InboxAckEnvelope, PlanInboxAckOptions, } from '../inboxAck.js';
36
- export { WAKE_LANE_HEADER, laneHeader, buildOperatorHeaders, buildCredsHeaders, } from './operatorHeaders.js';
36
+ export { WAKE_LANE_HEADER, WAKE_AGREEMENT_HEADER, laneHeader, laneUnderAgreement, buildOperatorHeaders, buildCredsHeaders, } from './operatorHeaders.js';
37
37
  export { backendToolPick, type ToolPickOption, type ToolPickAnswer, type PickTool } from './ToolPickClient.js';
@@ -25,5 +25,5 @@ export { planInboxAck, planPartialInboxAck, inboxAckAllowed, inboxAckHandledIds,
25
25
  // The lane header, built in one place — see operatorHeaders.ts. Exported
26
26
  // because the MCP transports in agent-sdk and ziggs-mcp send it too, and a
27
27
  // copy of a header nobody notices missing goes stale unseen.
28
- export { WAKE_LANE_HEADER, laneHeader, buildOperatorHeaders, buildCredsHeaders, } from './operatorHeaders.js';
28
+ export { WAKE_LANE_HEADER, WAKE_AGREEMENT_HEADER, laneHeader, laneUnderAgreement, buildOperatorHeaders, buildCredsHeaders, } from './operatorHeaders.js';
29
29
  export { backendToolPick } from './ToolPickClient.js';
@@ -24,8 +24,22 @@ export declare function assistantRequestHeaders(): Record<string, string>;
24
24
  /** The header the backend's wake fence reads (`access/wake-fence.ts`). */
25
25
  export declare const WAKE_LANE_HEADER = "X-Ziggs-Lane";
26
26
  /**
27
- * The lane header, or nothing. `laneId` is a chat id, or `agrn-<agreementId>`
28
- * for a task with no origin chat.
27
+ * The header naming the agreement a room wake works under. The backend takes
28
+ * it only for an agreement the agent has mail under in that room, and reads
29
+ * it off that mail when it is absent.
30
+ */
31
+ export declare const WAKE_AGREEMENT_HEADER = "X-Ziggs-Agreement";
32
+ /**
33
+ * A room lane that also names the agreement the wake works under:
34
+ * `<chatId>@<agreementId>`. Every client already carries the lane to
35
+ * {@link laneHeader}, so the agreement travels with it without a second
36
+ * parameter on each of them. An `agrn-` lane already is its agreement.
37
+ */
38
+ export declare function laneUnderAgreement(laneId: string, agreementId?: string | null): string;
39
+ /**
40
+ * The lane headers, or nothing. `laneId` is a chat id, `agrn-<agreementId>`
41
+ * for a task with no origin chat, or a chat id with the agreement the wake
42
+ * works under (`<chatId>@<agreementId>`, see {@link laneUnderAgreement}).
29
43
  */
30
44
  export declare function laneHeader(laneId?: string): Record<string, string>;
31
45
  export declare function buildOperatorHeaders(operatorKey: string, agentId?: string, extra?: Record<string, string>,
@@ -37,11 +37,37 @@ export function assistantRequestHeaders() {
37
37
  /** The header the backend's wake fence reads (`access/wake-fence.ts`). */
38
38
  export const WAKE_LANE_HEADER = 'X-Ziggs-Lane';
39
39
  /**
40
- * The lane header, or nothing. `laneId` is a chat id, or `agrn-<agreementId>`
41
- * for a task with no origin chat.
40
+ * The header naming the agreement a room wake works under. The backend takes
41
+ * it only for an agreement the agent has mail under in that room, and reads
42
+ * it off that mail when it is absent.
43
+ */
44
+ export const WAKE_AGREEMENT_HEADER = 'X-Ziggs-Agreement';
45
+ /**
46
+ * A room lane that also names the agreement the wake works under:
47
+ * `<chatId>@<agreementId>`. Every client already carries the lane to
48
+ * {@link laneHeader}, so the agreement travels with it without a second
49
+ * parameter on each of them. An `agrn-` lane already is its agreement.
50
+ */
51
+ export function laneUnderAgreement(laneId, agreementId) {
52
+ if (!agreementId || laneId.startsWith('agrn-') || laneId.includes('@'))
53
+ return laneId;
54
+ return `${laneId}@${agreementId}`;
55
+ }
56
+ /**
57
+ * The lane headers, or nothing. `laneId` is a chat id, `agrn-<agreementId>`
58
+ * for a task with no origin chat, or a chat id with the agreement the wake
59
+ * works under (`<chatId>@<agreementId>`, see {@link laneUnderAgreement}).
42
60
  */
43
61
  export function laneHeader(laneId) {
44
- return laneId ? { [WAKE_LANE_HEADER]: laneId } : {};
62
+ if (!laneId)
63
+ return {};
64
+ const at = laneId.indexOf('@');
65
+ if (at <= 0)
66
+ return { [WAKE_LANE_HEADER]: laneId };
67
+ return {
68
+ [WAKE_LANE_HEADER]: laneId.slice(0, at),
69
+ [WAKE_AGREEMENT_HEADER]: laneId.slice(at + 1),
70
+ };
45
71
  }
46
72
  export function buildOperatorHeaders(operatorKey, agentId, extra,
47
73
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/api-client",
3
- "version": "0.37.19",
3
+ "version": "0.37.21",
4
4
  "description": "HTTP and WebSocket client for the Ziggs backend API",
5
5
  "homepage": "https://ziggsai.com/docs",
6
6
  "bugs": {