@ziggs-ai/agent-sdk 0.1.4 → 0.1.6

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 (49) hide show
  1. package/README.md +7 -5
  2. package/package.json +2 -2
  3. package/src/AgentHost.ts +165 -12
  4. package/src/adapters/OpenAIAdapter.ts +21 -0
  5. package/src/agent/Agent.ts +5 -2
  6. package/src/cognition/validateContext.ts +1 -1
  7. package/src/context/batch.ts +3 -3
  8. package/src/context/classifyEnvelope.ts +2 -2
  9. package/src/context/routingLabels.ts +1 -1
  10. package/src/formatters/AgreementFormatter.ts +5 -5
  11. package/src/formatters/HistoryFormatter.ts +3 -3
  12. package/src/index.ts +25 -4
  13. package/src/ingress/normalizeIncoming.ts +50 -7
  14. package/src/pricing/fleetDefaults.ts +218 -0
  15. package/src/pricing/fleetEvalFree.ts +24 -0
  16. package/src/pricing/fleetFreeTierA.gen.ts +12 -0
  17. package/src/pricing/fleetTierByAgentId.gen.ts +1022 -0
  18. package/src/runtime/AgentMachine.ts +68 -2
  19. package/src/runtime/PromptBuilder.ts +25 -23
  20. package/src/runtime/buildOutcome.ts +33 -3
  21. package/src/runtime/defaults.ts +3 -0
  22. package/src/runtime/runTurn.ts +115 -61
  23. package/src/runtime/validateWorkflow.ts +16 -0
  24. package/src/server/EventQueue.ts +14 -0
  25. package/src/server/InboxCatchUp.ts +251 -0
  26. package/src/server/SeenMessages.ts +27 -0
  27. package/src/server/ZiggsEffectHandler.ts +82 -8
  28. package/src/server/agreements/AgreementService.ts +7 -1
  29. package/src/server/createHealthServer.ts +79 -2
  30. package/src/server/runLauncher.ts +40 -25
  31. package/src/server/tasks/TaskService.ts +4 -5
  32. package/src/server/tasks/index.ts +0 -3
  33. package/src/server/telemetryIngest.ts +91 -0
  34. package/src/server/tools/index.ts +46 -0
  35. package/src/server/{tasks → tools/tier1}/protocolRunner.ts +52 -20
  36. package/src/server/{tasks → tools/tier1}/protocolTools.ts +6 -3
  37. package/src/server/tools/tier2/connectionTools.ts +75 -0
  38. package/src/server/tools/tier2/contextTools.ts +74 -0
  39. package/src/server/tools/tier2/discoveryTools.ts +34 -0
  40. package/src/server/tools/tier2/marketplaceTools.ts +25 -0
  41. package/src/server/{tasks → tools/tier2}/paymentTools.ts +74 -37
  42. package/src/server/ziggsconnect/ZiggsConnectClient.ts +126 -0
  43. package/src/server/ziggscontext/ZiggsContextClient.ts +137 -0
  44. package/src/server/ziggspay/ZiggsPayClient.ts +12 -12
  45. package/src/shared/types.ts +0 -2
  46. package/src/tools/index.ts +2 -0
  47. package/src/tools/recordReport.ts +82 -0
  48. package/src/types.ts +47 -8
  49. package/src/tasks/taskCore.ts +0 -139
@@ -1,5 +1,5 @@
1
- import { defineTool, type ToolDefinition } from '../../tools/defineTool.js';
2
- import { ZiggsPayClient } from '../ziggspay/ZiggsPayClient.js';
1
+ import { defineTool, type ToolDefinition } from '../../../tools/defineTool.js';
2
+ import { ZiggsPayClient } from '../../ziggspay/ZiggsPayClient.js';
3
3
 
4
4
  type Ctx = Record<string, unknown>;
5
5
  type AnyObj = Record<string, unknown>;
@@ -35,17 +35,18 @@ export const paymentBalanceTool: ToolDefinition = defineTool('payment_balance',
35
35
  try { return await clientFor(ctx as Ctx).balance(); }
36
36
  catch (e) { rethrowWithContext(e, 'Failed to get balance'); }
37
37
  },
38
+ { description: "Check the caller's current wallet balance and available balance (total minus active holds). Use before a transfer to confirm sufficient funds." },
38
39
  );
39
40
 
