@ziggs-ai/agent-sdk 0.1.3 → 0.1.4
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/README.md +1 -1
- package/package.json +9 -4
- package/src/AgentHost.ts +342 -0
- package/src/adapters/OpenAIAdapter.ts +125 -0
- package/src/agent/Agent.ts +98 -0
- package/src/cognition/validateContext.ts +95 -0
- package/src/context/applyEffects.ts +80 -0
- package/src/context/batch.ts +17 -0
- package/src/context/classifyEnvelope.ts +38 -0
- package/src/context/routingLabels.ts +46 -0
- package/src/defineAgent.ts +62 -0
- package/src/formatters/AgreementFormatter.ts +111 -0
- package/src/formatters/HistoryFormatter.ts +166 -0
- package/src/formatters/index.ts +2 -0
- package/src/index.ts +86 -0
- package/src/ingress/normalizeIncoming.ts +119 -0
- package/src/memory/MemoryStore.ts +104 -0
- package/src/runtime/AgentMachine.ts +298 -0
- package/src/runtime/PromptBuilder.ts +461 -0
- package/src/runtime/buildOutcome.ts +488 -0
- package/src/runtime/defaults.ts +72 -0
- package/src/runtime/runTurn.ts +637 -0
- package/src/runtime/validateWorkflow.ts +165 -0
- package/src/server/ConnectionPool.ts +155 -0
- package/src/server/EventQueue.ts +119 -0
- package/src/server/OutboxBuffer.ts +90 -0
- package/src/server/ZiggsEffectHandler.ts +335 -0
- package/src/server/agreements/AgreementService.ts +111 -0
- package/src/server/createHealthServer.ts +8 -0
- package/src/server/proactive/ProactiveTrigger.ts +83 -0
- package/src/server/runLauncher.ts +131 -0
- package/src/server/tasks/TaskService.ts +111 -0
- package/src/server/tasks/index.ts +4 -0
- package/src/server/tasks/paymentTools.ts +156 -0
- package/src/server/tasks/protocolRunner.ts +101 -0
- package/src/server/tasks/protocolTools.ts +96 -0
- package/src/server/ziggspay/ZiggsPayClient.ts +193 -0
- package/src/shared/ids.ts +3 -0
- package/src/shared/runtimeLog.ts +72 -0
- package/src/shared/types.ts +31 -0
- package/src/tasks/protocolRegistry.ts +25 -0
- package/src/tasks/taskCore.ts +139 -0
- package/src/tools/ToolManager.ts +95 -0
- package/src/tools/{ToolProvider.js → ToolProvider.ts} +5 -15
- package/src/tools/defineTool.ts +90 -0
- package/src/tools/index.ts +5 -0
- package/src/types.ts +368 -0
- package/src/utils/jsonExtractor.ts +100 -0
- package/src/ConnectionPool.js +0 -133
- package/src/adapters/OpenAIAdapter.js +0 -73
- package/src/agent/Agent.js +0 -121
- package/src/agent/EventQueue.js +0 -68
- package/src/agent/OutboxBuffer.js +0 -62
- package/src/cognition/PromptBuilder.js +0 -312
- package/src/cognition/resolveActionTool.js +0 -12
- package/src/cognition/runTurn.js +0 -578
- package/src/context/applyEffects.js +0 -133
- package/src/context/batch.js +0 -25
- package/src/context/classifyEnvelope.js +0 -82
- package/src/context/routingLabels.js +0 -54
- package/src/createHealthServer.js +0 -28
- package/src/formatters/HistoryFormatter.js +0 -257
- package/src/formatters/TaskFormatter.js +0 -180
- package/src/formatters/index.js +0 -9
- package/src/index.js +0 -76
- package/src/ingress/normalizeIncoming.js +0 -70
- package/src/runLauncher.js +0 -159
- package/src/shared/ids.js +0 -7
- package/src/shared/types.js +0 -86
- package/src/tasks/TaskService.js +0 -247
- package/src/tasks/index.js +0 -9
- package/src/tasks/taskCore.js +0 -229
- package/src/tasks/taskProtocolRegistry.js +0 -22
- package/src/tasks/taskProtocolRunner.js +0 -107
- package/src/tasks/taskProtocolTools.js +0 -87
- package/src/tools/ToolManager.js +0 -79
- package/src/tools/defineTool.js +0 -82
- package/src/tools/index.js +0 -11
- package/src/utils/jsonExtractor.js +0 -139
- package/src/workflow/AgentMachine.js +0 -250
- package/src/workflow/WorkflowRuntime.js +0 -63
- package/src/workflow/dsl.js +0 -287
- package/src/workflow/motifs.js +0 -435
- package/src/ziggs/runtime.js +0 -192
- /package/src/adapters/{index.js → index.ts} +0 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createTask, getTask, getActiveTasksForAgent, getActiveTasksForChat, getSubtasks,
|
|
3
|
+
updateTaskState, cancelTask,
|
|
4
|
+
claimLedgerTask, reportTask, setTaskSatisfaction, countTasks,
|
|
5
|
+
type Creds, type Task,
|
|
6
|
+
} from '@ziggs-ai/api-client';
|
|
7
|
+
import type { TaskWithFlags, TaskStateValue } from '../../tasks/taskCore.js';
|
|
8
|
+
import type { CountTasksFilters } from '@ziggs-ai/api-client';
|
|
9
|
+
import { runtimeLog } from '../../shared/runtimeLog.js';
|
|
10
|
+
|
|
11
|
+
function safeJsonStringify(value: unknown): unknown {
|
|
12
|
+
if (value === null || value === undefined) return value;
|
|
13
|
+
if (typeof value === 'string') return value;
|
|
14
|
+
if (typeof value !== 'object') return String(value);
|
|
15
|
+
const seen = new WeakSet();
|
|
16
|
+
try {
|
|
17
|
+
return JSON.stringify(value, (_key, val) => {
|
|
18
|
+
if (typeof val === 'object' && val !== null) {
|
|
19
|
+
if (seen.has(val)) return '[Circular]';
|
|
20
|
+
seen.add(val);
|
|
21
|
+
}
|
|
22
|
+
return val;
|
|
23
|
+
}, 2);
|
|
24
|
+
} catch {
|
|
25
|
+
return String(value);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class TaskService {
|
|
30
|
+
private creds: Creds;
|
|
31
|
+
|
|
32
|
+
constructor(operatorKey: string, agentId: string) {
|
|
33
|
+
if (!operatorKey) throw new Error('TaskService: operatorKey is required');
|
|
34
|
+
if (!agentId) throw new Error('TaskService: agentId is required (operator-token impersonation)');
|
|
35
|
+
this.creds = { operatorKey, agentId };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async getTask(taskId: string): Promise<Task> {
|
|
39
|
+
return getTask(taskId, this.creds);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async getActiveTasksForAgent(agentId: string): Promise<Task[]> {
|
|
43
|
+
return getActiveTasksForAgent(agentId, this.creds);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async getActiveTasksForChat(chatId: string): Promise<Task[]> {
|
|
47
|
+
return getActiveTasksForChat(chatId, this.creds);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async getSubtasks(parentTaskId: string): Promise<Task[]> {
|
|
51
|
+
return getSubtasks(parentTaskId, this.creds);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async spawnUnderAgreement(agreementId: string, taskData: { description: string; parentTaskId?: string; plan?: unknown; planReviewTiming?: string; requireMidWorkPlanAck?: boolean }): Promise<Task> {
|
|
55
|
+
if (!agreementId) throw new Error('TaskService.spawnUnderAgreement: agreementId is required');
|
|
56
|
+
try {
|
|
57
|
+
const result = await createTask({ ...taskData, agreementId } as Parameters<typeof createTask>[0], this.creds);
|
|
58
|
+
runtimeLog.info('TaskService', `Task spawned under agreement ${agreementId}: ${result?.taskId || 'unknown'}`);
|
|
59
|
+
return result;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
runtimeLog.error('TaskService', `Task spawn failed: ${(error as Error).message}`);
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async updateState(taskId: string, state: TaskStateValue, result?: unknown): Promise<Task> {
|
|
67
|
+
try {
|
|
68
|
+
const data = result !== undefined ? { result: safeJsonStringify(result) } : {};
|
|
69
|
+
const updated = await updateTaskState(taskId, state, data, this.creds);
|
|
70
|
+
runtimeLog.info('TaskService', `Task state: ${taskId} → ${state}`);
|
|
71
|
+
return updated;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
runtimeLog.error('TaskService', `Task state update failed: ${(error as Error).message}`);
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async cancel(taskId: string): Promise<Task> {
|
|
79
|
+
try {
|
|
80
|
+
const updated = await cancelTask(taskId, this.creds);
|
|
81
|
+
runtimeLog.info('TaskService', `Task cancelled: ${taskId}`);
|
|
82
|
+
return updated;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
runtimeLog.error('TaskService', `Task cancel failed: ${(error as Error).message}`);
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async claimLedgerTask(taskId: string): Promise<TaskWithFlags> {
|
|
90
|
+
try {
|
|
91
|
+
const task = await claimLedgerTask(taskId, this.creds);
|
|
92
|
+
runtimeLog.info('TaskService', `[ledger] Claimed task ${taskId}`);
|
|
93
|
+
return task as unknown as TaskWithFlags;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
runtimeLog.warn('TaskService', `[ledger] Claim failed for ${taskId}: ${(error as Error).message}`);
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async report(taskId: string, message = ''): Promise<Task> {
|
|
101
|
+
return reportTask(taskId, message, this.creds);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async setSatisfaction(taskId: string, satisfaction: 'positive' | 'negative'): Promise<Task> {
|
|
105
|
+
return setTaskSatisfaction(taskId, satisfaction, this.creds);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async count(filters: CountTasksFilters = {}): Promise<number> {
|
|
109
|
+
return countTasks(filters, this.creds);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { TaskService } from './TaskService.js';
|
|
2
|
+
export { PROTOCOL_TOOLS } from './protocolTools.js';
|
|
3
|
+
export { PROTOCOL_TOOL_NAMES, PROTOCOL_TOOL_TO_OPERATION, mapProtocolToolToOperation, isProtocolToolName } from '../../tasks/protocolRegistry.js';
|
|
4
|
+
export { dispatchProtocolOp } from './protocolRunner.js';
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { defineTool, type ToolDefinition } from '../../tools/defineTool.js';
|
|
2
|
+
import { ZiggsPayClient } from '../ziggspay/ZiggsPayClient.js';
|
|
3
|
+
|
|
4
|
+
type Ctx = Record<string, unknown>;
|
|
5
|
+
type AnyObj = Record<string, unknown>;
|
|
6
|
+
|
|
7
|
+
function clientFor(ctx: Ctx): ZiggsPayClient {
|
|
8
|
+
const operatorKey = ctx?.['operatorKey'] as string | undefined;
|
|
9
|
+
if (!operatorKey) throw new Error('operatorKey missing from tool context');
|
|
10
|
+
return new ZiggsPayClient({ operatorKey, actAsAgentId: (ctx?.['agentId'] as string) || undefined });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface PayError extends Error { status?: number; body?: string; }
|
|
14
|
+
|
|
15
|
+
function rethrowWithContext(error: unknown, prefix: string): never {
|
|
16
|
+
const e = error as PayError;
|
|
17
|
+
const wrapped = new Error(`${prefix}: ${e.message}`) as PayError;
|
|
18
|
+
if (e.status !== undefined) wrapped.status = e.status;
|
|
19
|
+
if (e.body !== undefined) wrapped.body = e.body;
|
|
20
|
+
(wrapped as unknown as AnyObj)['cause'] = error;
|
|
21
|
+
throw wrapped;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function buildCaveats(args: AnyObj): AnyObj[] {
|
|
25
|
+
const caveats: AnyObj[] = [];
|
|
26
|
+
if (args['maxAmount'] != null) caveats.push({ type: 'max_amount', value: args['maxAmount'] });
|
|
27
|
+
if (args['dailyBudget'] != null) caveats.push({ type: 'daily_budget', value: args['dailyBudget'] });
|
|
28
|
+
if (args['allowedRecipients'] != null) caveats.push({ type: 'allowed_recipients', value: args['allowedRecipients'] });
|
|
29
|
+
if (args['expiresInSeconds'] != null) caveats.push({ type: 'expires_at', value: Date.now() + (args['expiresInSeconds'] as number) * 1000 });
|
|
30
|
+
return caveats;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const paymentBalanceTool: ToolDefinition = defineTool('payment_balance', {},
|
|
34
|
+
async (_args, ctx) => {
|
|
35
|
+
try { return await clientFor(ctx as Ctx).balance(); }
|
|
36
|
+
catch (e) { rethrowWithContext(e, 'Failed to get balance'); }
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
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
|
+
async (args, ctx) => {
|
|
43
|
+
if (!args['toWalletId']) throw new Error('toWalletId is required');
|
|
44
|
+
if (!args['amount'] || (args['amount'] as number) <= 0) throw new Error('amount must be positive');
|
|
45
|
+
const client = clientFor(ctx as Ctx);
|
|
46
|
+
let result: AnyObj;
|
|
47
|
+
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
|
+
} catch (e) { rethrowWithContext(e, 'Transfer failed'); }
|
|
50
|
+
if (result!['status'] === 'approval_required') {
|
|
51
|
+
return { status: 'approval_required', approvalId: result!['approvalId'] || null, expiresAt: result!['expiresAt'] || null, reason: result!['reason'] || null, amount: args['amount'], toWalletId: result!['toWalletId'],
|
|
52
|
+
message: 'Transfer paused: the wallet owner must approve this amount.',
|
|
53
|
+
next_actions: [{ tool: 'payment_wait_for_approval', when: 'You expect a quick decision (≤2 min) and can wait inline.', args: { approvalId: result!['approvalId'], timeoutMs: 120000 } }, { tool: 'task_update_plan_step', when: 'You want to abandon the transfer and route around it.' }] };
|
|
54
|
+
}
|
|
55
|
+
return { status: 'transferred', transactionId: result!['transactionId'] || null, amount: args['amount'], toWalletId: result!['toWalletId'] };
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
export const paymentWaitForApprovalTool: ToolDefinition = defineTool('payment_wait_for_approval',
|
|
60
|
+
{ approvalId: { type: 'string', required: true }, timeoutMs: 'number', pollMs: 'number' },
|
|
61
|
+
async (args, ctx) => {
|
|
62
|
+
if (!args['approvalId']) throw new Error('approvalId is required');
|
|
63
|
+
try {
|
|
64
|
+
const result = await clientFor(ctx as Ctx).waitForApproval(args['approvalId'] as string, { timeoutMs: args['timeoutMs'] as number | undefined, pollMs: args['pollMs'] as number | undefined }) as AnyObj;
|
|
65
|
+
return { status: result['status'], approvalId: args['approvalId'], transactionId: result['transactionId'] || null, approval: result['approval'] || null };
|
|
66
|
+
} catch (e) { rethrowWithContext(e, 'wait_for_approval failed'); }
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
export const paymentResolveWalletTool: ToolDefinition = defineTool('payment_resolve_wallet',
|
|
71
|
+
{ userId: 'string', agentId: 'string' },
|
|
72
|
+
async (args, ctx) => {
|
|
73
|
+
if (!args['userId'] && !args['agentId']) throw new Error('Provide userId or agentId to resolve a wallet');
|
|
74
|
+
try {
|
|
75
|
+
const wallet = await clientFor(ctx as Ctx).resolve({ userId: args['userId'] as string | undefined, agentId: args['agentId'] as string | undefined }) as AnyObj | null;
|
|
76
|
+
return { walletId: wallet?.['walletId'] || null, ownerId: wallet?.['ownerId'] || null, currency: wallet?.['currency'] || 'pez', status: wallet?.['status'] || null };
|
|
77
|
+
} catch (e) { rethrowWithContext(e, 'Failed to resolve wallet'); }
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
export const paymentHoldTool: ToolDefinition = defineTool('payment_hold',
|
|
82
|
+
{ amount: { type: 'number', required: true }, description: 'string', idempotencyKey: 'string' },
|
|
83
|
+
async (args, ctx) => {
|
|
84
|
+
if (!args['amount'] || (args['amount'] as number) <= 0) throw new Error('amount must be positive');
|
|
85
|
+
try {
|
|
86
|
+
const result = await clientFor(ctx as Ctx).hold({ amount: Math.round(args['amount'] as number), description: (args['description'] as string) || 'Agent escrow hold', idempotencyKey: args['idempotencyKey'] as string | undefined }) as AnyObj;
|
|
87
|
+
return { status: 'held', transactionId: (result['transaction'] as AnyObj | undefined)?.['transactionId'] || null, amount: args['amount'] };
|
|
88
|
+
} catch (e) { rethrowWithContext(e, 'Hold failed'); }
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
export const paymentReleaseTool: ToolDefinition = defineTool('payment_release',
|
|
93
|
+
{ holdId: { type: 'string', required: true }, action: { type: 'string', required: true }, toWalletId: 'string', idempotencyKey: 'string' },
|
|
94
|
+
async (args, ctx) => {
|
|
95
|
+
if (!args['holdId']) throw new Error('holdId is required');
|
|
96
|
+
if (args['action'] !== 'complete' && args['action'] !== 'refund') throw new Error("action must be 'complete' or 'refund'");
|
|
97
|
+
if (args['action'] === 'complete' && !args['toWalletId']) throw new Error('toWalletId is required when action=complete');
|
|
98
|
+
try {
|
|
99
|
+
const result = await clientFor(ctx as Ctx).release({ holdId: args['holdId'] as string, action: args['action'] as string, toWalletId: args['toWalletId'] as string | undefined, idempotencyKey: args['idempotencyKey'] as string | undefined }) as AnyObj;
|
|
100
|
+
return { status: args['action'] === 'complete' ? 'settled' : 'refunded', transactionId: (result['transaction'] as AnyObj | undefined)?.['transactionId'] || null, holdId: args['holdId'], action: args['action'] };
|
|
101
|
+
} catch (e) { rethrowWithContext(e, 'Release failed'); }
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
export const paymentIssueTokenTool: ToolDefinition = defineTool('payment_issue_token',
|
|
106
|
+
{ 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
|
+
},
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
export const paymentAttenuateTokenTool: ToolDefinition = defineTool('payment_attenuate_token',
|
|
119
|
+
{ 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
|
+
},
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
export const paymentRevokeTokenTool: ToolDefinition = defineTool('payment_revoke_token',
|
|
133
|
+
{ 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
|
+
},
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
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
|
+
},
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
export const PAYMENT_TOOLS: ToolDefinition[] = [
|
|
153
|
+
paymentBalanceTool, paymentTransferTool, paymentWaitForApprovalTool,
|
|
154
|
+
paymentHoldTool, paymentReleaseTool, paymentResolveWalletTool,
|
|
155
|
+
paymentIssueTokenTool, paymentAttenuateTokenTool, paymentRevokeTokenTool, paymentListTokensTool,
|
|
156
|
+
];
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { normalizeState } from '../../tasks/taskCore.js';
|
|
2
|
+
import type { AgreementService } from '../agreements/AgreementService.js';
|
|
3
|
+
import type { TaskService } from './TaskService.js';
|
|
4
|
+
|
|
5
|
+
interface Services {
|
|
6
|
+
agreementService: AgreementService;
|
|
7
|
+
taskService: TaskService;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ProtocolPayload {
|
|
11
|
+
operation?: string;
|
|
12
|
+
agreementId?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
proposedTo?: string;
|
|
15
|
+
executorId?: string;
|
|
16
|
+
action?: 'approve' | 'reject';
|
|
17
|
+
price?: number;
|
|
18
|
+
lifecycle?: string;
|
|
19
|
+
expiresAt?: string;
|
|
20
|
+
maxExecutions?: number;
|
|
21
|
+
agreementDescription?: string;
|
|
22
|
+
parentAgreementId?: string;
|
|
23
|
+
parentTaskId?: string;
|
|
24
|
+
taskId?: string;
|
|
25
|
+
status?: string;
|
|
26
|
+
result?: unknown;
|
|
27
|
+
plan?: unknown;
|
|
28
|
+
planReviewTiming?: string;
|
|
29
|
+
requireMidWorkPlanAck?: boolean;
|
|
30
|
+
engagementKind?: 'hire' | 'service';
|
|
31
|
+
subtasks?: (string | { description?: string })[];
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function dispatchProtocolOp(
|
|
36
|
+
services: Services,
|
|
37
|
+
payload: ProtocolPayload,
|
|
38
|
+
chatId: string | null,
|
|
39
|
+
agents: unknown[] = [],
|
|
40
|
+
): Promise<unknown> {
|
|
41
|
+
if (!payload?.operation) return null;
|
|
42
|
+
const { operation } = payload;
|
|
43
|
+
const { agreementService, taskService } = services;
|
|
44
|
+
|
|
45
|
+
switch (operation) {
|
|
46
|
+
case 'agreement-propose':
|
|
47
|
+
return proposeAgreement(services, payload, chatId, agents);
|
|
48
|
+
case 'agreement-subcontract':
|
|
49
|
+
return subcontractAgreement(services, payload, chatId, agents);
|
|
50
|
+
case 'agreement-respond':
|
|
51
|
+
if (!payload.agreementId || !payload.action) return null;
|
|
52
|
+
return agreementService.respond(payload.agreementId, payload.action);
|
|
53
|
+
case 'agreement-counter-proposal':
|
|
54
|
+
if (!payload.agreementId) return null;
|
|
55
|
+
return agreementService.counter(payload.agreementId, {
|
|
56
|
+
price: payload.price,
|
|
57
|
+
agreementDescription: payload.agreementDescription,
|
|
58
|
+
expiresAt: payload.expiresAt,
|
|
59
|
+
lifecycle: payload.lifecycle,
|
|
60
|
+
maxExecutions: payload.maxExecutions,
|
|
61
|
+
description: payload.description,
|
|
62
|
+
plan: payload.plan,
|
|
63
|
+
planReviewTiming: payload.planReviewTiming as import('@ziggs-ai/api-client').PlanReviewTiming | undefined,
|
|
64
|
+
requireMidWorkPlanAck: payload.requireMidWorkPlanAck,
|
|
65
|
+
});
|
|
66
|
+
case 'agreement-check-proposal':
|
|
67
|
+
if (!payload.agreementId) return null;
|
|
68
|
+
return agreementService.getStatus(payload.agreementId);
|
|
69
|
+
case 'agreement-revoke':
|
|
70
|
+
if (!payload.agreementId) return null;
|
|
71
|
+
return agreementService.revoke(payload.agreementId);
|
|
72
|
+
case 'task-spawn':
|
|
73
|
+
if (!payload.agreementId || !payload.description) return null;
|
|
74
|
+
return taskService.spawnUnderAgreement(payload.agreementId, {
|
|
75
|
+
description: payload.description,
|
|
76
|
+
parentTaskId: payload.parentTaskId,
|
|
77
|
+
plan: payload.plan,
|
|
78
|
+
planReviewTiming: payload.planReviewTiming,
|
|
79
|
+
requireMidWorkPlanAck: payload.requireMidWorkPlanAck,
|
|
80
|
+
});
|
|
81
|
+
case 'task-update':
|
|
82
|
+
if (!payload.taskId || !payload.status) return null;
|
|
83
|
+
return taskService.updateState(payload.taskId, normalizeState(payload.status) as Parameters<typeof taskService.updateState>[1], payload.result);
|
|
84
|
+
default:
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function proposeAgreement(services: Services, payload: ProtocolPayload, chatId: string | null, _agents: unknown[]): Promise<unknown> {
|
|
90
|
+
if (!payload.description) throw new Error('agreement-propose requires `description`');
|
|
91
|
+
if (!payload.proposedTo) throw new Error('agreement-propose requires `proposedTo` (userId or agentId). To spawn a task under an existing agreement, use `task_spawn` instead.');
|
|
92
|
+
const { operation: _op, ...fields } = payload;
|
|
93
|
+
return services.agreementService.propose({ ...fields, chatId } as Parameters<typeof services.agreementService.propose>[0]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function subcontractAgreement(services: Services, payload: ProtocolPayload, chatId: string | null, _agents: unknown[]): Promise<unknown> {
|
|
97
|
+
if (!payload.description || !payload.executorId) return null;
|
|
98
|
+
const { operation: _op, ...fields } = payload;
|
|
99
|
+
return services.agreementService.delegate({ ...fields, chatId } as Parameters<typeof services.agreementService.delegate>[0]);
|
|
100
|
+
}
|
|
101
|
+
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { defineTool, type ToolDefinition } from '../../tools/defineTool.js';
|
|
2
|
+
import { dispatchProtocolOp } from './protocolRunner.js';
|
|
3
|
+
import type { AgreementService } from '../agreements/AgreementService.js';
|
|
4
|
+
import type { TaskService } from './TaskService.js';
|
|
5
|
+
|
|
6
|
+
interface ToolContext extends Record<string, unknown> {
|
|
7
|
+
taskService: TaskService;
|
|
8
|
+
agreementService: AgreementService;
|
|
9
|
+
chatId?: string | null;
|
|
10
|
+
agents?: unknown[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function handlerFor(operation: string) {
|
|
14
|
+
return async (args: Record<string, unknown>, ctx: Record<string, unknown>): Promise<unknown> => {
|
|
15
|
+
const c = ctx as ToolContext;
|
|
16
|
+
if (!c.taskService) throw new Error('taskService missing from tool context (expected when running under Agent)');
|
|
17
|
+
if (!c.agreementService) throw new Error('agreementService missing from tool context (expected when running under Agent)');
|
|
18
|
+
const result = await dispatchProtocolOp(
|
|
19
|
+
{ taskService: c.taskService, agreementService: c.agreementService },
|
|
20
|
+
{ operation, ...args },
|
|
21
|
+
c.chatId ?? null,
|
|
22
|
+
c.agents ?? [],
|
|
23
|
+
);
|
|
24
|
+
if (result == null) throw new Error(`Operation ${operation} returned no result`);
|
|
25
|
+
return result;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const agreementProposeTool: ToolDefinition = defineTool(
|
|
30
|
+
'agreement_propose',
|
|
31
|
+
{ description: { type: 'string', required: true }, proposedTo: { type: 'string', required: true }, engagementKind: { type: 'string', enum: ['hire', 'service'], description: 'Contract type. Defaults to service when omitted.' }, parentAgreementId: 'string', parentTaskId: 'string', payerId: 'string', providerId: 'string', price: 'number', lifecycle: 'string', expiresAt: 'string', maxExecutions: 'number', agreementDescription: 'string', plan: { type: 'object' }, planReviewTiming: 'string', requireMidWorkPlanAck: 'boolean' },
|
|
32
|
+
handlerFor('agreement-propose'),
|
|
33
|
+
{ description: 'Propose a contract to a user, agent, or everyone (proposedTo="everyone" for open broadcast). engagementKind defaults to service.', isAgreementCreation: true },
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
export const agreementSubcontractTool: ToolDefinition = defineTool(
|
|
37
|
+
'agreement_subcontract',
|
|
38
|
+
{ description: { type: 'string', required: true }, executorId: { type: 'string', required: true }, parentAgreementId: { type: 'string', required: true }, parentTaskId: 'string', payerId: 'string', price: 'number', lifecycle: 'string', expiresAt: 'string', maxExecutions: 'number', agreementDescription: 'string', plan: { type: 'object' }, planReviewTiming: 'string', requireMidWorkPlanAck: 'boolean' },
|
|
39
|
+
handlerFor('agreement-subcontract'),
|
|
40
|
+
{ description: 'Delegate work to another agent under an existing parent agreement. Requires parentAgreementId.', isAgreementCreation: true },
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
export const agreementRespondTool: ToolDefinition = defineTool(
|
|
44
|
+
'agreement_respond',
|
|
45
|
+
{ agreementId: { type: 'string', required: true }, action: { type: 'string', required: true, enum: ['approve', 'reject'] } },
|
|
46
|
+
handlerFor('agreement-respond'),
|
|
47
|
+
{ description: 'Approve or reject a pending agreement proposal.' },
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
export const taskUpdateTool: ToolDefinition = defineTool(
|
|
51
|
+
'task_update',
|
|
52
|
+
{ taskId: { type: 'string', required: true }, status: { type: 'string', required: true, enum: ['completed', 'failed', 'cancelled'] }, result: 'string' },
|
|
53
|
+
handlerFor('task-update'),
|
|
54
|
+
{ description: 'Transition a task to completed, failed, or cancelled. Include a result string for completed tasks.' },
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
export const agreementCounterProposalTool: ToolDefinition = defineTool(
|
|
58
|
+
'agreement_counter_proposal',
|
|
59
|
+
{ agreementId: { type: 'string', required: true }, price: 'number', agreementDescription: 'string', expiresAt: 'string', lifecycle: 'string', maxExecutions: 'number', description: 'string', plan: { type: 'object' }, planReviewTiming: 'string', requireMidWorkPlanAck: 'boolean' },
|
|
60
|
+
handlerFor('agreement-counter-proposal'),
|
|
61
|
+
{ description: 'Counter a pending proposal with revised terms instead of approving or rejecting. Check status first with agreement_check_proposal.' },
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
export const agreementCheckProposalTool: ToolDefinition = defineTool(
|
|
65
|
+
'agreement_check_proposal',
|
|
66
|
+
{ agreementId: { type: 'string', required: true } },
|
|
67
|
+
handlerFor('agreement-check-proposal'),
|
|
68
|
+
{ description: 'Read the current proposal status for an agreement (pending, approved, rejected, countered, expired).' },
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
export const agreementRevokeTool: ToolDefinition = defineTool(
|
|
72
|
+
'agreement_revoke',
|
|
73
|
+
{ agreementId: { type: 'string', required: true } },
|
|
74
|
+
handlerFor('agreement-revoke'),
|
|
75
|
+
{ 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.' },
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
export const taskUpdatePlanStepTool: ToolDefinition = defineTool(
|
|
79
|
+
'task_update_plan_step',
|
|
80
|
+
{ taskId: { type: 'string', required: true }, stepId: { type: 'string', required: true }, status: { type: 'string', required: true }, result: 'string' },
|
|
81
|
+
handlerFor('task-update-plan-step'),
|
|
82
|
+
{ description: "Update the status of a single step within a task's plan." },
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
export const taskSpawnTool: ToolDefinition = defineTool(
|
|
86
|
+
'task_spawn',
|
|
87
|
+
{ agreementId: { type: 'string', required: true }, description: { type: 'string', required: true }, parentTaskId: 'string', plan: { type: 'object' }, planReviewTiming: 'string', requireMidWorkPlanAck: 'boolean' },
|
|
88
|
+
handlerFor('task-spawn'),
|
|
89
|
+
{ description: 'Spawn a new task under an existing approved agreement. Use when the contract is already in place.' },
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
export const PROTOCOL_TOOLS: ToolDefinition[] = [
|
|
93
|
+
agreementProposeTool, agreementSubcontractTool,
|
|
94
|
+
agreementRespondTool, agreementCounterProposalTool, agreementCheckProposalTool, agreementRevokeTool,
|
|
95
|
+
taskSpawnTool, taskUpdateTool, taskUpdatePlanStepTool,
|
|
96
|
+
];
|
|
@@ -0,0 +1,193 @@
|
|
|
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 randomIdempotencyKey(prefix = 'op'): string {
|
|
8
|
+
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseError(text: string, fallback: string): string {
|
|
12
|
+
if (!text) return fallback;
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(text) as Record<string, unknown>;
|
|
15
|
+
return (parsed['error'] as string) || (parsed['message'] as string) || text;
|
|
16
|
+
} catch { return text; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface PayError extends Error { status?: number; body?: string; }
|
|
20
|
+
|
|
21
|
+
export interface ZiggsPayClientOptions {
|
|
22
|
+
operatorKey: string;
|
|
23
|
+
actAsAgentId?: string;
|
|
24
|
+
baseUrl?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class ZiggsPayClient {
|
|
28
|
+
private operatorKey: string;
|
|
29
|
+
private actAsAgentId: string | null;
|
|
30
|
+
private baseUrl: string;
|
|
31
|
+
|
|
32
|
+
constructor({ operatorKey, actAsAgentId, baseUrl }: ZiggsPayClientOptions) {
|
|
33
|
+
if (!operatorKey || typeof operatorKey !== 'string') throw new Error('ZiggsPayClient requires an operatorKey');
|
|
34
|
+
this.operatorKey = operatorKey;
|
|
35
|
+
this.actAsAgentId = actAsAgentId || null;
|
|
36
|
+
this.baseUrl = baseUrl || getDefaultBaseUrl();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async balance(): Promise<{ walletId: string | null; currency: string; balance: number; availableBalance: number }> {
|
|
40
|
+
const w = await this._get('/payments/wallet') as Record<string, unknown>;
|
|
41
|
+
const wallet = w['wallet'] as Record<string, unknown> | undefined;
|
|
42
|
+
return { walletId: (wallet?.['walletId'] as string) || null, currency: (wallet?.['currency'] as string) || 'pez', balance: (w['balance'] as number) ?? 0, availableBalance: (w['availableBalance'] as number) ?? 0 };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async resolve({ userId, agentId }: { userId?: string; agentId?: string } = {}): Promise<unknown> {
|
|
46
|
+
if (!userId && !agentId) throw new Error('resolve: provide userId or agentId');
|
|
47
|
+
const params = new URLSearchParams();
|
|
48
|
+
if (userId) params.set('userId', userId);
|
|
49
|
+
if (agentId) params.set('agentId', agentId);
|
|
50
|
+
const res = await this._get(`/payments/wallets/resolve?${params}`) as Record<string, unknown>;
|
|
51
|
+
return res['wallet'] || null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async transfer({ to, amount, idempotencyKey, description, capabilityTokenId }: { to: string; amount: number; idempotencyKey?: string; description?: string; capabilityTokenId?: string }): Promise<unknown> {
|
|
55
|
+
if (!to) throw new Error('transfer: `to` is required');
|
|
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.');
|
|
58
|
+
let toWalletId = to;
|
|
59
|
+
if (!to.startsWith('wal_')) {
|
|
60
|
+
const w = await this.resolve(to.startsWith('agent_') ? { agentId: to } : { userId: to }) as Record<string, unknown> | null;
|
|
61
|
+
if (!w?.['walletId']) throw new Error(`transfer: could not resolve wallet for "${to}"`);
|
|
62
|
+
toWalletId = w['walletId'] as string;
|
|
63
|
+
}
|
|
64
|
+
const result = await this._post('/payments/transfer', { toWalletId, amount, idempotencyKey: idempotencyKey || randomIdempotencyKey('xfer'), description, capabilityTokenId }) as Record<string, unknown>;
|
|
65
|
+
if (result?.['status'] === 'approval_required') {
|
|
66
|
+
const approval = (result['approval'] as Record<string, unknown>) || {};
|
|
67
|
+
return { status: 'approval_required', approvalId: approval['approvalId'] || null, expiresAt: approval['expiresAt'] || null, reason: approval['reason'] || 'Amount exceeds auto-approve threshold', toWalletId, amount };
|
|
68
|
+
}
|
|
69
|
+
const tx = result?.['transaction'] as Record<string, unknown> | undefined;
|
|
70
|
+
return { status: 'transferred', transactionId: tx?.['transactionId'] || null, toWalletId, amount };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async hold({ amount, idempotencyKey, description }: { amount: number; idempotencyKey?: string; description?: string }): Promise<unknown> {
|
|
74
|
+
if (!(Number.isInteger(amount) && amount > 0)) throw new Error('hold: `amount` must be a positive integer (cents)');
|
|
75
|
+
return this._post('/payments/hold', { amount, idempotencyKey: idempotencyKey || randomIdempotencyKey('hold'), description });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async release({ holdId, action = 'complete', toWalletId, idempotencyKey }: { holdId: string; action?: string; toWalletId?: string; idempotencyKey?: string }): Promise<unknown> {
|
|
79
|
+
return this._post(`/payments/release/${holdId}`, { idempotencyKey: idempotencyKey || randomIdempotencyKey('rel'), action, toWalletId });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async history(params: { limit?: number; offset?: number; type?: string } = {}): Promise<unknown> {
|
|
83
|
+
const q = new URLSearchParams();
|
|
84
|
+
if (params.limit != null) q.set('limit', String(params.limit));
|
|
85
|
+
if (params.offset != null) q.set('offset', String(params.offset));
|
|
86
|
+
if (params.type) q.set('type', params.type);
|
|
87
|
+
return this._get('/payments/history' + (q.toString() ? `?${q}` : ''));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async issueToken({ holderId, caveats }: { holderId: string; caveats?: unknown }): Promise<unknown> {
|
|
91
|
+
return this._post('/payments/tokens', { holderId, caveats });
|
|
92
|
+
}
|
|
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 });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async revokeToken(tokenId: string): Promise<unknown> {
|
|
99
|
+
return this._request('DELETE', `/payments/tokens/${tokenId}`, undefined);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async listTokens(): Promise<unknown[]> {
|
|
103
|
+
const res = await this._get('/payments/tokens') as Record<string, unknown>;
|
|
104
|
+
return (res['tokens'] as unknown[]) || [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async createTopUpIntent({ amount, description, currency }: { amount?: number; description?: string; currency?: string } = {}): Promise<unknown> {
|
|
108
|
+
return this._post('/payments/onramp/intent', { amount, description, currency });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async confirmMockIntent(intentId: string): Promise<unknown> {
|
|
112
|
+
return this._post(`/payments/onramp/mock-confirm/${intentId}`, {});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async faucet({ amount, description = 'Faucet', idempotencyKey }: { amount?: number; description?: string; idempotencyKey?: string } = {}): Promise<unknown> {
|
|
116
|
+
return this._post('/payments/wallet/fund', { amount, idempotencyKey: idempotencyKey || randomIdempotencyKey('faucet'), description });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async approvals({ status = 'pending' }: { status?: string } = {}): Promise<unknown[]> {
|
|
120
|
+
const res = await this._get(`/payments/approvals?status=${encodeURIComponent(status)}`) as Record<string, unknown>;
|
|
121
|
+
return (res['approvals'] as unknown[]) || [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async getApproval(approvalId: string): Promise<unknown | null> {
|
|
125
|
+
if (!approvalId) throw new Error('getApproval: approvalId is required');
|
|
126
|
+
try {
|
|
127
|
+
const res = await this._get(`/payments/approvals/${approvalId}`) as Record<string, unknown>;
|
|
128
|
+
return res?.['approval'] || null;
|
|
129
|
+
} catch (err) {
|
|
130
|
+
if ((err as PayError).status === 404) return null;
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async decide(approvalId: string, decision: string, note = ''): Promise<unknown> {
|
|
136
|
+
return this._post(`/payments/approvals/${approvalId}/decide`, { decision, note });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async waitForApproval(approvalId: string, opts: { pollMs?: number; timeoutMs?: number; signal?: AbortSignal } = {}): Promise<unknown> {
|
|
140
|
+
const pollMs = Math.max(500, opts.pollMs ?? 3_000);
|
|
141
|
+
const timeoutMs = Math.max(pollMs, opts.timeoutMs ?? 120_000);
|
|
142
|
+
const signal = opts.signal;
|
|
143
|
+
const deadline = Date.now() + timeoutMs;
|
|
144
|
+
|
|
145
|
+
const sleep = (ms: number) => new Promise<void>((resolve, reject) => {
|
|
146
|
+
const timer = setTimeout(resolve, ms);
|
|
147
|
+
if (signal) {
|
|
148
|
+
if (signal.aborted) { clearTimeout(timer); reject(new Error('aborted')); return; }
|
|
149
|
+
signal.addEventListener('abort', () => { clearTimeout(timer); reject(new Error('aborted')); }, { once: true });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
while (true) {
|
|
154
|
+
if (signal?.aborted) throw new Error('aborted');
|
|
155
|
+
const approval = await this.getApproval(approvalId) as Record<string, unknown> | null;
|
|
156
|
+
if (!approval) return { status: 'gone' };
|
|
157
|
+
const s = approval['status'];
|
|
158
|
+
if (s === 'executed') return { status: 'executed', approval, transactionId: approval['executedTransactionId'] || null };
|
|
159
|
+
if (s === 'rejected') return { status: 'rejected', approval };
|
|
160
|
+
if (s === 'expired') return { status: 'expired', approval };
|
|
161
|
+
const remaining = deadline - Date.now();
|
|
162
|
+
if (remaining <= 0) return { status: 'timeout', approval };
|
|
163
|
+
await sleep(Math.min(pollMs, remaining));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private _buildHeaders(extra: Record<string, string> = {}): Record<string, string> {
|
|
168
|
+
const h: Record<string, string> = { Authorization: `Bearer ${this.operatorKey}`, ...extra };
|
|
169
|
+
if (this.actAsAgentId) h['X-Agent-Id'] = this.actAsAgentId;
|
|
170
|
+
return h;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private async _request(method: string, path: string, body: unknown): Promise<unknown> {
|
|
174
|
+
const init: RequestInit = { method, headers: this._buildHeaders(body !== undefined ? { 'content-type': 'application/json' } : {}) };
|
|
175
|
+
if (body !== undefined) init.body = JSON.stringify(body);
|
|
176
|
+
const response = await fetch(`${this.baseUrl}${path}`, init);
|
|
177
|
+
const text = await response.text();
|
|
178
|
+
if (!response.ok) {
|
|
179
|
+
const err = new Error(parseError(text, `HTTP ${response.status}`)) as PayError;
|
|
180
|
+
err.status = response.status;
|
|
181
|
+
err.body = text;
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
return text ? JSON.parse(text) : null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private _get(path: string): Promise<unknown> { return this._request('GET', path, undefined); }
|
|
188
|
+
private _post(path: string, body: unknown): Promise<unknown> { return this._request('POST', path, body ?? {}); }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function createZiggsPayClient(opts: ZiggsPayClientOptions): ZiggsPayClient {
|
|
192
|
+
return new ZiggsPayClient(opts);
|
|
193
|
+
}
|