@agentxv2/gateway 0.1.1

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 (175) hide show
  1. package/.env.example +53 -0
  2. package/db/migrations/001_init.sql +117 -0
  3. package/db/migrations/002_agents.sql +20 -0
  4. package/db/migrations/003_a2a_results.sql +23 -0
  5. package/deploy/.env.deploy.example +23 -0
  6. package/deploy/add_swap_build.py +45 -0
  7. package/deploy/build_and_deploy.py +110 -0
  8. package/deploy/build_deploy_a2a.py +190 -0
  9. package/deploy/build_no_lint.py +52 -0
  10. package/deploy/build_on_fe.py +86 -0
  11. package/deploy/build_on_gw.py +98 -0
  12. package/deploy/build_prod.py +54 -0
  13. package/deploy/check_bal.py +31 -0
  14. package/deploy/check_code.py +47 -0
  15. package/deploy/check_deploy.sh +20 -0
  16. package/deploy/check_docs.py +16 -0
  17. package/deploy/check_frontend.py +42 -0
  18. package/deploy/check_frontend2.py +35 -0
  19. package/deploy/check_pks.py +34 -0
  20. package/deploy/check_status.py +32 -0
  21. package/deploy/check_v2.py +8 -0
  22. package/deploy/clean_install.py +28 -0
  23. package/deploy/deploy_a2a.py +58 -0
  24. package/deploy/deploy_a2a.sh +33 -0
  25. package/deploy/deploy_a2a_bg.sh +40 -0
  26. package/deploy/deploy_a2a_v2.py +151 -0
  27. package/deploy/deploy_a2a_v2.sh +36 -0
  28. package/deploy/deploy_bin.py +64 -0
  29. package/deploy/deploy_config.py +34 -0
  30. package/deploy/deploy_final.py +42 -0
  31. package/deploy/deploy_frontend.py +108 -0
  32. package/deploy/deploy_mcp.py +108 -0
  33. package/deploy/deploy_now.py +49 -0
  34. package/deploy/deploy_push_all.py +128 -0
  35. package/deploy/deploy_script.py +64 -0
  36. package/deploy/deploy_sep_a2a.sh +18 -0
  37. package/deploy/dev_mode.py +51 -0
  38. package/deploy/diag.py +12 -0
  39. package/deploy/diff_test.py +22 -0
  40. package/deploy/final_check.py +33 -0
  41. package/deploy/final_deploy.py +50 -0
  42. package/deploy/final_fix.py +54 -0
  43. package/deploy/final_push.py +114 -0
  44. package/deploy/find_frontend.py +38 -0
  45. package/deploy/fix_a2a.py +43 -0
  46. package/deploy/fix_and_build.py +77 -0
  47. package/deploy/fix_and_verify.py +56 -0
  48. package/deploy/fix_build.py +51 -0
  49. package/deploy/forge_deploy.py +67 -0
  50. package/deploy/full_push.py +101 -0
  51. package/deploy/full_push2.py +106 -0
  52. package/deploy/go_prod.py +59 -0
  53. package/deploy/gw_deploy.py +133 -0
  54. package/deploy/gw_final.py +206 -0
  55. package/deploy/gw_restart.py +28 -0
  56. package/deploy/gw_status.py +18 -0
  57. package/deploy/inline_build.py +99 -0
  58. package/deploy/one_shot.py +84 -0
  59. package/deploy/ox_deploy.py +32 -0
  60. package/deploy/pg_setup.py +36 -0
  61. package/deploy/prod_deploy.py +126 -0
  62. package/deploy/prod_final.py +140 -0
  63. package/deploy/prod_go.py +61 -0
  64. package/deploy/prod_one_shot.py +139 -0
  65. package/deploy/prod_setup.sh +62 -0
  66. package/deploy/prod_v2.py +91 -0
  67. package/deploy/prod_v3.py +103 -0
  68. package/deploy/push_docs.py +93 -0
  69. package/deploy/push_mcp.py +66 -0
  70. package/deploy/push_mcp_doc.py +28 -0
  71. package/deploy/push_v063.py +80 -0
  72. package/deploy/push_v2.py +71 -0
  73. package/deploy/qcheck.py +13 -0
  74. package/deploy/qcheck2.py +18 -0
  75. package/deploy/quick_check.py +8 -0
  76. package/deploy/rebuild.py +47 -0
  77. package/deploy/rebuild_frontend.py +44 -0
  78. package/deploy/restart.py +28 -0
  79. package/deploy/restore_frontend.py +42 -0
  80. package/deploy/restore_frontend2.py +41 -0
  81. package/deploy/restore_gateway.py +34 -0
  82. package/deploy/restore_quick.py +33 -0
  83. package/deploy/run_bash.py +19 -0
  84. package/deploy/run_sep.py +20 -0
  85. package/deploy/sdk_upgrade.py +50 -0
  86. package/deploy/server_prepare.py +41 -0
  87. package/deploy/server_setup.py +41 -0
  88. package/deploy/ssh_check.py +14 -0
  89. package/deploy/ssh_push.py +24 -0
  90. package/deploy/start_deploy.py +35 -0
  91. package/deploy/start_frontend.py +12 -0
  92. package/deploy/start_v2.py +29 -0
  93. package/deploy/stat.py +18 -0
  94. package/deploy/tail_log.py +11 -0
  95. package/deploy/update_env.py +32 -0
  96. package/deploy/upload_and_start.py +107 -0
  97. package/deploy/verify_a2a.py +13 -0
  98. package/deploy/verify_mcp.py +37 -0
  99. package/deploy/verify_sdk.py +21 -0
  100. package/dist/config.d.ts +26 -0
  101. package/dist/config.js +41 -0
  102. package/dist/config.js.map +1 -0
  103. package/dist/index.d.ts +1 -0
  104. package/dist/index.js +149 -0
  105. package/dist/index.js.map +1 -0
  106. package/dist/lib/crypto.d.ts +2 -0
  107. package/dist/lib/crypto.js +43 -0
  108. package/dist/lib/crypto.js.map +1 -0
  109. package/dist/lib/db.d.ts +3 -0
  110. package/dist/lib/db.js +26 -0
  111. package/dist/lib/db.js.map +1 -0
  112. package/dist/lib/ethers-utils.d.ts +60 -0
  113. package/dist/lib/ethers-utils.js +186 -0
  114. package/dist/lib/ethers-utils.js.map +1 -0
  115. package/dist/middleware/adminAuth.d.ts +2 -0
  116. package/dist/middleware/adminAuth.js +31 -0
  117. package/dist/middleware/adminAuth.js.map +1 -0
  118. package/dist/middleware/auth.d.ts +22 -0
  119. package/dist/middleware/auth.js +192 -0
  120. package/dist/middleware/auth.js.map +1 -0
  121. package/dist/middleware/error-handler.d.ts +36 -0
  122. package/dist/middleware/error-handler.js +100 -0
  123. package/dist/middleware/error-handler.js.map +1 -0
  124. package/dist/middleware/rate-limiter.d.ts +5 -0
  125. package/dist/middleware/rate-limiter.js +107 -0
  126. package/dist/middleware/rate-limiter.js.map +1 -0
  127. package/dist/routes/a2a.d.ts +2 -0
  128. package/dist/routes/a2a.js +84 -0
  129. package/dist/routes/a2a.js.map +1 -0
  130. package/dist/routes/admin.d.ts +2 -0
  131. package/dist/routes/admin.js +180 -0
  132. package/dist/routes/admin.js.map +1 -0
  133. package/dist/routes/agents.d.ts +2 -0
  134. package/dist/routes/agents.js +43 -0
  135. package/dist/routes/agents.js.map +1 -0
  136. package/dist/routes/chat.d.ts +2 -0
  137. package/dist/routes/chat.js +183 -0
  138. package/dist/routes/chat.js.map +1 -0
  139. package/dist/routes/history.d.ts +2 -0
  140. package/dist/routes/history.js +92 -0
  141. package/dist/routes/history.js.map +1 -0
  142. package/dist/routes/mcp.d.ts +13 -0
  143. package/dist/routes/mcp.js +513 -0
  144. package/dist/routes/mcp.js.map +1 -0
  145. package/dist/routes/tenant.d.ts +2 -0
  146. package/dist/routes/tenant.js +154 -0
  147. package/dist/routes/tenant.js.map +1 -0
  148. package/dist/services/a2a-worker.d.ts +7 -0
  149. package/dist/services/a2a-worker.js +406 -0
  150. package/dist/services/a2a-worker.js.map +1 -0
  151. package/dist/services/agent-indexer.d.ts +4 -0
  152. package/dist/services/agent-indexer.js +124 -0
  153. package/dist/services/agent-indexer.js.map +1 -0
  154. package/e2e_wallet.js +175 -0
  155. package/ecosystem.config.js +17 -0
  156. package/package.json +33 -0
  157. package/src/config.ts +47 -0
  158. package/src/index.ts +143 -0
  159. package/src/lib/crypto.ts +44 -0
  160. package/src/lib/db.ts +25 -0
  161. package/src/lib/ethers-utils.ts +229 -0
  162. package/src/middleware/adminAuth.ts +34 -0
  163. package/src/middleware/auth.ts +244 -0
  164. package/src/middleware/error-handler.ts +109 -0
  165. package/src/middleware/rate-limiter.ts +115 -0
  166. package/src/routes/a2a.ts +97 -0
  167. package/src/routes/admin.ts +219 -0
  168. package/src/routes/agents.ts +52 -0
  169. package/src/routes/chat.ts +219 -0
  170. package/src/routes/history.ts +114 -0
  171. package/src/routes/mcp.ts +567 -0
  172. package/src/routes/tenant.ts +196 -0
  173. package/src/services/a2a-worker.ts +486 -0
  174. package/src/services/agent-indexer.ts +127 -0
  175. package/tsconfig.json +18 -0