40
41
  export const paymentTransferTool: ToolDefinition = defineTool('payment_transfer',
41
- { toWalletId: { type: 'string', required: true }, amount: { type: 'number', required: true }, description: 'string', idempotencyKey: 'string', capabilityTokenId: 'string' },
42
+ { toWalletId: { type: 'string', required: true }, amount: { type: 'number', required: true }, description: 'string', idempotencyKey: 'string', paymentGrantId: 'string' },
42
43
  async (args, ctx) => {
43
44
  if (!args['toWalletId']) throw new Error('toWalletId is required');
44
45
  if (!args['amount'] || (args['amount'] as number) <= 0) throw new Error('amount must be positive');
45
46
  const client = clientFor(ctx as Ctx);
46
47
  let result: AnyObj;
47
48
  try {
48
- result = await client.transfer({ to: args['toWalletId'] as string, amount: Math.round(args['amount'] as number), description: (args['description'] as string) || 'Agent-initiated transfer', idempotencyKey: args['idempotencyKey'] as string | undefined, capabilityTokenId: args['capabilityTokenId'] as string | undefined }) as AnyObj;
49
+ result = await client.transfer({ to: args['toWalletId'] as string, amount: Math.round(args['amount'] as number), description: (args['description'] as string) || 'Agent-initiated transfer', idempotencyKey: args['idempotencyKey'] as string | undefined, paymentGrantId: args['paymentGrantId'] as string | undefined }) as AnyObj;
49
50
  } catch (e) { rethrowWithContext(e, 'Transfer failed'); }
50
51
  if (result!['status'] === 'approval_required') {
51
52
  return { status: 'approval_required', approvalId: result!['approvalId'] || null, expiresAt: result!['expiresAt'] || null, reason: result!['reason'] || null, amount: args['amount'], toWalletId: result!['toWalletId'],
@@ -76,6 +77,7 @@ export const paymentResolveWalletTool: ToolDefinition = defineTool('payment_reso
76
77
  return { walletId: wallet?.['walletId'] || null, ownerId: wallet?.['ownerId'] || null, currency: wallet?.['currency'] || 'pez', status: wallet?.['status'] || null };
77
78
  } catch (e) { rethrowWithContext(e, 'Failed to resolve wallet'); }
78
79
  },
80
+ { description: 'Look up a walletId by userId or agentId. Use before a transfer when you only know the recipient by their platform ID.' },
79
81
  );
80
82
 
81
83
  export const paymentHoldTool: ToolDefinition = defineTool('payment_hold',
@@ -87,6 +89,7 @@ export const paymentHoldTool: ToolDefinition = defineTool('payment_hold',
87
89
  return { status: 'held', transactionId: (result['transaction'] as AnyObj | undefined)?.['transactionId'] || null, amount: args['amount'] };
88
90
  } catch (e) { rethrowWithContext(e, 'Hold failed'); }
89
91
  },
92
+ { description: 'Pre-authorize (escrow) funds without moving them. Use to reserve payment at agreement formation; release with payment_release once work is complete or to refund if work is cancelled.' },
90
93
  );
91
94
 
92
95
  export const paymentReleaseTool: ToolDefinition = defineTool('payment_release',
@@ -100,57 +103,91 @@ export const paymentReleaseTool: ToolDefinition = defineTool('payment_release',
100
103
  return { status: args['action'] === 'complete' ? 'settled' : 'refunded', transactionId: (result['transaction'] as AnyObj | undefined)?.['transactionId'] || null, holdId: args['holdId'], action: args['action'] };
101
104
  } catch (e) { rethrowWithContext(e, 'Release failed'); }
102
105
  },
106
+ { description: "Settle or refund an escrow hold. Use action='complete' to transfer held funds to toWalletId (work done), or action='refund' to return funds to the sender (work cancelled)." },
103
107
  );
104
108
 
