@balchemyai/agent-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +282 -0
- package/dist/agent-loop/agent-loop.d.ts +54 -0
- package/dist/agent-loop/agent-loop.d.ts.map +1 -0
- package/dist/agent-loop/agent-loop.js +328 -0
- package/dist/agent-loop/agent-loop.js.map +1 -0
- package/dist/agent-loop/decision-handler.d.ts +37 -0
- package/dist/agent-loop/decision-handler.d.ts.map +1 -0
- package/dist/agent-loop/decision-handler.js +91 -0
- package/dist/agent-loop/decision-handler.js.map +1 -0
- package/dist/agent-loop/llm-adapters/anthropic.d.ts +10 -0
- package/dist/agent-loop/llm-adapters/anthropic.d.ts.map +1 -0
- package/dist/agent-loop/llm-adapters/anthropic.js +60 -0
- package/dist/agent-loop/llm-adapters/anthropic.js.map +1 -0
- package/dist/agent-loop/llm-adapters/openai.d.ts +11 -0
- package/dist/agent-loop/llm-adapters/openai.d.ts.map +1 -0
- package/dist/agent-loop/llm-adapters/openai.js +54 -0
- package/dist/agent-loop/llm-adapters/openai.js.map +1 -0
- package/dist/agent-loop/llm-cost-tracker.d.ts +21 -0
- package/dist/agent-loop/llm-cost-tracker.d.ts.map +1 -0
- package/dist/agent-loop/llm-cost-tracker.js +63 -0
- package/dist/agent-loop/llm-cost-tracker.js.map +1 -0
- package/dist/agent-loop/model-router.d.ts +35 -0
- package/dist/agent-loop/model-router.d.ts.map +1 -0
- package/dist/agent-loop/model-router.js +55 -0
- package/dist/agent-loop/model-router.js.map +1 -0
- package/dist/agent-loop/telemetry-reporter.d.ts +60 -0
- package/dist/agent-loop/telemetry-reporter.d.ts.map +1 -0
- package/dist/agent-loop/telemetry-reporter.js +78 -0
- package/dist/agent-loop/telemetry-reporter.js.map +1 -0
- package/dist/agent-loop/types.d.ts +125 -0
- package/dist/agent-loop/types.d.ts.map +1 -0
- package/dist/agent-loop/types.js +10 -0
- package/dist/agent-loop/types.js.map +1 -0
- package/dist/agent-loop/webhook-receiver.d.ts +22 -0
- package/dist/agent-loop/webhook-receiver.d.ts.map +1 -0
- package/dist/agent-loop/webhook-receiver.js +145 -0
- package/dist/agent-loop/webhook-receiver.js.map +1 -0
- package/dist/auth/onboarding.d.ts +12 -0
- package/dist/auth/onboarding.d.ts.map +1 -0
- package/dist/auth/onboarding.js +51 -0
- package/dist/auth/onboarding.js.map +1 -0
- package/dist/auth/token-store.d.ts +44 -0
- package/dist/auth/token-store.d.ts.map +1 -0
- package/dist/auth/token-store.js +67 -0
- package/dist/auth/token-store.js.map +1 -0
- package/dist/client/http-client.d.ts +22 -0
- package/dist/client/http-client.d.ts.map +1 -0
- package/dist/client/http-client.js +127 -0
- package/dist/client/http-client.js.map +1 -0
- package/dist/errors/agent-sdk-error.d.ts +13 -0
- package/dist/errors/agent-sdk-error.d.ts.map +1 -0
- package/dist/errors/agent-sdk-error.js +17 -0
- package/dist/errors/agent-sdk-error.js.map +1 -0
- package/dist/errors/error-codes.d.ts +2 -0
- package/dist/errors/error-codes.d.ts.map +1 -0
- package/dist/errors/error-codes.js +3 -0
- package/dist/errors/error-codes.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/mcp-client.d.ts +184 -0
- package/dist/mcp/mcp-client.d.ts.map +1 -0
- package/dist/mcp/mcp-client.js +469 -0
- package/dist/mcp/mcp-client.js.map +1 -0
- package/dist/streaming/sse-event-stream.d.ts +58 -0
- package/dist/streaming/sse-event-stream.d.ts.map +1 -0
- package/dist/streaming/sse-event-stream.js +263 -0
- package/dist/streaming/sse-event-stream.js.map +1 -0
- package/dist/types.d.ts +151 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/retry.d.ts +30 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +85 -0
- package/dist/utils/retry.js.map +1 -0
- package/package.json +68 -0
package/README.md
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# @balchemyai/agent-sdk
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for Balchemy external AI agents. Handles onboarding (SIWE wallet-based or walletless Identity flow), MCP tool access, token lifecycle management, and real-time SSE event streaming.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @balchemyai/agent-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Auth Paths
|
|
12
|
+
|
|
13
|
+
### Path 1 — SIWE (wallet-based)
|
|
14
|
+
|
|
15
|
+
Use this when your agent controls a Solana or EVM wallet and can sign messages.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { BalchemyAgentSdk } from "@balchemyai/agent-sdk";
|
|
19
|
+
|
|
20
|
+
const sdk = new BalchemyAgentSdk({
|
|
21
|
+
apiBaseUrl: "https://api.balchemy.ai/api",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// 1. Request a nonce and SIWS message
|
|
25
|
+
const { message, nonce } = await sdk.requestSiweNonce({
|
|
26
|
+
address: "YOUR_WALLET_ADDRESS",
|
|
27
|
+
chainId: 8453,
|
|
28
|
+
domain: "youragent.example.com",
|
|
29
|
+
uri: "https://youragent.example.com",
|
|
30
|
+
statement: "Sign in to Balchemy",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// 2. Sign `message` with your wallet (off-chain, using your signing library)
|
|
34
|
+
const signature = await wallet.signMessage(message);
|
|
35
|
+
|
|
36
|
+
// 3. Onboard
|
|
37
|
+
const response = await sdk.onboardWithSiwe({
|
|
38
|
+
message,
|
|
39
|
+
signature,
|
|
40
|
+
agentId: "your-agent-unique-id",
|
|
41
|
+
scope: "trade", // "read" | "trade"
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const mcp = sdk.connectMcp({
|
|
45
|
+
endpoint: response.mcp.endpoint,
|
|
46
|
+
apiKey: response.mcp.apiKey ?? "",
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Path 2 — Identity / Walletless
|
|
51
|
+
|
|
52
|
+
Use this when your agent has an HMAC-signed identity token (for balchemy native) or ES256 JWT (for external providers) from a provider registered with Balchemy.
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { BalchemyAgentSdk } from "@balchemyai/agent-sdk";
|
|
56
|
+
|
|
57
|
+
const sdk = new BalchemyAgentSdk({
|
|
58
|
+
apiBaseUrl: "https://api.balchemy.ai/api",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const response = await sdk.onboardWithIdentity({
|
|
62
|
+
provider: "your-registered-provider-id",
|
|
63
|
+
identityToken: "YOUR_PROVIDER_JWT",
|
|
64
|
+
agentId: "your-agent-unique-id",
|
|
65
|
+
chainId: 8453,
|
|
66
|
+
scope: "trade",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const mcp = sdk.connectMcp({
|
|
70
|
+
endpoint: response.mcp.endpoint,
|
|
71
|
+
apiKey: response.mcp.apiKey ?? "",
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Using the MCP Client
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
// List available tools
|
|
79
|
+
const { tools } = await mcp.listTools();
|
|
80
|
+
|
|
81
|
+
// Natural language query
|
|
82
|
+
const reply = await mcp.askBot({ message: "What is the price of SOL?" });
|
|
83
|
+
|
|
84
|
+
// Execute an agent instruction
|
|
85
|
+
const result = await mcp.agentExecute({
|
|
86
|
+
instruction: "Find a low-risk setup on Base with 50 USDC",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// EVM quote (read-only, no wallet interaction)
|
|
90
|
+
const quote = await mcp.evmQuote({
|
|
91
|
+
chainId: 8453,
|
|
92
|
+
sellToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
|
|
93
|
+
buyToken: "0x4200000000000000000000000000000000000006", // WETH on Base
|
|
94
|
+
sellAmount: "50000000", // 50 USDC (6 decimals)
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// EVM swap (submit: false = pending order, submit: true = on-chain execution)
|
|
98
|
+
const swap = await mcp.evmSwap({
|
|
99
|
+
chainId: 8453,
|
|
100
|
+
sellToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
101
|
+
buyToken: "0x4200000000000000000000000000000000000006",
|
|
102
|
+
sellAmount: "50000000",
|
|
103
|
+
submit: true,
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Tool Exposure and Scope
|
|
108
|
+
|
|
109
|
+
By default the MCP endpoint exposes **7 tools**: `ask_bot`, `trade_command`, `agent_execute`, `agent_research`, `agent_portfolio`, `agent_status`, `agent_config`.
|
|
110
|
+
|
|
111
|
+
The full catalog of **106 tools** is available when the platform flag `MCP_EXPOSE_GRANULAR_TOOLS=true` is enabled on the bot. Contact the Balchemy team to enable granular tool access for your integration.
|
|
112
|
+
|
|
113
|
+
Tool scopes:
|
|
114
|
+
|
|
115
|
+
| Scope | Access |
|
|
116
|
+
|-------|--------|
|
|
117
|
+
| `"read"` | Read-only tools — market data, portfolio views, research |
|
|
118
|
+
| `"trade"` | All read tools + trade execution tools |
|
|
119
|
+
|
|
120
|
+
Pass `scope` during onboarding to receive an MCP key with the appropriate permissions.
|
|
121
|
+
|
|
122
|
+
## Error Handling
|
|
123
|
+
|
|
124
|
+
All SDK methods throw `AgentSdkError` on failure.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import { AgentSdkError } from "@balchemyai/agent-sdk";
|
|
128
|
+
import type { AgentSdkErrorCode } from "@balchemyai/agent-sdk";
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const result = await mcp.agentExecute({ instruction: "..." });
|
|
132
|
+
} catch (err: unknown) {
|
|
133
|
+
if (err instanceof AgentSdkError) {
|
|
134
|
+
const code: AgentSdkErrorCode = err.code;
|
|
135
|
+
// "auth_error" | "policy_error" | "rate_limit" | "provider_auth_error"
|
|
136
|
+
// | "network_error" | "execution_error" | "invalid_response"
|
|
137
|
+
console.error(`[${code}] ${err.message}`, err.details);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Tool Response Helpers
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { getToolText, parseToolJson, isToolError } from "@balchemyai/agent-sdk";
|
|
146
|
+
|
|
147
|
+
const response = await mcp.agentPortfolio();
|
|
148
|
+
|
|
149
|
+
if (isToolError(response)) {
|
|
150
|
+
console.error("Tool returned an error:", getToolText(response));
|
|
151
|
+
} else {
|
|
152
|
+
const data = parseToolJson(response); // T | null
|
|
153
|
+
console.log(data);
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Token Management
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { TokenStore } from "@balchemyai/agent-sdk";
|
|
161
|
+
|
|
162
|
+
const store = new TokenStore({
|
|
163
|
+
// Called when the stored token nears expiry — return a fresh OnboardingResponse
|
|
164
|
+
refreshFn: async () => {
|
|
165
|
+
return sdk.onboardWithIdentity({ ... });
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await store.set(response);
|
|
170
|
+
const token = await store.get(); // auto-refreshes if expiry < threshold
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Identity Access Token
|
|
174
|
+
|
|
175
|
+
The `OnboardingResponse` includes an `identityAccess` field when the platform issues a short-lived access token alongside the MCP key.
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import type { IdentityAccess } from "@balchemyai/agent-sdk";
|
|
179
|
+
|
|
180
|
+
const access: IdentityAccess | undefined = response.identityAccess;
|
|
181
|
+
if (access) {
|
|
182
|
+
console.log(access.scope); // "read" | "trade"
|
|
183
|
+
console.log(access.expiresAt); // ISO timestamp
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## SSE Event Streaming
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import { SseEventStream } from "@balchemyai/agent-sdk";
|
|
191
|
+
import type { SseEvent } from "@balchemyai/agent-sdk";
|
|
192
|
+
|
|
193
|
+
const stream = new SseEventStream(
|
|
194
|
+
"https://api.balchemy.ai/api/events",
|
|
195
|
+
response.mcp.apiKey ?? "",
|
|
196
|
+
{ reconnectDelayMs: 2000, maxReconnects: 5 }
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// Async iterator
|
|
200
|
+
for await (const event of stream) {
|
|
201
|
+
const e: SseEvent = event;
|
|
202
|
+
console.log(e.event, e.data);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Or callback-based
|
|
206
|
+
const unsubscribe = stream.subscribe(
|
|
207
|
+
(event) => console.log(event),
|
|
208
|
+
(err) => console.error(err)
|
|
209
|
+
);
|
|
210
|
+
// later: unsubscribe();
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Identity Token Revocation
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
// Revoke a token by JTI
|
|
217
|
+
await sdk.revokeIdentityToken({ jti: "the-token-jti", ttlSeconds: 86400 });
|
|
218
|
+
|
|
219
|
+
// Check revocation status
|
|
220
|
+
const { revoked } = await sdk.getIdentityTokenRevokeStatus({ jti: "the-token-jti" });
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Platform Endpoints Reference
|
|
224
|
+
|
|
225
|
+
| Endpoint | Path | Auth |
|
|
226
|
+
|----------|------|------|
|
|
227
|
+
| MCP server | `POST /api/mcp/{publicId}` | `Authorization: Bearer <mcp-api-key>` |
|
|
228
|
+
| SIWE nonce | `POST /api/nest/auth/evm/nonce` | Public |
|
|
229
|
+
| SIWE onboarding | `POST /api/public/erc8004/onboarding/siwe` | Public |
|
|
230
|
+
| Walletless onboarding | `POST /api/public/erc8004/onboarding/identity` | Public |
|
|
231
|
+
| Token revoke | `POST /api/public/erc8004/onboarding/tokens/revoke` | Public |
|
|
232
|
+
| JWKS | `GET /.well-known/jwks.json` | Public |
|
|
233
|
+
| MCP discovery | `GET /.well-known/mcp.json` | Public |
|
|
234
|
+
| Agent directory | `GET /api/nest/agents/verified/page` | Public |
|
|
235
|
+
|
|
236
|
+
> Note: the JWKS endpoint is served at `/.well-known/jwks.json` (root-relative, **not** under `/api`).
|
|
237
|
+
|
|
238
|
+
## Platform Operator Setup
|
|
239
|
+
|
|
240
|
+
To enable agent onboarding, the following environment variables must be configured on the backend:
|
|
241
|
+
|
|
242
|
+
| Variable | Required | Description |
|
|
243
|
+
|----------|----------|-------------|
|
|
244
|
+
| `AGENT_WALLETLESS_ONBOARDING_ENABLED` | For walletless path | Set to `true` to enable the identity/walletless onboarding endpoint. Default: `false`. |
|
|
245
|
+
| `SIWE_DOMAIN_ALLOWLIST` | For SIWE path | Comma-separated list of allowed domains for SIWE message verification (e.g. `youragent.example.com,localhost`). |
|
|
246
|
+
| `ERC8004_IDENTITY_PROVIDERS` | For walletless path | Comma-separated list of allowed identity provider IDs (e.g. `github-actions,openclaw`). |
|
|
247
|
+
| `AGENT_IDENTITY_ISSUER_PRIVATE_KEY_PEM` | For identity tokens | ES256 private key (PEM or base64) for signing agent identity access tokens. Required if issuing short-lived identity tokens. |
|
|
248
|
+
| `API_URL` | Always | Base API URL used to build MCP endpoint URLs in onboarding responses (e.g. `https://api.balchemy.ai/api`). |
|
|
249
|
+
|
|
250
|
+
## Getting Started (Quickstart)
|
|
251
|
+
|
|
252
|
+
1. Install the SDK:
|
|
253
|
+
```sh
|
|
254
|
+
npm install @balchemyai/agent-sdk
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
2. Create an MCP API key for your bot via the Hub UI: `Hub > Your Bot > API Keys > Create Key`.
|
|
258
|
+
|
|
259
|
+
3. Choose an auth path:
|
|
260
|
+
- **SIWE (wallet-based):** Your agent must control an EVM wallet and can sign messages.
|
|
261
|
+
- **Walletless (identity token):** Your agent has an HMAC-signed identity token (balchemy native) or ES256 JWT (external provider) from a registered provider. The platform operator must set `AGENT_WALLETLESS_ONBOARDING_ENABLED=true`.
|
|
262
|
+
|
|
263
|
+
4. Run the relevant code example in the [Auth Paths](#auth-paths) section above.
|
|
264
|
+
|
|
265
|
+
5. Use the returned `mcp.endpoint` and `mcp.apiKey` to make MCP tool calls.
|
|
266
|
+
|
|
267
|
+
6. The MCP key is scoped — `"read"` for read-only tools, `"trade"` for trade execution tools. Choose at onboarding time via the `scope` parameter.
|
|
268
|
+
|
|
269
|
+
## Notes
|
|
270
|
+
|
|
271
|
+
- `agent_seed_request` is disabled on the platform. The `requestSeed()` method exists for backward compatibility but always throws a deterministic `AgentSdkError` with code `"execution_error"`.
|
|
272
|
+
- `apiBaseUrl` must include the `/api` path segment and must **not** have a trailing slash.
|
|
273
|
+
- The JWKS endpoint is at `/.well-known/jwks.json` (root-relative). Do not prefix with `/api`.
|
|
274
|
+
- If walletless onboarding returns HTTP 403 with `code: "FEATURE_DISABLED"`, the platform operator needs to set `AGENT_WALLETLESS_ONBOARDING_ENABLED=true`.
|
|
275
|
+
|
|
276
|
+
## Docs
|
|
277
|
+
|
|
278
|
+
- [Error and retry strategy](docs/error-retry-strategy.md)
|
|
279
|
+
- [Python parity backlog](docs/python-parity-backlog.md)
|
|
280
|
+
- [Partner integration checklist](docs/partner-integration-checklist.md)
|
|
281
|
+
- [Release policy](docs/release-policy.md)
|
|
282
|
+
- Full API reference: [https://balchemy.ai/docs](https://balchemy.ai/docs)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { AgentLoopConfig, AgentStatus } from './types';
|
|
2
|
+
export declare class AgentLoop {
|
|
3
|
+
private readonly config;
|
|
4
|
+
private readonly sseEndpoint;
|
|
5
|
+
private readonly costTracker;
|
|
6
|
+
private readonly llm;
|
|
7
|
+
private readonly decisionHandler;
|
|
8
|
+
private readonly mcp;
|
|
9
|
+
private readonly modelRouter;
|
|
10
|
+
private readonly telemetry;
|
|
11
|
+
private webhookReceiver;
|
|
12
|
+
private sseStream;
|
|
13
|
+
private unsubscribeSse;
|
|
14
|
+
private status;
|
|
15
|
+
private startedAt;
|
|
16
|
+
private eventsReceived;
|
|
17
|
+
private decisionsExecuted;
|
|
18
|
+
private tradesExecuted;
|
|
19
|
+
private lastEventAt;
|
|
20
|
+
private lastTradeAt;
|
|
21
|
+
private seenTraceIds;
|
|
22
|
+
private portfolioCache;
|
|
23
|
+
private rulesCache;
|
|
24
|
+
/** publicId extracted from the MCP endpoint path (last path segment). */
|
|
25
|
+
private readonly publicId;
|
|
26
|
+
constructor(config: AgentLoopConfig);
|
|
27
|
+
start(): Promise<void>;
|
|
28
|
+
stop(): Promise<void>;
|
|
29
|
+
getStatus(): AgentStatus;
|
|
30
|
+
/**
|
|
31
|
+
* Send a user message to the bot via the ask_bot MCP tool.
|
|
32
|
+
* Parses the JSON envelope returned by the server and extracts the `reply`
|
|
33
|
+
* field if present; otherwise returns the raw text.
|
|
34
|
+
* On network / tool error, calls `config.onError` and returns an error string.
|
|
35
|
+
*/
|
|
36
|
+
sendMessage(message: string): Promise<string>;
|
|
37
|
+
private handleEvent;
|
|
38
|
+
private processEvent;
|
|
39
|
+
/**
|
|
40
|
+
* Fetch the agent portfolio via `agent_portfolio` MCP tool.
|
|
41
|
+
* Result is cached for 30 seconds. On failure, returns an empty snapshot
|
|
42
|
+
* so the decision loop continues with degraded context.
|
|
43
|
+
*/
|
|
44
|
+
private fetchPortfolio;
|
|
45
|
+
/**
|
|
46
|
+
* Fetch compressed behavior rules from the MCP resource
|
|
47
|
+
* `balchemy://behavior-rules/{publicId}`.
|
|
48
|
+
* Result is cached for 5 minutes. On failure, returns empty string
|
|
49
|
+
* so decisions still proceed without rule context.
|
|
50
|
+
*/
|
|
51
|
+
private fetchBehaviorRules;
|
|
52
|
+
private createLlmAdapter;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=agent-loop.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-loop.d.ts","sourceRoot":"","sources":["../../src/agent-loop/agent-loop.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,eAAe,EACf,WAAW,EAMZ,MAAM,SAAS,CAAC;AAejB,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAiB;IAC7C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAa;IACjC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAoB;IACxC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqB;IACjD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAoB;IAC9C,OAAO,CAAC,eAAe,CAAgC;IACvD,OAAO,CAAC,SAAS,CAA+B;IAChD,OAAO,CAAC,cAAc,CAA6B;IAEnD,OAAO,CAAC,MAAM,CAA8B;IAC5C,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,YAAY,CAAqB;IAEzC,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,UAAU,CAA2B;IAE7C,yEAAyE;IACzE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;gBAEtB,MAAM,EAAE,eAAe;IAuC7B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA0CtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAS3B,SAAS,IAAI,WAAW;IAkBxB;;;;;OAKG;IACG,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAoBnD,OAAO,CAAC,WAAW;YAsCL,YAAY;IAmF1B;;;;OAIG;YACW,cAAc;IAmB5B;;;;;OAKG;YACW,kBAAkB;IAmBhC,OAAO,CAAC,gBAAgB;CAuBzB"}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AgentLoop = void 0;
|
|
4
|
+
const sse_event_stream_1 = require("../streaming/sse-event-stream");
|
|
5
|
+
const mcp_client_1 = require("../mcp/mcp-client");
|
|
6
|
+
const llm_cost_tracker_1 = require("./llm-cost-tracker");
|
|
7
|
+
const decision_handler_1 = require("./decision-handler");
|
|
8
|
+
const webhook_receiver_1 = require("./webhook-receiver");
|
|
9
|
+
const openai_1 = require("./llm-adapters/openai");
|
|
10
|
+
const anthropic_1 = require("./llm-adapters/anthropic");
|
|
11
|
+
const model_router_1 = require("./model-router");
|
|
12
|
+
const telemetry_reporter_1 = require("./telemetry-reporter");
|
|
13
|
+
const PORTFOLIO_TTL_MS = 30_000; // 30 seconds
|
|
14
|
+
const RULES_TTL_MS = 5 * 60_000; // 5 minutes
|
|
15
|
+
class AgentLoop {
|
|
16
|
+
config;
|
|
17
|
+
sseEndpoint;
|
|
18
|
+
costTracker;
|
|
19
|
+
llm;
|
|
20
|
+
decisionHandler;
|
|
21
|
+
mcp;
|
|
22
|
+
modelRouter;
|
|
23
|
+
telemetry;
|
|
24
|
+
webhookReceiver = null;
|
|
25
|
+
sseStream = null;
|
|
26
|
+
unsubscribeSse = null;
|
|
27
|
+
status = 'stopped';
|
|
28
|
+
startedAt = 0;
|
|
29
|
+
eventsReceived = 0;
|
|
30
|
+
decisionsExecuted = 0;
|
|
31
|
+
tradesExecuted = 0;
|
|
32
|
+
lastEventAt;
|
|
33
|
+
lastTradeAt;
|
|
34
|
+
seenTraceIds = new Set();
|
|
35
|
+
portfolioCache = null;
|
|
36
|
+
rulesCache = null;
|
|
37
|
+
/** publicId extracted from the MCP endpoint path (last path segment). */
|
|
38
|
+
publicId;
|
|
39
|
+
constructor(config) {
|
|
40
|
+
this.config = config;
|
|
41
|
+
this.sseEndpoint = config.sseEndpoint ??
|
|
42
|
+
`${config.mcpEndpoint}/events/sse`;
|
|
43
|
+
// Extract publicId from endpoint (last path segment after filtering empty segments)
|
|
44
|
+
this.publicId = config.mcpEndpoint.split('/').filter(Boolean).pop() ?? '';
|
|
45
|
+
this.costTracker = new llm_cost_tracker_1.LlmCostTracker({
|
|
46
|
+
maxDailyUsd: config.maxDailyLlmCost ?? 5,
|
|
47
|
+
});
|
|
48
|
+
this.llm = this.createLlmAdapter();
|
|
49
|
+
this.decisionHandler = new decision_handler_1.DecisionHandler(this.llm, this.costTracker, {
|
|
50
|
+
maxConsecutiveFailures: config.maxConsecutiveFailures ?? 3,
|
|
51
|
+
});
|
|
52
|
+
this.mcp = (0, mcp_client_1.connectMcp)({
|
|
53
|
+
endpoint: config.mcpEndpoint,
|
|
54
|
+
apiKey: config.apiKey,
|
|
55
|
+
fetchFn: config.mcpFetchFn,
|
|
56
|
+
});
|
|
57
|
+
// ModelRouter activates only when both cheapModel and fullModel are configured.
|
|
58
|
+
if (config.cheapModel && config.fullModel) {
|
|
59
|
+
this.modelRouter = new model_router_1.ModelRouter({
|
|
60
|
+
cheapModel: config.cheapModel,
|
|
61
|
+
fullModel: config.fullModel,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
this.modelRouter = null;
|
|
66
|
+
}
|
|
67
|
+
// Derive the telemetry endpoint from the MCP endpoint:
|
|
68
|
+
// https://api.balchemy.ai/mcp/abc123 → https://api.balchemy.ai/api/agent-telemetry/abc123
|
|
69
|
+
const telemetryEndpoint = config.mcpEndpoint.replace(/\/mcp\//, '/api/agent-telemetry/');
|
|
70
|
+
this.telemetry = new telemetry_reporter_1.TelemetryReporter(telemetryEndpoint, config.apiKey);
|
|
71
|
+
}
|
|
72
|
+
async start() {
|
|
73
|
+
this.status = 'starting';
|
|
74
|
+
this.startedAt = Date.now();
|
|
75
|
+
this.telemetry.start();
|
|
76
|
+
// Start webhook receiver if configured
|
|
77
|
+
if (this.config.webhookPort && this.config.webhookSecret) {
|
|
78
|
+
this.webhookReceiver = new webhook_receiver_1.WebhookReceiver({
|
|
79
|
+
secret: this.config.webhookSecret,
|
|
80
|
+
port: this.config.webhookPort,
|
|
81
|
+
});
|
|
82
|
+
await this.webhookReceiver.start((event) => this.handleEvent(event));
|
|
83
|
+
}
|
|
84
|
+
// Start SSE stream
|
|
85
|
+
this.sseStream = new sse_event_stream_1.SseEventStream(this.sseEndpoint, this.config.apiKey, {
|
|
86
|
+
maxReconnects: 0, // unlimited
|
|
87
|
+
reconnectDelayMs: 2000,
|
|
88
|
+
maxReconnectDelayMs: 30_000,
|
|
89
|
+
jitterFactor: 0.25,
|
|
90
|
+
});
|
|
91
|
+
this.unsubscribeSse = this.sseStream.subscribe((sseEvent) => {
|
|
92
|
+
const event = {
|
|
93
|
+
id: sseEvent.id,
|
|
94
|
+
type: sseEvent.event,
|
|
95
|
+
data: sseEvent.data,
|
|
96
|
+
timestamp: Date.now(),
|
|
97
|
+
source: 'sse',
|
|
98
|
+
};
|
|
99
|
+
this.handleEvent(event);
|
|
100
|
+
}, (err) => {
|
|
101
|
+
this.config.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
102
|
+
});
|
|
103
|
+
this.status = 'running';
|
|
104
|
+
this.config.onStatusChange?.(this.getStatus());
|
|
105
|
+
}
|
|
106
|
+
async stop() {
|
|
107
|
+
this.status = 'stopped';
|
|
108
|
+
this.unsubscribeSse?.();
|
|
109
|
+
this.sseStream?.close();
|
|
110
|
+
await this.webhookReceiver?.stop();
|
|
111
|
+
this.telemetry.stop();
|
|
112
|
+
this.config.onStatusChange?.(this.getStatus());
|
|
113
|
+
}
|
|
114
|
+
getStatus() {
|
|
115
|
+
return {
|
|
116
|
+
status: this.status,
|
|
117
|
+
uptime: this.startedAt > 0 ? Date.now() - this.startedAt : 0,
|
|
118
|
+
eventsReceived: this.eventsReceived,
|
|
119
|
+
decisionsExecuted: this.decisionsExecuted,
|
|
120
|
+
tradesExecuted: this.tradesExecuted,
|
|
121
|
+
llmCallsToday: 0,
|
|
122
|
+
llmCostToday: this.costTracker.getTodaySpend(),
|
|
123
|
+
maxDailyLlmCost: this.config.maxDailyLlmCost ?? 5,
|
|
124
|
+
consecutiveLlmFailures: this.decisionHandler.getConsecutiveFailures(),
|
|
125
|
+
lastEventAt: this.lastEventAt,
|
|
126
|
+
lastTradeAt: this.lastTradeAt,
|
|
127
|
+
sseConnected: this.status === 'running',
|
|
128
|
+
webhookActive: this.webhookReceiver !== null,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Send a user message to the bot via the ask_bot MCP tool.
|
|
133
|
+
* Parses the JSON envelope returned by the server and extracts the `reply`
|
|
134
|
+
* field if present; otherwise returns the raw text.
|
|
135
|
+
* On network / tool error, calls `config.onError` and returns an error string.
|
|
136
|
+
*/
|
|
137
|
+
async sendMessage(message) {
|
|
138
|
+
try {
|
|
139
|
+
const response = await this.mcp.callTool('ask_bot', { message });
|
|
140
|
+
const text = response.content?.find((c) => c.type === 'text')?.text ?? '';
|
|
141
|
+
try {
|
|
142
|
+
const parsed = JSON.parse(text);
|
|
143
|
+
const reply = parsed['reply'] ?? parsed['text'];
|
|
144
|
+
return typeof reply === 'string' ? reply : text;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return text;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
152
|
+
this.config.onError?.(new Error(`ask_bot failed: ${msg}`));
|
|
153
|
+
return `Error: ${msg}`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
handleEvent(event) {
|
|
157
|
+
// Deduplicate across SSE + webhook
|
|
158
|
+
if (event.id && this.seenTraceIds.has(event.id))
|
|
159
|
+
return;
|
|
160
|
+
if (event.id) {
|
|
161
|
+
this.seenTraceIds.add(event.id);
|
|
162
|
+
// Limit set size
|
|
163
|
+
if (this.seenTraceIds.size > 10_000) {
|
|
164
|
+
const first = this.seenTraceIds.values().next().value;
|
|
165
|
+
if (first)
|
|
166
|
+
this.seenTraceIds.delete(first);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
this.eventsReceived++;
|
|
170
|
+
this.lastEventAt = Date.now();
|
|
171
|
+
this.config.onEvent?.(event);
|
|
172
|
+
// Check budget
|
|
173
|
+
if (!this.costTracker.canCallLlm()) {
|
|
174
|
+
if (this.status !== 'budget_exhausted') {
|
|
175
|
+
this.status = 'budget_exhausted';
|
|
176
|
+
this.config.onStatusChange?.(this.getStatus());
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// Check if decision handler is paused (too many failures)
|
|
181
|
+
if (this.decisionHandler.isPaused()) {
|
|
182
|
+
if (this.status !== 'llm_failing') {
|
|
183
|
+
this.status = 'llm_failing';
|
|
184
|
+
this.config.onStatusChange?.(this.getStatus());
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// Process asynchronously
|
|
189
|
+
void this.processEvent(event);
|
|
190
|
+
}
|
|
191
|
+
async processEvent(event) {
|
|
192
|
+
try {
|
|
193
|
+
// Fetch portfolio and behavior rules (both cached)
|
|
194
|
+
const [portfolio, compressedRules] = await Promise.all([
|
|
195
|
+
this.fetchPortfolio(),
|
|
196
|
+
this.fetchBehaviorRules(),
|
|
197
|
+
]);
|
|
198
|
+
// Apply model routing if configured
|
|
199
|
+
let selectedModel = null;
|
|
200
|
+
let modelTier = null;
|
|
201
|
+
if (this.modelRouter) {
|
|
202
|
+
const score = this.modelRouter.score(event);
|
|
203
|
+
selectedModel = this.modelRouter.selectModel(event);
|
|
204
|
+
modelTier = score >= 60 ? 'full' : 'cheap';
|
|
205
|
+
this.decisionHandler.setModel(selectedModel);
|
|
206
|
+
this.telemetry.reportModelRoute({
|
|
207
|
+
eventType: event.type,
|
|
208
|
+
score,
|
|
209
|
+
selectedModel,
|
|
210
|
+
tier: modelTier,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
const llmCallStart = Date.now();
|
|
214
|
+
const decision = await this.decisionHandler.handleEvent(event, {
|
|
215
|
+
compressedRules,
|
|
216
|
+
portfolioValue: portfolio.totalValueSol ?? 0,
|
|
217
|
+
portfolioSummary: portfolio.summary,
|
|
218
|
+
});
|
|
219
|
+
const llmLatencyMs = Date.now() - llmCallStart;
|
|
220
|
+
// Report LLM call metrics — DecisionHandler exposes last call stats via getCostTracker
|
|
221
|
+
const lastCall = this.decisionHandler.getLastCallStats();
|
|
222
|
+
if (lastCall) {
|
|
223
|
+
this.telemetry.reportLlmCall({
|
|
224
|
+
model: lastCall.model,
|
|
225
|
+
inputTokens: lastCall.inputTokens,
|
|
226
|
+
outputTokens: lastCall.outputTokens,
|
|
227
|
+
latencyMs: llmLatencyMs,
|
|
228
|
+
costUsd: lastCall.costUsd,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
if (!decision || decision.action === 'hold')
|
|
232
|
+
return;
|
|
233
|
+
this.decisionsExecuted++;
|
|
234
|
+
this.config.onDecision?.(decision);
|
|
235
|
+
this.telemetry.reportDecision({
|
|
236
|
+
action: decision.action,
|
|
237
|
+
token: decision.token,
|
|
238
|
+
amount: decision.amount,
|
|
239
|
+
confidence: decision.confidence,
|
|
240
|
+
reasoning: decision.reasoning,
|
|
241
|
+
});
|
|
242
|
+
// Execute via MCP
|
|
243
|
+
if (decision.action === 'buy' || decision.action === 'sell') {
|
|
244
|
+
const tradeResponse = await this.mcp.callTool('trade_command', {
|
|
245
|
+
intent: decision.action,
|
|
246
|
+
token: decision.token,
|
|
247
|
+
amount: decision.amount,
|
|
248
|
+
});
|
|
249
|
+
this.tradesExecuted++;
|
|
250
|
+
this.lastTradeAt = Date.now();
|
|
251
|
+
// Fire trade result callback
|
|
252
|
+
const resultText = tradeResponse.content?.find((c) => c.type === 'text')?.text ?? '';
|
|
253
|
+
this.config.onTradeResult?.({
|
|
254
|
+
action: decision.action,
|
|
255
|
+
token: decision.token,
|
|
256
|
+
amount: decision.amount,
|
|
257
|
+
response: resultText,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
this.config.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Fetch the agent portfolio via `agent_portfolio` MCP tool.
|
|
267
|
+
* Result is cached for 30 seconds. On failure, returns an empty snapshot
|
|
268
|
+
* so the decision loop continues with degraded context.
|
|
269
|
+
*/
|
|
270
|
+
async fetchPortfolio() {
|
|
271
|
+
const now = Date.now();
|
|
272
|
+
if (this.portfolioCache && (now - this.portfolioCache.fetchedAt) < PORTFOLIO_TTL_MS) {
|
|
273
|
+
return this.portfolioCache.snapshot;
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
const response = await this.mcp.agentPortfolio();
|
|
277
|
+
const parsed = (0, mcp_client_1.parseToolJson)(response);
|
|
278
|
+
const snapshot = parsed ?? {};
|
|
279
|
+
this.portfolioCache = { snapshot, fetchedAt: now };
|
|
280
|
+
return snapshot;
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
// Graceful degradation — continue with empty snapshot
|
|
284
|
+
this.config.onError?.(new Error('agent_portfolio fetch failed — continuing with empty snapshot'));
|
|
285
|
+
return {};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Fetch compressed behavior rules from the MCP resource
|
|
290
|
+
* `balchemy://behavior-rules/{publicId}`.
|
|
291
|
+
* Result is cached for 5 minutes. On failure, returns empty string
|
|
292
|
+
* so decisions still proceed without rule context.
|
|
293
|
+
*/
|
|
294
|
+
async fetchBehaviorRules() {
|
|
295
|
+
const now = Date.now();
|
|
296
|
+
if (this.rulesCache && (now - this.rulesCache.fetchedAt) < RULES_TTL_MS) {
|
|
297
|
+
return this.rulesCache.compressed;
|
|
298
|
+
}
|
|
299
|
+
const uri = `balchemy://behavior-rules/${this.publicId}`;
|
|
300
|
+
try {
|
|
301
|
+
const contents = await this.mcp.readResource(uri);
|
|
302
|
+
const compressed = contents[0]?.text ?? '';
|
|
303
|
+
this.rulesCache = { compressed, fetchedAt: now };
|
|
304
|
+
return compressed;
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
// Graceful degradation — continue without rule context
|
|
308
|
+
this.config.onError?.(new Error('behavior-rules resource fetch failed — continuing without rules'));
|
|
309
|
+
return '';
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
createLlmAdapter() {
|
|
313
|
+
switch (this.config.llmProvider) {
|
|
314
|
+
case 'anthropic':
|
|
315
|
+
return new anthropic_1.AnthropicAdapter(this.config.llmApiKey, this.config.llmModel, this.config.llmTimeoutMs);
|
|
316
|
+
case 'openai':
|
|
317
|
+
return new openai_1.OpenAiAdapter(this.config.llmApiKey, this.config.llmModel, this.config.llmTimeoutMs, this.config.llmBaseUrl);
|
|
318
|
+
case 'custom':
|
|
319
|
+
throw new Error('Custom LLM adapter must be provided via config.llmAdapter');
|
|
320
|
+
default: {
|
|
321
|
+
const exhaustive = this.config.llmProvider;
|
|
322
|
+
throw new Error(`Unknown LLM provider: ${exhaustive}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
exports.AgentLoop = AgentLoop;
|
|
328
|
+
//# sourceMappingURL=agent-loop.js.map
|