@@ -0,0 +1,196 @@
1
+ // ---------------------------------------------------------------------------
2
+ // AgentX Gateway — Tenant Management Routes
3
+ // ---------------------------------------------------------------------------
4
+
5
+ import { Router, Request, Response } from 'express'
6
+ import { getPool } from '../lib/db'
7
+ import { encryptApiKey, decryptApiKey } from '../lib/crypto'
8
+ import { config } from '../config'
9
+
10
+ const router = Router()
11
+
12
+ // GET /api/v1/tenant/me
13
+ router.get('/me', async (req: Request, res: Response) => {
14
+ const tenant = req.tenant
15
+
16
+ const pool = getPool()
17
+ const [planRow, keysRow, usageRow] = await Promise.all([
18
+ pool.query(`SELECT name, slug, quota_daily, quota_monthly, platform_models, byok_enabled, rate_limit_rpm, max_concurrent, features FROM plans WHERE id = $1`, [tenant!.planId]),
19
+ pool.query(`SELECT id, provider, model, label, endpoint, is_active, last_validated, created_at FROM tenant_api_keys WHERE tenant_id = $1 ORDER BY created_at DESC`, [tenant!.id]),
20
+ pool.query(`SELECT COALESCE(SUM(tokens_total), 0) as total_tokens, COALESCE(SUM(tool_calls), 0) as total_tool_calls FROM usage_logs WHERE tenant_id = $1 AND created_at > NOW() - INTERVAL '24 hours'`, [tenant!.id]),
21
+ ])
22
+
23
+ const platformModels = planRow.rows[0] ? planRow.rows[0].platform_models : []
24
+ const ownKeys = keysRow.rows.map(r => ({
25
+ id: r.id,
26
+ provider: r.provider,
27
+ model: r.model,
28
+ label: r.label,
29
+ endpoint: r.endpoint,
30
+ is_active: r.is_active,
31
+ last_validated: r.last_validated,
32
+ created_at: r.created_at,
33
+ }))
34
+
35
+ res.json({
36
+ tenant: {
37
+ id: tenant!.id,
38
+ wallet_address: tenant!.walletAddress,
39
+ status: tenant!.status,
40
+ },
41
+ plan: planRow.rows[0] ? {
42
+ name: planRow.rows[0].name,
43
+ slug: planRow.rows[0].slug,
44
+ quota_daily: planRow.rows[0].quota_daily,
45
+ quota_used: tenant!.quotaUsed,
46
+ platform_models: platformModels,
47
+ byok_enabled: planRow.rows[0].byok_enabled,
48
+ rate_limit_rpm: planRow.rows[0].rate_limit_rpm,
49
+ max_concurrent: planRow.rows[0].max_concurrent,
50
+ } : null,
51
+ own_keys: ownKeys,
52
+ usage_today: {
53
+ total_tokens: parseInt(usageRow.rows[0]?.total_tokens || '0', 10),
54
+ total_tool_calls: parseInt(usageRow.rows[0]?.total_tool_calls || '0', 10),
55
+ },
56
+ })
57
+ })
58
+
59
+ // GET /api/v1/tenant/keys
60
+ router.get('/keys', async (req: Request, res: Response) => {
61
+ const pool = getPool()
62
+ const result = await pool.query(
63
+ `SELECT id, provider, model, label, endpoint, is_active, last_validated, created_at FROM tenant_api_keys WHERE tenant_id = $1 ORDER BY created_at DESC`,
64
+ [req.tenant!.id]
65
+ )
66
+ res.json({ keys: result.rows })
67
+ })
68
+
69
+ // POST /api/v1/tenant/keys
70
+ router.post('/keys', async (req: Request, res: Response) => {
71
+ const { provider, endpoint, api_key, model, label } = req.body
72
+
73
+ if (!provider || !endpoint || !api_key || !model) {
74
+ res.status(400).json({ error: 'provider, endpoint, api_key, and model are required' })
75
+ return
76
+ }
77
+
78
+ const encrypted = encryptApiKey(api_key, config.masterEncryptionKey)
79
+
80
+ const pool = getPool()
81
+ const result = await pool.query(
82
+ `INSERT INTO tenant_api_keys (tenant_id, provider, endpoint, api_key, model, label)
83
+ VALUES ($1, $2, $3, $4, $5, $6)
84
+ RETURNING id, provider, model, label, endpoint, is_active, created_at`,
85
+ [req.tenant!.id, provider, endpoint, encrypted, model, label || null]
86
+ )
87
+
88
+ res.status(201).json({ key: result.rows[0] })
89
+ })
90
+
91
+ // DELETE /api/v1/tenant/keys/:keyId
92
+ router.delete('/keys/:keyId', async (req: Request, res: Response) => {
93
+ const pool = getPool()
94
+ const result = await pool.query(
95
+ `DELETE FROM tenant_api_keys WHERE id = $1 AND tenant_id = $2 RETURNING id`,
96
+ [req.params.keyId, req.tenant!.id]
97
+ )
98
+ if (result.rows.length === 0) {
99
+ res.status(404).json({ error: 'Key not found' })
100
+ return
101
+ }
102
+ res.json({ deleted: true })
103
+ })
104
+
105
+ // POST /api/v1/tenant/keys/:keyId/validate
106
+ router.post('/keys/:keyId/validate', async (req: Request, res: Response) => {
107
+ const pool = getPool()
108
+ const keyRow = await pool.query(
109
+ `SELECT * FROM tenant_api_keys WHERE id = $1 AND tenant_id = $2`,
110
+ [req.params.keyId, req.tenant!.id]
111
+ )
112
+ if (keyRow.rows.length === 0) {
113
+ res.status(404).json({ error: 'Key not found' })
114
+ return
115
+ }
116
+
117
+ const tk = keyRow.rows[0]
118
+ const key = decryptApiKey(tk.api_key, config.masterEncryptionKey)
119
+
120
+ try {
121
+ const testRes = await fetch(`${tk.endpoint}/models`, {
122
+ headers: { Authorization: `Bearer ${key}` },
123
+ })
124
+ if (testRes.ok) {
125
+ await pool.query(
126
+ `UPDATE tenant_api_keys SET is_active = true, last_validated = NOW() WHERE id = $1`,
127
+ [tk.id]
128
+ )
129
+ res.json({ valid: true, provider: tk.provider, endpoint: tk.endpoint })
130
+ } else {
131
+ await pool.query(
132
+ `UPDATE tenant_api_keys SET is_active = false WHERE id = $1`,
133
+ [tk.id]
134
+ )
135
+ res.json({ valid: false, error: `HTTP ${testRes.status}` })
136
+ }
137
+ } catch (err) {
138
+ res.json({ valid: false, error: String(err) })
139
+ }
140
+ })
141
+
142
+ // GET /api/v1/tenant/usage
143
+ router.get('/usage', async (req: Request, res: Response) => {
144
+ const days = parseInt(req.query.days as string || '30', 10)
145
+ const pool = getPool()
146
+
147
+ const [summaryRow, timelineRows] = await Promise.all([
148
+ pool.query(
149
+ `SELECT
150
+ key_source,
151
+ COALESCE(SUM(tokens_total), 0) as total_tokens,
152
+ COALESCE(SUM(tool_calls), 0) as total_tool_calls,
153
+ COUNT(*) as request_count
154
+ FROM usage_logs
155
+ WHERE tenant_id = $1 AND created_at > NOW() - INTERVAL '1 day' * $2
156
+ GROUP BY key_source`,
157
+ [req.tenant!.id, days]
158
+ ),
159
+ pool.query(
160
+ `SELECT DATE(created_at) as day, key_source, COALESCE(SUM(tokens_total), 0) as tokens
161
+ FROM usage_logs
162
+ WHERE tenant_id = $1 AND created_at > NOW() - INTERVAL '1 day' * $2
163
+ GROUP BY DATE(created_at), key_source
164
+ ORDER BY day`,
165
+ [req.tenant!.id, days]
166
+ ),
167
+ ])
168
+
169
+ res.json({
170
+ summary: summaryRow.rows,
171
+ timeline: timelineRows.rows,
172
+ })
173
+ })
174
+
175
+ // GET /api/v1/models
176
+ router.get('/models', async (req: Request, res: Response) => {
177
+ const tenant = req.tenant
178
+
179
+ const pool = getPool()
180
+ const [planRow, keysRow] = await Promise.all([
181
+ pool.query(`SELECT platform_models FROM plans WHERE id = $1`, [tenant!.planId]),
182
+ pool.query(`SELECT id, provider, model, label FROM tenant_api_keys WHERE tenant_id = $1 AND is_active = true`, [tenant!.id]),
183
+ ])
184
+
185
+ res.json({
186
+ platform: planRow.rows[0]?.platform_models || [],
187
+ tenant_owned: keysRow.rows.map(r => ({
188
+ id: r.id,
189
+ provider: r.provider,
190
+ model: r.model,
191
+ label: r.label,
192
+ })),
193
+ })
194
+ })
195
+
196
+ export default router
@@ -0,0 +1,486 @@
1
+ // ---------------------------------------------------------------------------
2
+ // AgentX Gateway — A2A Task Worker (v2 — ReAct AgentLoop + Multi-Agent Orchestration)
3
+ // ---------------------------------------------------------------------------
4
+ // Background service that:
5
+ // 1. Polls OxaChain A2A contract for pending tasks (status=Created/Accepted)
6
+ // 2. Runs ReAct-style AgentLoop with A2A tools for each task
7
+ // 3. LLM can autonomously delegate sub-tasks to other agents via createTask()
8
+ // 4. Sub-tasks are processed inline and results fed back to the parent LLM
9
+ //
10
+ // Multi-Agent Orchestration Flow:
11
+ // Agent A's task → LLM(Agent A) analyzes
12
+ // → LLM decides: "I need Agent B for auditing"
13
+ // → calls agentx_a2a_create_task(Agent B, "audit", ...)
14
+ // → Worker submits tx on-chain, processes Agent B's task inline
15
+ // → Agent B's result fed back to Agent A's LLM
16
+ // → LLM(Agent A) continues: "Now I need Agent C to summarize"
17
+ // → calls agentx_a2a_create_task(Agent C, "summarize", ...)
18
+ // → Worker processes Agent C's task inline
19
+ // → Agent A's LLM aggregates all results → final output
20
+ // ---------------------------------------------------------------------------
21
+
22
+ import { ethers } from 'ethers'
23
+ import { getPool } from '../lib/db'
24
+ import { config } from '../config'
25
+ import { decryptApiKey } from '../lib/crypto'
26
+
27
+ // ── A2A Contract ABI ──────────────────────────────────────────────────────
28
+
29
+ const A2A_ABI = [
30
+ 'function getAgentTasks(uint256 agentId) view returns (tuple(uint256,uint256,string,string,string,uint256,address,uint256,uint256,bytes32)[])',
31
+ 'function getTask(uint256 taskId) view returns (uint256,uint256,string,string,string,uint256,address,uint256,uint256)',
32
+ 'function createTask(uint256 agentId, string taskType, string inputData) returns (uint256)',
33
+ 'event TaskCreated(uint256 indexed taskId, uint256 indexed agentId, address indexed client, string taskType, uint256 status)',
34
+ ]
35
+
36
+ // ── Types ──────────────────────────────────────────────────────────────────
37
+
38
+ interface A2ATaskOnChain {
39
+ taskId: number
40
+ agentId: number
41
+ taskType: string
42
+ inputData: string
43
+ outputData: string
44
+ status: number
45
+ clientAddress: string
46
+ createdAt: number
47
+ }
48
+
49
+ interface AgentInfo {
50
+ id: number
51
+ name: string
52
+ owner: string
53
+ description: string
54
+ }
55
+
56
+ // ── Worker State ───────────────────────────────────────────────────────────
57
+
58
+ let pollingTimer: ReturnType<typeof setInterval> | null = null
59
+ let isRunning = false
60
+ const POLL_INTERVAL_MS = 30_000
61
+ const MAX_BATCH_SIZE = 3 // Reduced since each task may spawn sub-tasks
62
+ const MAX_DEPTH = 3 // Max delegation depth
63
+ const MAX_REACT_ITERATIONS = 5 // Max LLM tool-call rounds per task
64
+ const processingSet = new Set<number>()
65
+ let totalOrchestrated = 0 // Counter for multi-agent orchestrations
66
+
67
+ // ── Public API ─────────────────────────────────────────────────────────────
68
+
69
+ export function startA2AWorker(): void {
70
+ if (pollingTimer) return
71
+ console.log('[A2A Worker v2] Starting ReAct AgentLoop mode, poll:', POLL_INTERVAL_MS / 1000, 's')
72
+ pollingTimer = setInterval(pollAndProcess, POLL_INTERVAL_MS)
73
+ setTimeout(pollAndProcess, 5_000)
74
+ }
75
+
76
+ export function stopA2AWorker(): void {
77
+ if (pollingTimer) { clearInterval(pollingTimer); pollingTimer = null; console.log('[A2A Worker] Stopped') }
78
+ }
79
+
80
+ export function getWorkerStatus(): { running: boolean; processingSetSize: number; totalOrchestrated: number } {
81
+ return { running: pollingTimer !== null, processingSetSize: processingSet.size, totalOrchestrated }
82
+ }
83
+
84
+ // ── Core Logic ─────────────────────────────────────────────────────────────
85
+
86
+ async function pollAndProcess(): Promise<void> {
87
+ if (isRunning) { console.log('[A2A Worker] Previous poll still running, skipping'); return }
88
+ isRunning = true
89
+ try {
90
+ const provider = new ethers.JsonRpcProvider(config.rpcUrlOxaChain)
91
+ const a2aContract = new ethers.Contract(config.a2aProtocolOxaChain, A2A_ABI, provider)
92
+ const pool = getPool()
93
+ const { rows: agents } = await pool.query(`SELECT id, name, owner, description FROM agents ORDER BY id`)
94
+ if (agents.length === 0) { isRunning = false; return }
95
+
96
+ let processedCount = 0
97
+ for (const agent of agents) {
98
+ if (processedCount >= MAX_BATCH_SIZE) break
99
+ const agentId = agent.id as number
100
+ const agentName = agent.name as string
101
+ const agentOwner = (agent.owner as string || '').toLowerCase()
102
+ let tenantId: string | null = null
103
+ if (agentOwner) {
104
+ const { rows: tenants } = await pool.query(`SELECT id FROM tenants WHERE LOWER(wallet_address) = $1`, [agentOwner])
105
+ tenantId = tenants[0]?.id || null
106
+ }
107
+
108
+ const { rows: existingResults } = await pool.query(
109
+ `SELECT task_id FROM a2a_task_results WHERE agent_id = $1 AND status = 2`, [agentId]
110
+ )
111
+ const completedTaskIds = new Set(existingResults.map((r: any) => r.task_id))
112
+
113
+ try {
114
+ const tasks: any[] = await a2aContract.getAgentTasks(agentId)
115
+ for (const task of tasks) {
116
+ if (processedCount >= MAX_BATCH_SIZE) break
117
+ const taskId = Number(task[0])
118
+ const status = Number(task[5])
119
+ if (status > 1 || completedTaskIds.has(taskId) || processingSet.has(taskId)) continue
120
+
121
+ const onChainTask: A2ATaskOnChain = {
122
+ taskId, agentId: Number(task[1]), taskType: String(task[2] || ''),
123
+ inputData: String(task[3] || ''), outputData: String(task[4] || ''),
124
+ status, clientAddress: String(task[6] || ''), createdAt: Number(task[7] || 0),
125
+ }
126
+
127
+ processingSet.add(taskId); processedCount++
128
+ console.log(`[A2A Worker] Task #${taskId} → Agent #${agentId} (${agentName})`)
129
+
130
+ processTask(pool, onChainTask, { id: agentId, name: agentName, owner: agentOwner, description: agent.description as string }, tenantId, agents as AgentInfo[], 0)
131
+ .catch(err => console.error(`[A2A Worker] Task #${taskId} failed:`, err.message))
132
+ .finally(() => processingSet.delete(taskId))
133
+ }
134
+ } catch (err: any) {
135
+ if (!err.message?.includes('TaskNotFound') && !err.message?.includes('revert')) {
136
+ console.warn(`[A2A Worker] Error for agent #${agentId}:`, err.message?.slice(0, 100))
137
+ }
138
+ }
139
+ }
140
+ if (processedCount > 0) console.log(`[A2A Worker] Queued ${processedCount} task(s)`)
141
+ } catch (err: any) { console.error('[A2A Worker] Poll error:', err.message) }
142
+ finally { isRunning = false }
143
+ }
144
+
145
+ // ── ReAct AgentLoop Task Processing ────────────────────────────────────────
146
+
147
+ const A2A_TOOLS = [
148
+ {
149
+ type: 'function' as const,
150
+ function: {
151
+ name: 'agentx_a2a_create_task',
152
+ description: 'DELEGATE work to another AgentX agent. Creates an on-chain task. Use this when you need another agent\'s expertise. The sub-task will be processed and its result returned to you.',
153
+ parameters: {
154
+ type: 'object',
155
+ properties: {
156
+ targetAgentId: { type: 'integer', description: 'The agent ID to delegate to' },
157
+ taskType: { type: 'string', description: 'Type of task (e.g., "audit", "analyze", "summarize", "translate")' },
158
+ inputData: { type: 'string', description: 'Full task description. Include ALL context the sub-agent needs.' },
159
+ },
160
+ required: ['targetAgentId', 'taskType', 'inputData'],
161
+ },
162
+ },
163
+ },
164
+ {
165
+ type: 'function' as const,
166
+ function: {
167
+ name: 'agentx_a2a_get_task',
168
+ description: 'Check the status and result of an A2A task by ID. Use to check if a delegated sub-task has completed.',
169
+ parameters: {
170
+ type: 'object',
171
+ properties: { taskId: { type: 'integer', description: 'The task ID to query' } },
172
+ required: ['taskId'],
173
+ },
174
+ },
175
+ },
176
+ {
177
+ type: 'function' as const,
178
+ function: {
179
+ name: 'agentx_list_agents',
180
+ description: 'List ALL available agents with their IDs, names, and descriptions. Use this to discover which agents can help with sub-tasks.',
181
+ parameters: { type: 'object', properties: {} },
182
+ },
183
+ },
184
+ ]
185
+
186
+ async function processTask(
187
+ pool: ReturnType<typeof getPool>,
188
+ task: A2ATaskOnChain,
189
+ agent: AgentInfo,
190
+ tenantId: string | null,
191
+ allAgents: AgentInfo[],
192
+ depth: number,
193
+ ): Promise<string> {
194
+ const indent = ' '.repeat(depth)
195
+ const taskId = task.taskId
196
+
197
+ if (depth >= MAX_DEPTH) {
198
+ console.log(`${indent}[A2A] Max depth reached for task #${taskId}, processing directly`)
199
+ }
200
+
201
+ await pool.query(
202
+ `INSERT INTO a2a_task_results (task_id, agent_id, task_type, input_data, tenant_id, status, created_at)
203
+ VALUES ($1, $2, $3, $4, $5, 1, NOW())
204
+ ON CONFLICT (task_id) DO UPDATE SET status = 1`,
205
+ [taskId, task.agentId, task.taskType, task.inputData, tenantId]
206
+ )
207
+
208
+ try {
209
+ const llmConfig = await resolveLLMConfig(pool, tenantId)
210
+ if (!llmConfig) throw new Error('No LLM API key available')
211
+
212
+ console.log(`${indent}[A2A] ReAct loop for task #${taskId} (Agent: ${agent.name}, depth: ${depth})`)
213
+
214
+ // ── ReAct AgentLoop ─────────────────────────────────────────────────
215
+ const messages: { role: string; content: string | null; tool_calls?: any[]; tool_call_id?: string }[] = [
216
+ {
217
+ role: 'system',
218
+ content: buildOrchestrationSystemPrompt(agent, allAgents),
219
+ },
220
+ {
221
+ role: 'user',
222
+ content: buildOrchestrationUserPrompt(task),
223
+ },
224
+ ]
225
+
226
+ let finalContent = ''
227
+ let totalTokens = 0
228
+ let subTaskResults: { taskId: number; agentName: string; output: string }[] = []
229
+
230
+ for (let iter = 0; iter < MAX_REACT_ITERATIONS; iter++) {
231
+ const response = await callLLMWithTools(llmConfig, messages, A2A_TOOLS)
232
+ totalTokens += response.tokens
233
+ const choice = response.choices[0]
234
+ const msg = choice.message
235
+
236
+ // If no tool calls, we have the final answer
237
+ if (!msg.tool_calls || msg.tool_calls.length === 0) {
238
+ finalContent = msg.content || ''
239
+ messages.push({ role: 'assistant', content: finalContent })
240
+ break
241
+ }
242
+
243
+ // Process tool calls
244
+ messages.push({ role: 'assistant', content: msg.content || null, tool_calls: msg.tool_calls })
245
+
246
+ for (const tc of msg.tool_calls) {
247
+ const toolName = tc.function.name
248
+ let args: Record<string, unknown> = {}
249
+ try { args = JSON.parse(tc.function.arguments) } catch { /* raw string */ }
250
+
251
+ console.log(`${indent}[A2A] LLM calls ${toolName}(${JSON.stringify(args).slice(0, 80)})`)
252
+
253
+ let toolResult: string
254
+
255
+ switch (toolName) {
256
+ case 'agentx_list_agents': {
257
+ const list = allAgents.map(a => `#${a.id} "${a.name}" — ${a.description || '(no description)'}`).join('\n')
258
+ toolResult = `Available agents:\n${list}`
259
+ break
260
+ }
261
+
262
+ case 'agentx_a2a_create_task': {
263
+ const targetId = Number(args.targetAgentId)
264
+ const subTaskType = String(args.taskType || 'analyze')
265
+ const subInput = String(args.inputData || '')
266
+
267
+ if (!targetId || isNaN(targetId)) {
268
+ toolResult = 'Error: targetAgentId is required'
269
+ break
270
+ }
271
+
272
+ const targetAgent = allAgents.find(a => a.id === targetId)
273
+ const targetName = targetAgent?.name || `Agent #${targetId}`
274
+
275
+ // Submit createTask on-chain (permissionless — anyone can call)
276
+ let subTaskId: number
277
+ try {
278
+ subTaskId = await createTaskOnChain(targetId, subTaskType, subInput)
279
+ } catch (err: any) {
280
+ toolResult = `Error creating sub-task: ${err.message}`
281
+ break
282
+ }
283
+
284
+ console.log(`${indent}[A2A] → Delegated to ${targetName} (#${targetId}), sub-task: #${subTaskId}`)
285
+ totalOrchestrated++
286
+
287
+ // Process sub-task recursively inline
288
+ const subOutput = await processTask(
289
+ pool,
290
+ {
291
+ taskId: subTaskId, agentId: targetId, taskType: subTaskType,
292
+ inputData: subInput, outputData: '', status: 0,
293
+ clientAddress: task.clientAddress, createdAt: Math.floor(Date.now() / 1000),
294
+ },
295
+ targetAgent || { id: targetId, name: targetName, owner: '', description: '' },
296
+ tenantId,
297
+ allAgents,
298
+ depth + 1,
299
+ )
300
+
301
+ subTaskResults.push({ taskId: subTaskId, agentName: targetName, output: subOutput })
302
+ toolResult = `Sub-task #${subTaskId} completed by ${targetName}.\nResult: ${subOutput.slice(0, 2000)}`
303
+ break
304
+ }
305
+
306
+ case 'agentx_a2a_get_task': {
307
+ const queryId = Number(args.taskId)
308
+ if (!queryId || isNaN(queryId)) { toolResult = 'Error: taskId required'; break }
309
+ try {
310
+ const provider = new ethers.JsonRpcProvider(config.rpcUrlOxaChain)
311
+ const a2aContract = new ethers.Contract(config.a2aProtocolOxaChain, A2A_ABI, provider)
312
+ const t = await a2aContract.getTask(queryId)
313
+ const statusMap = ['Created', 'Accepted', 'InProgress', 'Completed', 'Failed']
314
+ toolResult = JSON.stringify({
315
+ taskId: Number(t[0]), agentId: Number(t[1]), taskType: String(t[2]),
316
+ status: statusMap[Number(t[5])] || 'Unknown',
317
+ outputData: String(t[4] || ''),
318
+ })
319
+ } catch { toolResult = `Task #${queryId} not found` }
320
+ break
321
+ }
322
+
323
+ default:
324
+ toolResult = `Unknown tool: ${toolName}`
325
+ }
326
+
327
+ messages.push({ role: 'tool', content: toolResult, tool_call_id: tc.id })
328
+ }
329
+ }
330
+
331
+ if (!finalContent && messages.length > 2) {
332
+ // LLM used tools but didn't produce final text — get a summary
333
+ messages.push({ role: 'user', content: 'Based on all the sub-task results above, provide your final combined output. Return ONLY the final result.' })
334
+ const finalResp = await callLLMWithTools(llmConfig, messages, [])
335
+ finalContent = finalResp.choices[0]?.message?.content || 'Task processed via multi-agent orchestration.'
336
+ totalTokens += finalResp.tokens
337
+ }
338
+
339
+ if (!finalContent) finalContent = 'Task processed.'
340
+
341
+ // Store result
342
+ await pool.query(
343
+ `UPDATE a2a_task_results SET status = 2, output_data = $2, llm_model = $3, tokens_used = $4, processed_at = NOW()
344
+ WHERE task_id = $1`,
345
+ [taskId, finalContent, llmConfig.model, totalTokens]
346
+ )
347
+
348
+ const orchestrationNote = subTaskResults.length > 0
349
+ ? ` (orchestrated ${subTaskResults.length} sub-task(s): ${subTaskResults.map(s => `#${s.taskId}→${s.agentName}`).join(', ')})`
350
+ : ''
351
+ console.log(`${indent}[A2A] Task #${taskId} done, tokens: ${totalTokens}${orchestrationNote}`)
352
+
353
+ return finalContent
354
+ } catch (err: any) {
355
+ await pool.query(
356
+ `UPDATE a2a_task_results SET status = 3, error_message = $2, processed_at = NOW() WHERE task_id = $1`,
357
+ [taskId, err.message.slice(0, 500)]
358
+ )
359
+ console.error(`${' '.repeat(depth)}[A2A] Task #${taskId} failed:`, err.message.slice(0, 100))
360
+ throw err
361
+ }
362
+ }
363
+
364
+ // ── On-Chain Helpers ──────────────────────────────────────────────────────
365
+
366
+ async function createTaskOnChain(agentId: number, taskType: string, inputData: string): Promise<number> {
367
+ const workerPk = process.env.A2A_WORKER_PRIVATE_KEY
368
+ if (!workerPk) throw new Error('A2A_WORKER_PRIVATE_KEY not configured — cannot create sub-tasks')
369
+
370
+ const provider = new ethers.JsonRpcProvider(config.rpcUrlOxaChain)
371
+ const wallet = new ethers.Wallet(workerPk, provider)
372
+ const a2aContract = new ethers.Contract(config.a2aProtocolOxaChain, A2A_ABI, wallet)
373
+
374
+ const tx = await a2aContract.createTask(agentId, taskType, inputData)
375
+ const receipt = await tx.wait()
376
+
377
+ // Parse TaskCreated event to get taskId
378
+ for (const log of receipt.logs) {
379
+ if (log.topics.length >= 2) {
380
+ try { return Number(log.topics[1]) } catch { /* */ }
381
+ }
382
+ }
383
+ throw new Error('Could not parse taskId from createTask tx')
384
+ }
385
+
386
+ // ── LLM Helpers ────────────────────────────────────────────────────────────
387
+
388
+ interface LLMConfig {
389
+ endpoint: string
390
+ apiKey: string
391
+ model: string
392
+ }
393
+
394
+ async function resolveLLMConfig(pool: ReturnType<typeof getPool>, tenantId: string | null): Promise<LLMConfig | null> {
395
+ if (tenantId) {
396
+ const { rows } = await pool.query(
397
+ `SELECT * FROM tenant_api_keys WHERE tenant_id = $1 AND is_active = true ORDER BY random() LIMIT 1`, [tenantId]
398
+ )
399
+ if (rows.length > 0) return { endpoint: rows[0].endpoint, apiKey: decryptApiKey(rows[0].api_key, config.masterEncryptionKey), model: rows[0].model || 'deepseek-chat' }
400
+ }
401
+ const { rows: pks } = await pool.query(`SELECT * FROM platform_api_keys WHERE is_active = true ORDER BY random() LIMIT 1`)
402
+ if (pks.length > 0) return { endpoint: pks[0].endpoint, apiKey: decryptApiKey(pks[0].api_key, config.masterEncryptionKey), model: pks[0].models?.[0] || 'deepseek-chat' }
403
+ return null
404
+ }
405
+
406
+ function buildOrchestrationSystemPrompt(agent: AgentInfo, allAgents: AgentInfo[]): string {
407
+ const agentList = allAgents.slice(0, 30).map(a => ` #${a.id} "${a.name}" — ${a.description || 'No description'}`).join('\n')
408
+
409
+ return `You are "${agent.name}", an AI agent on the AgentX decentralized platform.
410
+
411
+ ## Your Role
412
+ ${agent.description || 'Process delegated tasks professionally and return results.'}
413
+
414
+ ## Multi-Agent Orchestration
415
+ You have access to A2A (Agent-to-Agent) tools that let you DELEGATE work to other agents.
416
+ When a task is complex or requires specialized skills, break it down and delegate sub-tasks.
417
+
418
+ ### Available Agents
419
+ ${agentList}
420
+
421
+ ### How to Orchestrate
422
+ 1. Analyze the task — what parts need specialized agents?
423
+ 2. Call agentx_list_agents to see available agents
424
+ 3. Call agentx_a2a_create_task to delegate sub-tasks (each runs autonomously)
425
+ 4. Call agentx_a2a_get_task to check sub-task results
426
+ 5. Aggregate ALL sub-task results into your final output
427
+
428
+ ### Rules
429
+ - Delegate when another agent has relevant expertise
430
+ - Include ALL context in inputData so the sub-agent can work independently
431
+ - After getting sub-task results, synthesize them into a coherent final answer
432
+ - If the task is simple and you can handle it alone, just answer directly
433
+ - Always provide a final answer (don't leave the user hanging)`
434
+ }
435
+
436
+ function buildOrchestrationUserPrompt(task: A2ATaskOnChain): string {
437
+ return `Task Type: ${task.taskType}
438
+ Client: ${task.clientAddress}
439
+
440
+ Task Input:
441
+ ${task.inputData || 'Please process this task.'}
442
+
443
+ Process this task. If it requires multiple specialized skills, use the available A2A tools to delegate sub-tasks to appropriate agents. Return your final combined output.`
444
+ }
445
+
446
+ // ── LLM Call with Tools (ReAct) ──────────────────────────────────────────
447
+
448
+ interface LLMToolCallResponse {
449
+ choices: { message: { role: string; content: string | null; tool_calls?: { id: string; function: { name: string; arguments: string } }[] } }[]
450
+ tokens: number
451
+ }
452
+
453
+ async function callLLMWithTools(
454
+ cfg: LLMConfig,
455
+ messages: { role: string; content: string | null; tool_calls?: any[]; tool_call_id?: string }[],
456
+ tools: typeof A2A_TOOLS,
457
+ ): Promise<LLMToolCallResponse> {
458
+ const body: Record<string, unknown> = {
459
+ model: cfg.model,
460
+ messages,
461
+ max_tokens: 2048,
462
+ temperature: 0.3,
463
+ }
464
+ if (tools.length > 0) {
465
+ body.tools = tools
466
+ body.tool_choice = 'auto'
467
+ }
468
+
469
+ const res = await fetch(`${cfg.endpoint}/chat/completions`, {
470
+ method: 'POST',
471
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${cfg.apiKey}` },
472
+ body: JSON.stringify(body),
473
+ signal: AbortSignal.timeout(120_000),
474
+ })
475
+
476
+ if (!res.ok) {
477
+ const errText = await res.text().catch(() => 'unknown error')
478
+ throw new Error(`LLM call failed (HTTP ${res.status}): ${errText.slice(0, 200)}`)
479
+ }
480
+
481
+ const data = await res.json()
482
+ return {
483
+ choices: data.choices || [],
484
+ tokens: data.usage?.total_tokens || 0,
485
+ }
486
+ }