109
+ const issueGrantHandler = async (args: AnyObj, ctx: Ctx) => {
110
+ if (!args['holderId']) throw new Error('holderId is required');
111
+ try {
112
+ const caveats = buildCaveats(args);
113
+ const result = await clientFor(ctx).issueGrant({ holderId: args['holderId'] as string, caveats }) as AnyObj;
114
+ const grant = result['grant'] as AnyObj | undefined;
115
+ return { grantId: grant?.['grantId'] || null, holderId: grant?.['holderId'] || args['holderId'], caveats: grant?.['caveats'] || caveats, expiresAt: grant?.['expiresAt'] || null };
116
+ } catch (e) { rethrowWithContext(e, 'Failed to issue payment grant'); }
117
+ };
118
+
119
+ export const paymentIssueGrantTool: ToolDefinition = defineTool('payment_issue_grant',
120
+ { holderId: { type: 'string', required: true }, maxAmount: 'number', dailyBudget: 'number', allowedRecipients: ['string'], expiresInSeconds: 'number' },
121
+ issueGrantHandler,
122
+ );
123
+
124
+ /** @deprecated Use payment_issue_grant */
105
125
  export const paymentIssueTokenTool: ToolDefinition = defineTool('payment_issue_token',
106
126
  { holderId: { type: 'string', required: true }, maxAmount: 'number', dailyBudget: 'number', allowedRecipients: ['string'], expiresInSeconds: 'number' },
107
- async (args, ctx) => {
108
- if (!args['holderId']) throw new Error('holderId is required');
109
- try {
110
- const caveats = buildCaveats(args);
111
- const result = await clientFor(ctx as Ctx).issueToken({ holderId: args['holderId'] as string, caveats }) as AnyObj;
112
- const token = result['token'] as AnyObj | undefined;
113
- return { tokenId: token?.['tokenId'] || null, holderId: token?.['holderId'] || args['holderId'], caveats: token?.['caveats'] || caveats, expiresAt: token?.['expiresAt'] || null };
114
- } catch (e) { rethrowWithContext(e, 'Failed to issue capability token'); }
115
- },
127
+ issueGrantHandler,
116
128
  );
117
129
 
130
+ const attenuateGrantHandler = async (args: AnyObj, ctx: Ctx) => {
131
+ const grantId = (args['grantId'] || args['tokenId']) as string;
132
+ if (!grantId) throw new Error('grantId is required');
133
+ if (!args['holderId']) throw new Error('holderId is required');
134
+ try {
135
+ const caveats = buildCaveats(args);
136
+ const result = await clientFor(ctx).attenuateGrant({ grantId, holderId: args['holderId'] as string, caveats }) as AnyObj;
137
+ const grant = result['grant'] as AnyObj | undefined;
138
+ return { grantId: grant?.['grantId'] || null, parentGrantId: grant?.['parentGrantId'] || grantId, holderId: grant?.['holderId'] || args['holderId'], caveats: grant?.['caveats'] || caveats, expiresAt: grant?.['expiresAt'] || null };
139
+ } catch (e) { rethrowWithContext(e, 'Failed to attenuate payment grant'); }
140
+ };
141
+
142
+ export const paymentAttenuateGrantTool: ToolDefinition = defineTool('payment_attenuate_grant',
143
+ { grantId: { type: 'string', required: true }, holderId: { type: 'string', required: true }, maxAmount: 'number', dailyBudget: 'number', allowedRecipients: ['string'], expiresInSeconds: 'number' },
144
+ attenuateGrantHandler,
145
+ );
146
+
147
+ /** @deprecated Use payment_attenuate_grant */
118
148
  export const paymentAttenuateTokenTool: ToolDefinition = defineTool('payment_attenuate_token',
119
149
  { tokenId: { type: 'string', required: true }, holderId: { type: 'string', required: true }, maxAmount: 'number', dailyBudget: 'number', allowedRecipients: ['string'], expiresInSeconds: 'number' },
120
- async (args, ctx) => {
121
- if (!args['tokenId']) throw new Error('tokenId is required');
122
- if (!args['holderId']) throw new Error('holderId is required');
123
- try {
124
- const caveats = buildCaveats(args);
125
- const result = await clientFor(ctx as Ctx).attenuateToken({ tokenId: args['tokenId'] as string, holderId: args['holderId'] as string, caveats }) as AnyObj;
126
- const token = result['token'] as AnyObj | undefined;
127
- return { tokenId: token?.['tokenId'] || null, parentTokenId: token?.['parentTokenId'] || args['tokenId'], holderId: token?.['holderId'] || args['holderId'], caveats: token?.['caveats'] || caveats, expiresAt: token?.['expiresAt'] || null };
128
- } catch (e) { rethrowWithContext(e, 'Failed to attenuate capability token'); }
129
- },
150
+ attenuateGrantHandler,
130
151
  );
131
152
 
153
+ const revokeGrantHandler = async (args: AnyObj, ctx: Ctx) => {
154
+ const grantId = (args['grantId'] || args['tokenId']) as string;
155
+ if (!grantId) throw new Error('grantId is required');
156
+ try {
157
+ const result = await clientFor(ctx).revokeGrant(grantId) as AnyObj;
158
+ return { status: 'revoked', grantId, revoked: result['revoked'] ?? null };
159
+ } catch (e) { rethrowWithContext(e, 'Failed to revoke payment grant'); }
160
+ };
161
+
162
+ export const paymentRevokeGrantTool: ToolDefinition = defineTool('payment_revoke_grant',
163
+ { grantId: { type: 'string', required: true } },
164
+ revokeGrantHandler,
165
+ );
166
+
167
+ /** @deprecated Use payment_revoke_grant */
132
168
  export const paymentRevokeTokenTool: ToolDefinition = defineTool('payment_revoke_token',
133
169
  { tokenId: { type: 'string', required: true } },
134
- async (args, ctx) => {
135
- if (!args['tokenId']) throw new Error('tokenId is required');
136
- try {
137
- const result = await clientFor(ctx as Ctx).revokeToken(args['tokenId'] as string) as AnyObj;
138
- return { status: 'revoked', tokenId: args['tokenId'], revoked: result['revoked'] ?? null };
139
- } catch (e) { rethrowWithContext(e, 'Failed to revoke capability token'); }
140
- },
170
+ revokeGrantHandler,
141
171
  );
142
172
 
173
+ const listGrantsHandler = async (_args: AnyObj, ctx: Ctx) => {
174
+ try {
175
+ const result = await clientFor(ctx).listGrants();
176
+ return { grants: result || [] };
177
+ } catch (e) { rethrowWithContext(e, 'Failed to list payment grants'); }
178
+ };
179
+
180
+ export const paymentListGrantsTool: ToolDefinition = defineTool('payment_list_grants', {},
181
+ listGrantsHandler,
182
+ );
183
+
184
+ /** @deprecated Use payment_list_grants */
143
185
  export const paymentListTokensTool: ToolDefinition = defineTool('payment_list_tokens', {},
144
- async (_args, ctx) => {
145
- try {
146
- const result = await clientFor(ctx as Ctx).listTokens();
147
- return { tokens: result || [] };
148
- } catch (e) { rethrowWithContext(e, 'Failed to list capability tokens'); }
149
- },
186
+ listGrantsHandler,
150
187
  );
151
188
 
152
189
  export const PAYMENT_TOOLS: ToolDefinition[] = [
153
190
  paymentBalanceTool, paymentTransferTool, paymentWaitForApprovalTool,
154
191
  paymentHoldTool, paymentReleaseTool, paymentResolveWalletTool,
155
- paymentIssueTokenTool, paymentAttenuateTokenTool, paymentRevokeTokenTool, paymentListTokensTool,
192
+ paymentIssueGrantTool, paymentAttenuateGrantTool, paymentRevokeGrantTool, paymentListGrantsTool,
156
193
  ];
@@ -0,0 +1,126 @@
1
+ import 'dotenv/config';
2
+
3
+ function getDefaultBaseUrl(): string {
4
+ return process.env.BACKEND_URL || process.env.ZIGGS_BACKEND_URL || 'http://localhost:3000';
5
+ }
6
+
7
+ function parseError(text: string, fallback: string): string {
8
+ if (!text) return fallback;
9
+ try {
10
+ const parsed = JSON.parse(text) as Record<string, unknown>;
11
+ return (parsed['message'] as string) || (parsed['error'] as string) || text;
12
+ } catch {
13
+ return text;
14
+ }
15
+ }
16
+
17
+ interface ConnectError extends Error {
18
+ status?: number;
19
+ body?: string;
20
+ }
21
+
22
+ export interface ZiggsConnectClientOptions {
23
+ operatorKey: string;
24
+ actAsAgentId?: string;
25
+ baseUrl?: string;
26
+ }
27
+
28
+ export interface ProxyParams {
29
+ connectionId: string;
30
+ grantId: string;
31
+ action: string;
32
+ payload?: unknown;
33
+ }
34
+
35
+ export class ZiggsConnectClient {
36
+ private operatorKey: string;
37
+ private actAsAgentId: string | null;
38
+ private baseUrl: string;
39
+
40
+ constructor({ operatorKey, actAsAgentId, baseUrl }: ZiggsConnectClientOptions) {
41
+ if (!operatorKey || typeof operatorKey !== 'string') {
42
+ throw new Error('ZiggsConnectClient requires an operatorKey');
43
+ }
44
+ this.operatorKey = operatorKey;
45
+ this.actAsAgentId = actAsAgentId || null;
46
+ this.baseUrl = baseUrl || getDefaultBaseUrl();
47
+ }
48
+
49
+ /**
50
+ * Present a ConnectionGrant to the broker and perform a provider action.
51
+ * Returns the provider result — never the raw OAuth token.
52
+ */
53
+ async proxy({ connectionId, grantId, action, payload }: ProxyParams): Promise<unknown> {
54
+ if (!connectionId) throw new Error('proxy: connectionId is required');
55
+ if (!grantId) throw new Error('proxy: grantId is required');
56
+ if (!action) throw new Error('proxy: action is required');
57
+ if (!this.actAsAgentId) {
58
+ throw new Error(
59
+ 'proxy: actAsAgentId is required — connection broker calls must impersonate the grant holder agent',
60
+ );
61
+ }
62
+ const res = (await this._post(`/connections/${encodeURIComponent(connectionId)}/proxy`, {
63
+ grantId,
64
+ action,
65
+ payload: payload ?? {},
66
+ })) as Record<string, unknown>;
67
+ return res['result'] ?? res;
68
+ }
69
+
70
+ /**
71
+ * List ConnectionGrants held by this agent for a connection.
72
+ * Requires actAsAgentId; filters server rows to holderId === agent.
73
+ */
74
+ async listGrants({ connectionId }: { connectionId: string }): Promise<unknown[]> {
75
+ if (!connectionId) throw new Error('listGrants: connectionId is required');
76
+ if (!this.actAsAgentId) {
77
+ throw new Error(
78
+ 'listGrants: actAsAgentId is required — grants are scoped to the impersonated agent',
79
+ );
80
+ }
81
+ const res = (await this._get(
82
+ `/connections/${encodeURIComponent(connectionId)}/grants`,
83
+ )) as Record<string, unknown>;
84
+ const grants = (res['grants'] as Array<Record<string, unknown>>) || [];
85
+ return grants.filter((g) => g['holderId'] === this.actAsAgentId);
86
+ }
87
+
88
+ private _buildHeaders(extra: Record<string, string> = {}): Record<string, string> {
89
+ const h: Record<string, string> = { Authorization: `Bearer ${this.operatorKey}`, ...extra };
90
+ if (this.actAsAgentId) h['X-Agent-Id'] = this.actAsAgentId;
91
+ return h;
92
+ }
93
+
94
+ private async _request(method: string, path: string, body: unknown): Promise<unknown> {
95
+ const init: RequestInit = {
96
+ method,
97
+ headers: this._buildHeaders(
98
+ body !== undefined ? { 'content-type': 'application/json' } : {},
99
+ ),
100
+ };
101
+ if (body !== undefined) init.body = JSON.stringify(body);
102
+ const response = await fetch(`${this.baseUrl}${path}`, init);
103
+ const text = await response.text();
104
+ if (!response.ok) {
105
+ const err = new Error(parseError(text, `HTTP ${response.status}`)) as ConnectError;
106
+ err.status = response.status;
107
+ err.body = text;
108
+ throw err;
109
+ }
110
+ return text ? JSON.parse(text) : null;
111
+ }
112
+
113
+ private _get(path: string): Promise<unknown> {
114
+ return this._request('GET', path, undefined);
115
+ }
116
+
117
+ private _post(path: string, body: unknown): Promise<unknown> {
118
+ return this._request('POST', path, body ?? {});
119
+ }
120
+ }
121
+
122
+ export function createZiggsConnectClient(
123
+ opts: ZiggsConnectClientOptions,
124
+ ): ZiggsConnectClient {
125
+ return new ZiggsConnectClient(opts);
126
+ }
@@ -0,0 +1,137 @@
1
+ import 'dotenv/config';
2
+
3
+ function getDefaultBaseUrl(): string {
4
+ return process.env.BACKEND_URL || process.env.ZIGGS_BACKEND_URL || 'http://localhost:3000';
5
+ }
6
+
7
+ function parseError(text: string, fallback: string): string {
8
+ if (!text) return fallback;
9
+ try {
10
+ const parsed = JSON.parse(text) as Record<string, unknown>;
11
+ return (parsed['message'] as string) || (parsed['error'] as string) || text;
12
+ } catch {
13
+ return text;
14
+ }
15
+ }
16
+
17
+ interface ContextError extends Error {
18
+ status?: number;
19
+ body?: string;
20
+ }
21
+
22
+ export interface ZiggsContextClientOptions {
23
+ operatorKey: string;
24
+ actAsAgentId?: string;
25
+ baseUrl?: string;
26
+ }
27
+
28
+ export interface ContextReachDescriptor {
29
+ grantId: string;
30
+ scope: { kind: 'chat' | 'agreement' | 'org'; id: string };
31
+ temporal: 'from-now' | 'from-start';
32
+ watermarkAt: string;
33
+ expiresAt: string | null;
34
+ parentGrantId: string | null;
35
+ createdAt: string;
36
+ }
37
+
38
+ export interface DelegateContextGrantParams {
39
+ parentGrantId: string;
40
+ holderId: string;
41
+ scope: { kind: 'chat' | 'agreement' | 'org'; id: string };
42
+ temporal: 'from-now' | 'from-start';
43
+ expiresAt?: string | null;
44
+ watermarkAt?: string;
45
+ }
46
+
47
+ /**
48
+ * HTTP client for context grants (ZIG-413).
49
+ * Presentation model (signed bytes vs grant id) is intentionally not baked in here.
50
+ */
51
+ export class ZiggsContextClient {
52
+ private operatorKey: string;
53
+ private actAsAgentId: string | null;
54
+ private baseUrl: string;
55
+
56
+ constructor({ operatorKey, actAsAgentId, baseUrl }: ZiggsContextClientOptions) {
57
+ if (!operatorKey || typeof operatorKey !== 'string') {
58
+ throw new Error('ZiggsContextClient requires an operatorKey');
59
+ }
60
+ this.operatorKey = operatorKey;
61
+ this.actAsAgentId = actAsAgentId || null;
62
+ this.baseUrl = baseUrl || getDefaultBaseUrl();
63
+ }
64
+
65
+ /** ZIG-412: scope descriptors for what this agent can reach (no content). */
66
+ async discover(): Promise<ContextReachDescriptor[]> {
67
+ this.requireActAsAgent('discover');
68
+ const res = (await this._get('/context/discovery')) as Record<string, unknown>;
69
+ return (res['reach'] as ContextReachDescriptor[]) || [];
70
+ }
71
+
72
+ /** List persisted grant rows held by this agent (may include signedBytes). */
73
+ async listGrants(): Promise<unknown[]> {
74
+ this.requireActAsAgent('listGrants');
75
+ const res = (await this._get('/context/grants')) as Record<string, unknown>;
76
+ const grants = (res['grants'] as Array<Record<string, unknown>>) || [];
77
+ return grants.filter((g) => g['holderId'] === this.actAsAgentId);
78
+ }
79
+
80
+ /** Holder delegates a narrower child grant. */
81
+ async delegate(params: DelegateContextGrantParams): Promise<unknown> {
82
+ this.requireActAsAgent('delegate');
83
+ if (!params.parentGrantId) throw new Error('delegate: parentGrantId is required');
84
+ if (!params.holderId) throw new Error('delegate: holderId is required');
85
+ if (!params.scope?.kind || !params.scope?.id) {
86
+ throw new Error('delegate: scope { kind, id } is required');
87
+ }
88
+ return this._post(`/context/grants/${encodeURIComponent(params.parentGrantId)}/delegate`, {
89
+ holderId: params.holderId,
90
+ scope: params.scope,
91
+ temporal: params.temporal,
92
+ expiresAt: params.expiresAt,
93
+ watermarkAt: params.watermarkAt,
94
+ });
95
+ }
96
+
97
+ private requireActAsAgent(method: string): void {
98
+ if (!this.actAsAgentId) {
99
+ throw new Error(
100
+ `${method}: actAsAgentId is required — context grant calls must impersonate the grant holder agent`,
101
+ );
102
+ }
103
+ }
104
+
105
+ private _buildHeaders(extra: Record<string, string> = {}): Record<string, string> {
106
+ const h: Record<string, string> = { Authorization: `Bearer ${this.operatorKey}`, ...extra };
107
+ if (this.actAsAgentId) h['X-Agent-Id'] = this.actAsAgentId;
108
+ return h;
109
+ }
110
+
111
+ private async _request(method: string, path: string, body: unknown): Promise<unknown> {
112
+ const init: RequestInit = {
113
+ method,
114
+ headers: this._buildHeaders(
115
+ body !== undefined ? { 'content-type': 'application/json' } : {},
116
+ ),
117
+ };
118
+ if (body !== undefined) init.body = JSON.stringify(body);
119
+ const res = await fetch(`${this.baseUrl}${path}`, init);
120
+ const text = await res.text();
121
+ if (!res.ok) {
122
+ const err = new Error(parseError(text, `${method} ${path} failed (${res.status})`)) as ContextError;
123
+ err.status = res.status;
124
+ err.body = text;
125
+ throw err;
126
+ }
127
+ return text ? JSON.parse(text) : {};
128
+ }
129
+
130
+ private _get(path: string): Promise<unknown> {
131
+ return this._request('GET', path, undefined);
132
+ }
133
+
134
+ private _post(path: string, body: unknown): Promise<unknown> {
135
+ return this._request('POST', path, body);
136
+ }
137
+ }
@@ -51,17 +51,17 @@ export class ZiggsPayClient {
51
51
  return res['wallet'] || null;
52
52
  }
53
53
 
54
- async transfer({ to, amount, idempotencyKey, description, capabilityTokenId }: { to: string; amount: number; idempotencyKey?: string; description?: string; capabilityTokenId?: string }): Promise<unknown> {
54
+ async transfer({ to, amount, idempotencyKey, description, paymentGrantId }: { to: string; amount: number; idempotencyKey?: string; description?: string; paymentGrantId?: string }): Promise<unknown> {
55
55
  if (!to) throw new Error('transfer: `to` is required');
56
56
  if (!(Number.isInteger(amount) && amount > 0)) throw new Error('transfer: `amount` must be a positive integer (cents)');
57
- if (this.actAsAgentId && !capabilityTokenId) throw new Error('transfer: capabilityTokenId is required for agent-impersonated transfers. The wallet owner must have issued a capability token to this agentId.');
57
+ if (this.actAsAgentId && !paymentGrantId) throw new Error('transfer: paymentGrantId is required for agent-impersonated transfers. The wallet owner must have issued a payment grant to this agentId.');
58
58
  let toWalletId = to;
59
59
  if (!to.startsWith('wal_')) {
60
60
  const w = await this.resolve(to.startsWith('agent_') ? { agentId: to } : { userId: to }) as Record<string, unknown> | null;
61
61
  if (!w?.['walletId']) throw new Error(`transfer: could not resolve wallet for "${to}"`);
62
62
  toWalletId = w['walletId'] as string;
63
63
  }
64
- const result = await this._post('/payments/transfer', { toWalletId, amount, idempotencyKey: idempotencyKey || randomIdempotencyKey('xfer'), description, capabilityTokenId }) as Record<string, unknown>;
64
+ const result = await this._post('/payments/transfer', { toWalletId, amount, idempotencyKey: idempotencyKey || randomIdempotencyKey('xfer'), description, paymentGrantId }) as Record<string, unknown>;
65
65
  if (result?.['status'] === 'approval_required') {
66
66
  const approval = (result['approval'] as Record<string, unknown>) || {};
67
67
  return { status: 'approval_required', approvalId: approval['approvalId'] || null, expiresAt: approval['expiresAt'] || null, reason: approval['reason'] || 'Amount exceeds auto-approve threshold', toWalletId, amount };
@@ -87,21 +87,21 @@ export class ZiggsPayClient {
87
87
  return this._get('/payments/history' + (q.toString() ? `?${q}` : ''));
88
88
  }
89
89
 
90
- async issueToken({ holderId, caveats }: { holderId: string; caveats?: unknown }): Promise<unknown> {
91
- return this._post('/payments/tokens', { holderId, caveats });
90
+ async issueGrant({ holderId, caveats }: { holderId: string; caveats?: unknown }): Promise<unknown> {
91
+ return this._post('/payments/grants', { holderId, caveats });
92
92
  }
93
93
 
94
- async attenuateToken({ tokenId, holderId, caveats }: { tokenId: string; holderId: string; caveats?: unknown }): Promise<unknown> {
95
- return this._post(`/payments/tokens/${tokenId}/attenuate`, { holderId, caveats });
94
+ async attenuateGrant({ grantId, holderId, caveats }: { grantId: string; holderId: string; caveats?: unknown }): Promise<unknown> {
95
+ return this._post(`/payments/grants/${grantId}/attenuate`, { holderId, caveats });
96
96
  }
97
97
 
98
- async revokeToken(tokenId: string): Promise<unknown> {
99
- return this._request('DELETE', `/payments/tokens/${tokenId}`, undefined);
98
+ async revokeGrant(grantId: string): Promise<unknown> {
99
+ return this._request('DELETE', `/payments/grants/${grantId}`, undefined);
100
100
  }
101
101
 
102
- async listTokens(): Promise<unknown[]> {
103
- const res = await this._get('/payments/tokens') as Record<string, unknown>;
104
- return (res['tokens'] as unknown[]) || [];
102
+ async listGrants(): Promise<unknown[]> {
103
+ const res = await this._get('/payments/grants') as Record<string, unknown>;
104
+ return (res['grants'] as unknown[]) || [];
105
105
  }
106
106
 
107
107
  async createTopUpIntent({ amount, description, currency }: { amount?: number; description?: string; currency?: string } = {}): Promise<unknown> {
@@ -21,8 +21,6 @@ export const ContentTypes = {
21
21
 
22
22
  export type ContentType = (typeof ContentTypes)[keyof typeof ContentTypes];
23
23
 
24
- export { TaskState, TERMINAL_STATES, OPEN_AGREEMENT_TARGET } from '../tasks/taskCore.js';
25
-
26
24
  export const ContextProperties = {
27
25
  HISTORY: 'history',
28
26
  AGREEMENTS: 'agreements',
@@ -1,5 +1,7 @@
1
1
  export { ToolManager } from './ToolManager.js';
2
2
  export { ToolProvider } from './ToolProvider.js';
3
3
  export { defineTool } from './defineTool.js';
4
+ export { recordReport } from './recordReport.js';
5
+ export type { RecordReportInput, RecordReportResult, TaskReport } from './recordReport.js';
4
6
  // Pure protocol-tool name registry (no handlers, no services).
5
7
  export { PROTOCOL_TOOL_NAMES, PROTOCOL_TOOL_TO_OPERATION, mapProtocolToolToOperation, isProtocolToolName } from '../tasks/protocolRegistry.js';
@@ -0,0 +1,82 @@
1
+ import {
2
+ ArtifactsClient,
3
+ updateTaskState,
4
+ type Creds,
5
+ } from '@ziggs-ai/api-client';
6
+
7
+ /**
8
+ * The canonical report shape (B1 convention — ZIG-561).
9
+ * Matches Task.result as set by ziggs_set_task_result.
10
+ */
11
+ export interface TaskReport {
12
+ summary: string;
13
+ status?: string;
14
+ links?: string[];
15
+ }
16
+
17
+ export interface RecordReportInput {
18
+ creds: Creds;
19
+ taskId: string;
20
+ report: TaskReport;
21
+ /**
22
+ * Primary artifact scope — required when `deliverable` is provided.
23
+ * Pass exactly one of chatId or agreementId.
24
+ */
25
+ chatId?: string;
26
+ agreementId?: string;
27
+ /**
28
+ * Heavy deliverable text (document, diff, data).
29
+ * When present, writes a `content_type=result` artifact bound to the task
30
+ * alongside setting Task.result.
31
+ */
32
+ deliverable?: string;
33
+ }
34
+
35
+ export interface RecordReportResult {
36
+ taskId: string;
37
+ artifactId?: string;
38
+ }
39
+
40
+ /**
41
+ * Record a conformant report for a completed task (B2 — ZIG-562).
42
+ *
43
+ * Always sets Task.result via updateTaskState (the canonical done payload).
44
+ * When `deliverable` is provided, also writes a task-bound result artifact
45
+ * via ArtifactsClient so it is retrievable as `via=task:<taskId>`.
46
+ *
47
+ * Implements the B1 convention (Agents/REPORTING.md).
48
+ */
49
+ export async function recordReport(
50
+ input: RecordReportInput,
51
+ ): Promise<RecordReportResult> {
52
+ const { creds, taskId, report, chatId, agreementId, deliverable } = input;
53
+
54
+ if (deliverable && !chatId && !agreementId) {
55
+ throw new Error(
56
+ 'recordReport: chatId or agreementId is required when deliverable is provided',
57
+ );
58
+ }
59
+
60
+ await updateTaskState(
61
+ taskId,
62
+ 'completed',
63
+ { result: { summary: report.summary, status: report.status ?? 'ok', links: report.links ?? [] } },
64
+ creds,
65
+ );
66
+
67
+ if (!deliverable) {
68
+ return { taskId };
69
+ }
70
+
71
+ const artifactsClient = new ArtifactsClient(creds.operatorKey, creds.agentId);
72
+ await artifactsClient.write({
73
+ text: deliverable,
74
+ content_type: 'result',
75
+ taskId,
76
+ chatId,
77
+ agreementId,
78
+ visibility: 'chat',
79
+ });
80
+
81
+ return { taskId };
82
+ }