@agi-cli/server 0.1.145 → 0.1.147
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/package.json +3 -3
- package/src/events/types.ts +4 -0
- package/src/routes/sessions.ts +104 -0
- package/src/routes/setu.ts +246 -0
- package/src/runtime/agent/runner-setup.ts +1 -0
- package/src/runtime/provider/index.ts +11 -3
- package/src/runtime/provider/setu.ts +45 -3
- package/src/runtime/stream/error-handler.ts +145 -10
- package/src/runtime/topup/manager.ts +110 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agi-cli/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.147",
|
|
4
4
|
"description": "HTTP API server for AGI CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"typecheck": "tsc --noEmit"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@agi-cli/sdk": "0.1.
|
|
33
|
-
"@agi-cli/database": "0.1.
|
|
32
|
+
"@agi-cli/sdk": "0.1.147",
|
|
33
|
+
"@agi-cli/database": "0.1.147",
|
|
34
34
|
"drizzle-orm": "^0.44.5",
|
|
35
35
|
"hono": "^4.9.9",
|
|
36
36
|
"zod": "^4.1.8"
|
package/src/events/types.ts
CHANGED
|
@@ -5,6 +5,10 @@ export type AGIEventType =
|
|
|
5
5
|
| 'setu.payment.signing'
|
|
6
6
|
| 'setu.payment.complete'
|
|
7
7
|
| 'setu.payment.error'
|
|
8
|
+
| 'setu.topup.required'
|
|
9
|
+
| 'setu.topup.method_selected'
|
|
10
|
+
| 'setu.topup.cancelled'
|
|
11
|
+
| 'setu.fiat.checkout_created'
|
|
8
12
|
| 'session.created'
|
|
9
13
|
| 'session.updated'
|
|
10
14
|
| 'message.created'
|
package/src/routes/sessions.ts
CHANGED
|
@@ -696,4 +696,108 @@ export function registerSessionsRoutes(app: Hono) {
|
|
|
696
696
|
newMessages: newMessages.length,
|
|
697
697
|
});
|
|
698
698
|
});
|
|
699
|
+
|
|
700
|
+
// Retry a failed assistant message
|
|
701
|
+
app.post('/v1/sessions/:sessionId/messages/:messageId/retry', async (c) => {
|
|
702
|
+
try {
|
|
703
|
+
const sessionId = c.req.param('sessionId');
|
|
704
|
+
const messageId = c.req.param('messageId');
|
|
705
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
706
|
+
const cfg = await loadConfig(projectRoot);
|
|
707
|
+
const db = await getDb(cfg.projectRoot);
|
|
708
|
+
|
|
709
|
+
// Get the assistant message
|
|
710
|
+
const [assistantMsg] = await db
|
|
711
|
+
.select()
|
|
712
|
+
.from(messages)
|
|
713
|
+
.where(
|
|
714
|
+
and(
|
|
715
|
+
eq(messages.id, messageId),
|
|
716
|
+
eq(messages.sessionId, sessionId),
|
|
717
|
+
eq(messages.role, 'assistant'),
|
|
718
|
+
),
|
|
719
|
+
)
|
|
720
|
+
.limit(1);
|
|
721
|
+
|
|
722
|
+
if (!assistantMsg) {
|
|
723
|
+
return c.json({ error: 'Message not found' }, 404);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Only allow retry on error or complete messages
|
|
727
|
+
if (
|
|
728
|
+
assistantMsg.status !== 'error' &&
|
|
729
|
+
assistantMsg.status !== 'complete'
|
|
730
|
+
) {
|
|
731
|
+
return c.json(
|
|
732
|
+
{ error: 'Can only retry error or complete messages' },
|
|
733
|
+
400,
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Get session for context
|
|
738
|
+
const [session] = await db
|
|
739
|
+
.select()
|
|
740
|
+
.from(sessions)
|
|
741
|
+
.where(eq(sessions.id, sessionId))
|
|
742
|
+
.limit(1);
|
|
743
|
+
|
|
744
|
+
if (!session) {
|
|
745
|
+
return c.json({ error: 'Session not found' }, 404);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Delete existing message parts (the error content)
|
|
749
|
+
await db
|
|
750
|
+
.delete(messageParts)
|
|
751
|
+
.where(eq(messageParts.messageId, messageId));
|
|
752
|
+
|
|
753
|
+
// Reset message status to pending
|
|
754
|
+
await db
|
|
755
|
+
.update(messages)
|
|
756
|
+
.set({
|
|
757
|
+
status: 'pending',
|
|
758
|
+
error: null,
|
|
759
|
+
errorType: null,
|
|
760
|
+
errorDetails: null,
|
|
761
|
+
completedAt: null,
|
|
762
|
+
})
|
|
763
|
+
.where(eq(messages.id, messageId));
|
|
764
|
+
|
|
765
|
+
// Emit event so UI updates
|
|
766
|
+
const { publish } = await import('../events/bus.ts');
|
|
767
|
+
publish({
|
|
768
|
+
type: 'message.updated',
|
|
769
|
+
sessionId,
|
|
770
|
+
payload: { id: messageId, status: 'pending' },
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
// Re-enqueue the assistant run
|
|
774
|
+
const { enqueueAssistantRun } = await import(
|
|
775
|
+
'../runtime/session/queue.ts'
|
|
776
|
+
);
|
|
777
|
+
const { runSessionLoop } = await import('../runtime/agent/runner.ts');
|
|
778
|
+
|
|
779
|
+
const toolApprovalMode = cfg.defaults.toolApproval ?? 'auto';
|
|
780
|
+
|
|
781
|
+
enqueueAssistantRun(
|
|
782
|
+
{
|
|
783
|
+
sessionId,
|
|
784
|
+
assistantMessageId: messageId,
|
|
785
|
+
agent: assistantMsg.agent ?? 'build',
|
|
786
|
+
provider: (assistantMsg.provider ??
|
|
787
|
+
cfg.defaults.provider) as ProviderId,
|
|
788
|
+
model: assistantMsg.model ?? cfg.defaults.model,
|
|
789
|
+
projectRoot: cfg.projectRoot,
|
|
790
|
+
oneShot: false,
|
|
791
|
+
toolApprovalMode,
|
|
792
|
+
},
|
|
793
|
+
runSessionLoop,
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
return c.json({ success: true, messageId });
|
|
797
|
+
} catch (err) {
|
|
798
|
+
logger.error('Failed to retry message', err);
|
|
799
|
+
const errorResponse = serializeError(err);
|
|
800
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
801
|
+
}
|
|
802
|
+
});
|
|
699
803
|
}
|
package/src/routes/setu.ts
CHANGED
|
@@ -8,6 +8,24 @@ import {
|
|
|
8
8
|
} from '@agi-cli/sdk';
|
|
9
9
|
import { logger } from '@agi-cli/sdk';
|
|
10
10
|
import { serializeError } from '../runtime/errors/api-error.ts';
|
|
11
|
+
import { Keypair } from '@solana/web3.js';
|
|
12
|
+
import bs58 from 'bs58';
|
|
13
|
+
import nacl from 'tweetnacl';
|
|
14
|
+
import { publish } from '../events/bus.ts';
|
|
15
|
+
import {
|
|
16
|
+
resolveTopupMethodSelection,
|
|
17
|
+
rejectTopupSelection,
|
|
18
|
+
getPendingTopup,
|
|
19
|
+
type TopupMethod,
|
|
20
|
+
} from '../runtime/topup/manager.ts';
|
|
21
|
+
|
|
22
|
+
const SETU_BASE_URL = process.env.SETU_BASE_URL || 'https://setu.agi.nitish.sh';
|
|
23
|
+
|
|
24
|
+
function getSetuBaseUrl(): string {
|
|
25
|
+
return SETU_BASE_URL.endsWith('/')
|
|
26
|
+
? SETU_BASE_URL.slice(0, -1)
|
|
27
|
+
: SETU_BASE_URL;
|
|
28
|
+
}
|
|
11
29
|
|
|
12
30
|
async function getSetuPrivateKey(): Promise<string | null> {
|
|
13
31
|
if (process.env.SETU_PRIVATE_KEY) {
|
|
@@ -25,6 +43,25 @@ async function getSetuPrivateKey(): Promise<string | null> {
|
|
|
25
43
|
return null;
|
|
26
44
|
}
|
|
27
45
|
|
|
46
|
+
function signNonce(nonce: string, privateKeyBytes: Uint8Array): string {
|
|
47
|
+
const data = new TextEncoder().encode(nonce);
|
|
48
|
+
const signature = nacl.sign.detached(data, privateKeyBytes);
|
|
49
|
+
return bs58.encode(signature);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildWalletHeaders(privateKey: string): Record<string, string> {
|
|
53
|
+
const privateKeyBytes = bs58.decode(privateKey);
|
|
54
|
+
const keypair = Keypair.fromSecretKey(privateKeyBytes);
|
|
55
|
+
const walletAddress = keypair.publicKey.toBase58();
|
|
56
|
+
const nonce = Date.now().toString();
|
|
57
|
+
const signature = signNonce(nonce, privateKeyBytes);
|
|
58
|
+
return {
|
|
59
|
+
'x-wallet-address': walletAddress,
|
|
60
|
+
'x-wallet-nonce': nonce,
|
|
61
|
+
'x-wallet-signature': signature,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
28
65
|
export function registerSetuRoutes(app: Hono) {
|
|
29
66
|
app.get('/v1/setu/balance', async (c) => {
|
|
30
67
|
try {
|
|
@@ -97,4 +134,213 @@ export function registerSetuRoutes(app: Hono) {
|
|
|
97
134
|
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
98
135
|
}
|
|
99
136
|
});
|
|
137
|
+
|
|
138
|
+
app.get('/v1/setu/topup/polar/estimate', async (c) => {
|
|
139
|
+
try {
|
|
140
|
+
const amount = c.req.query('amount');
|
|
141
|
+
if (!amount) {
|
|
142
|
+
return c.json({ error: 'Missing amount parameter' }, 400);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const baseUrl = getSetuBaseUrl();
|
|
146
|
+
const response = await fetch(
|
|
147
|
+
`${baseUrl}/v1/topup/polar/estimate?amount=${amount}`,
|
|
148
|
+
{
|
|
149
|
+
method: 'GET',
|
|
150
|
+
headers: { 'Content-Type': 'application/json' },
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const data = await response.json();
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
return c.json(data, response.status as 400 | 500);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return c.json(data);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
logger.error('Failed to get Polar estimate', error);
|
|
162
|
+
const errorResponse = serializeError(error);
|
|
163
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
app.post('/v1/setu/topup/polar', async (c) => {
|
|
168
|
+
try {
|
|
169
|
+
const privateKey = await getSetuPrivateKey();
|
|
170
|
+
if (!privateKey) {
|
|
171
|
+
return c.json({ error: 'Setu wallet not configured' }, 401);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const body = await c.req.json();
|
|
175
|
+
const { amount, successUrl } = body as {
|
|
176
|
+
amount: number;
|
|
177
|
+
successUrl: string;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
if (!amount || typeof amount !== 'number') {
|
|
181
|
+
return c.json({ error: 'Invalid amount' }, 400);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!successUrl || typeof successUrl !== 'string') {
|
|
185
|
+
return c.json({ error: 'Missing successUrl' }, 400);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const walletHeaders = buildWalletHeaders(privateKey);
|
|
189
|
+
const baseUrl = getSetuBaseUrl();
|
|
190
|
+
|
|
191
|
+
const response = await fetch(`${baseUrl}/v1/topup/polar`, {
|
|
192
|
+
method: 'POST',
|
|
193
|
+
headers: {
|
|
194
|
+
'Content-Type': 'application/json',
|
|
195
|
+
...walletHeaders,
|
|
196
|
+
},
|
|
197
|
+
body: JSON.stringify({ amount, successUrl }),
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const data = await response.json();
|
|
201
|
+
if (!response.ok) {
|
|
202
|
+
return c.json(data, response.status as 400 | 500);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return c.json(data);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
logger.error('Failed to create Polar checkout', error);
|
|
208
|
+
const errorResponse = serializeError(error);
|
|
209
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
app.post('/v1/setu/topup/select', async (c) => {
|
|
214
|
+
try {
|
|
215
|
+
const body = await c.req.json();
|
|
216
|
+
const { sessionId, method } = body as {
|
|
217
|
+
sessionId: string;
|
|
218
|
+
method: TopupMethod;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
if (!sessionId || typeof sessionId !== 'string') {
|
|
222
|
+
return c.json({ error: 'Missing sessionId' }, 400);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!method || !['crypto', 'fiat'].includes(method)) {
|
|
226
|
+
return c.json(
|
|
227
|
+
{ error: 'Invalid method, must be "crypto" or "fiat"' },
|
|
228
|
+
400,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const resolved = resolveTopupMethodSelection(sessionId, method);
|
|
233
|
+
if (!resolved) {
|
|
234
|
+
return c.json(
|
|
235
|
+
{ error: 'No pending topup request found for this session' },
|
|
236
|
+
404,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
publish({
|
|
241
|
+
type: 'setu.topup.method_selected',
|
|
242
|
+
sessionId,
|
|
243
|
+
payload: { method },
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return c.json({ success: true, method });
|
|
247
|
+
} catch (error) {
|
|
248
|
+
logger.error('Failed to select topup method', error);
|
|
249
|
+
const errorResponse = serializeError(error);
|
|
250
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
app.post('/v1/setu/topup/cancel', async (c) => {
|
|
255
|
+
try {
|
|
256
|
+
const body = await c.req.json();
|
|
257
|
+
const { sessionId, reason } = body as {
|
|
258
|
+
sessionId: string;
|
|
259
|
+
reason?: string;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
if (!sessionId || typeof sessionId !== 'string') {
|
|
263
|
+
return c.json({ error: 'Missing sessionId' }, 400);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const rejected = rejectTopupSelection(
|
|
267
|
+
sessionId,
|
|
268
|
+
reason ?? 'User cancelled',
|
|
269
|
+
);
|
|
270
|
+
if (!rejected) {
|
|
271
|
+
return c.json(
|
|
272
|
+
{ error: 'No pending topup request found for this session' },
|
|
273
|
+
404,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
publish({
|
|
278
|
+
type: 'setu.topup.cancelled',
|
|
279
|
+
sessionId,
|
|
280
|
+
payload: { reason: reason ?? 'User cancelled' },
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
return c.json({ success: true });
|
|
284
|
+
} catch (error) {
|
|
285
|
+
logger.error('Failed to cancel topup', error);
|
|
286
|
+
const errorResponse = serializeError(error);
|
|
287
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
app.get('/v1/setu/topup/pending', async (c) => {
|
|
292
|
+
try {
|
|
293
|
+
const sessionId = c.req.query('sessionId');
|
|
294
|
+
if (!sessionId) {
|
|
295
|
+
return c.json({ error: 'Missing sessionId parameter' }, 400);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const pending = getPendingTopup(sessionId);
|
|
299
|
+
if (!pending) {
|
|
300
|
+
return c.json({ hasPending: false });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return c.json({
|
|
304
|
+
hasPending: true,
|
|
305
|
+
sessionId: pending.sessionId,
|
|
306
|
+
messageId: pending.messageId,
|
|
307
|
+
amountUsd: pending.amountUsd,
|
|
308
|
+
currentBalance: pending.currentBalance,
|
|
309
|
+
createdAt: pending.createdAt,
|
|
310
|
+
});
|
|
311
|
+
} catch (error) {
|
|
312
|
+
logger.error('Failed to get pending topup', error);
|
|
313
|
+
const errorResponse = serializeError(error);
|
|
314
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
app.get('/v1/setu/topup/polar/status', async (c) => {
|
|
319
|
+
try {
|
|
320
|
+
const checkoutId = c.req.query('checkoutId');
|
|
321
|
+
if (!checkoutId) {
|
|
322
|
+
return c.json({ error: 'Missing checkoutId parameter' }, 400);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const baseUrl = getSetuBaseUrl();
|
|
326
|
+
const response = await fetch(
|
|
327
|
+
`${baseUrl}/v1/topup/polar/status?checkoutId=${checkoutId}`,
|
|
328
|
+
{
|
|
329
|
+
method: 'GET',
|
|
330
|
+
headers: { 'Content-Type': 'application/json' },
|
|
331
|
+
},
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
const data = await response.json();
|
|
335
|
+
if (!response.ok) {
|
|
336
|
+
return c.json(data, response.status as 400 | 500);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return c.json(data);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
logger.error('Failed to check Polar status', error);
|
|
342
|
+
const errorResponse = serializeError(error);
|
|
343
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
100
346
|
}
|
|
@@ -201,6 +201,7 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
201
201
|
const model = await resolveModel(opts.provider, opts.model, cfg, {
|
|
202
202
|
systemPrompt: oauthSystemPrompt,
|
|
203
203
|
sessionId: opts.sessionId,
|
|
204
|
+
messageId: opts.assistantMessageId,
|
|
204
205
|
});
|
|
205
206
|
debugLog(
|
|
206
207
|
`[RUNNER] Model created: ${JSON.stringify({ id: model.modelId, provider: model.provider })}`,
|
|
@@ -3,7 +3,7 @@ import { getAnthropicInstance } from './anthropic.ts';
|
|
|
3
3
|
import { resolveOpenAIModel } from './openai.ts';
|
|
4
4
|
import { resolveGoogleModel } from './google.ts';
|
|
5
5
|
import { resolveOpenRouterModel } from './openrouter.ts';
|
|
6
|
-
import { resolveSetuModel } from './setu.ts';
|
|
6
|
+
import { resolveSetuModel, type ResolveSetuModelOptions } from './setu.ts';
|
|
7
7
|
import { getZaiInstance, getZaiCodingInstance } from './zai.ts';
|
|
8
8
|
import { resolveOpencodeModel } from './opencode.ts';
|
|
9
9
|
import { getMoonshotInstance } from './moonshot.ts';
|
|
@@ -14,7 +14,12 @@ export async function resolveModel(
|
|
|
14
14
|
provider: ProviderName,
|
|
15
15
|
model: string,
|
|
16
16
|
cfg: AGIConfig,
|
|
17
|
-
options?: {
|
|
17
|
+
options?: {
|
|
18
|
+
systemPrompt?: string;
|
|
19
|
+
sessionId?: string;
|
|
20
|
+
messageId?: string;
|
|
21
|
+
topupApprovalMode?: ResolveSetuModelOptions['topupApprovalMode'];
|
|
22
|
+
},
|
|
18
23
|
) {
|
|
19
24
|
if (provider === 'openai') {
|
|
20
25
|
return resolveOpenAIModel(model, cfg, {
|
|
@@ -35,7 +40,10 @@ export async function resolveModel(
|
|
|
35
40
|
return resolveOpencodeModel(model, cfg);
|
|
36
41
|
}
|
|
37
42
|
if (provider === 'setu') {
|
|
38
|
-
return resolveSetuModel(model, options?.sessionId
|
|
43
|
+
return resolveSetuModel(model, options?.sessionId, {
|
|
44
|
+
messageId: options?.messageId,
|
|
45
|
+
topupApprovalMode: options?.topupApprovalMode,
|
|
46
|
+
});
|
|
39
47
|
}
|
|
40
48
|
if (provider === 'zai') {
|
|
41
49
|
return getZaiInstance(cfg, model);
|
|
@@ -4,13 +4,28 @@ import {
|
|
|
4
4
|
type SetuPaymentCallbacks,
|
|
5
5
|
} from '@agi-cli/sdk';
|
|
6
6
|
import { publish } from '../../events/bus.ts';
|
|
7
|
+
import {
|
|
8
|
+
waitForTopupMethodSelection,
|
|
9
|
+
type TopupMethod,
|
|
10
|
+
} from '../topup/manager.ts';
|
|
11
|
+
|
|
12
|
+
const MIN_TOPUP_USD = 5;
|
|
7
13
|
|
|
8
14
|
function getProviderNpm(model: string): string | undefined {
|
|
9
15
|
const entry = catalog.setu?.models?.find((m) => m.id === model);
|
|
10
16
|
return entry?.provider?.npm;
|
|
11
17
|
}
|
|
12
18
|
|
|
13
|
-
export
|
|
19
|
+
export interface ResolveSetuModelOptions {
|
|
20
|
+
messageId?: string;
|
|
21
|
+
topupApprovalMode?: 'auto' | 'approval';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveSetuModel(
|
|
25
|
+
model: string,
|
|
26
|
+
sessionId?: string,
|
|
27
|
+
options: ResolveSetuModelOptions = {},
|
|
28
|
+
) {
|
|
14
29
|
const privateKey = process.env.SETU_PRIVATE_KEY ?? '';
|
|
15
30
|
if (!privateKey) {
|
|
16
31
|
throw new Error(
|
|
@@ -19,14 +34,15 @@ export function resolveSetuModel(model: string, sessionId?: string) {
|
|
|
19
34
|
}
|
|
20
35
|
const baseURL = process.env.SETU_BASE_URL;
|
|
21
36
|
const rpcURL = process.env.SETU_SOLANA_RPC_URL;
|
|
37
|
+
const { messageId, topupApprovalMode = 'approval' } = options;
|
|
22
38
|
|
|
23
39
|
const callbacks: SetuPaymentCallbacks = sessionId
|
|
24
40
|
? {
|
|
25
|
-
onPaymentRequired: (amountUsd) => {
|
|
41
|
+
onPaymentRequired: (amountUsd, currentBalance) => {
|
|
26
42
|
publish({
|
|
27
43
|
type: 'setu.payment.required',
|
|
28
44
|
sessionId,
|
|
29
|
-
payload: { amountUsd },
|
|
45
|
+
payload: { amountUsd, currentBalance },
|
|
30
46
|
});
|
|
31
47
|
},
|
|
32
48
|
onPaymentSigning: () => {
|
|
@@ -50,6 +66,31 @@ export function resolveSetuModel(model: string, sessionId?: string) {
|
|
|
50
66
|
payload: { error },
|
|
51
67
|
});
|
|
52
68
|
},
|
|
69
|
+
onPaymentApproval: async (info): Promise<TopupMethod | 'cancel'> => {
|
|
70
|
+
const suggestedTopupUsd = Math.max(
|
|
71
|
+
MIN_TOPUP_USD,
|
|
72
|
+
Math.ceil(info.amountUsd * 2),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
publish({
|
|
76
|
+
type: 'setu.topup.required',
|
|
77
|
+
sessionId,
|
|
78
|
+
payload: {
|
|
79
|
+
messageId,
|
|
80
|
+
amountUsd: info.amountUsd,
|
|
81
|
+
currentBalance: info.currentBalance,
|
|
82
|
+
minTopupUsd: MIN_TOPUP_USD,
|
|
83
|
+
suggestedTopupUsd,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return waitForTopupMethodSelection(
|
|
88
|
+
sessionId,
|
|
89
|
+
messageId ?? '',
|
|
90
|
+
info.amountUsd,
|
|
91
|
+
info.currentBalance,
|
|
92
|
+
);
|
|
93
|
+
},
|
|
53
94
|
}
|
|
54
95
|
: {};
|
|
55
96
|
|
|
@@ -63,6 +104,7 @@ export function resolveSetuModel(model: string, sessionId?: string) {
|
|
|
63
104
|
rpcURL,
|
|
64
105
|
callbacks,
|
|
65
106
|
providerNpm,
|
|
107
|
+
topupApprovalMode,
|
|
66
108
|
},
|
|
67
109
|
);
|
|
68
110
|
}
|
|
@@ -9,6 +9,7 @@ import type { ToolAdapterContext } from '../../tools/adapter.ts';
|
|
|
9
9
|
import { pruneSession, performAutoCompaction } from '../message/compaction.ts';
|
|
10
10
|
import { debugLog } from '../debug/index.ts';
|
|
11
11
|
import { enqueueAssistantRun } from '../session/queue.ts';
|
|
12
|
+
import { clearPendingTopup } from '../topup/manager.ts';
|
|
12
13
|
|
|
13
14
|
export function createErrorHandler(
|
|
14
15
|
opts: RunOpts,
|
|
@@ -26,21 +27,155 @@ export function createErrorHandler(
|
|
|
26
27
|
const nestedError = (errObj?.error as Record<string, unknown>)?.error as
|
|
27
28
|
| Record<string, unknown>
|
|
28
29
|
| undefined;
|
|
30
|
+
const causeError = errObj?.cause as Record<string, unknown> | undefined;
|
|
31
|
+
|
|
32
|
+
// Check for SETU_FIAT_SELECTED code specifically (not string matching)
|
|
29
33
|
const errorCode =
|
|
30
|
-
(errObj?.code as string) ??
|
|
34
|
+
(errObj?.code as string) ??
|
|
35
|
+
((errObj?.error as Record<string, unknown>)?.code as string) ??
|
|
36
|
+
((
|
|
37
|
+
(errObj?.error as Record<string, unknown>)?.error as Record<
|
|
38
|
+
string,
|
|
39
|
+
unknown
|
|
40
|
+
>
|
|
41
|
+
)?.code as string) ??
|
|
42
|
+
((errObj?.data as Record<string, unknown>)?.code as string) ??
|
|
43
|
+
((errObj?.cause as Record<string, unknown>)?.code as string) ??
|
|
44
|
+
((
|
|
45
|
+
(errObj?.cause as Record<string, unknown>)?.error as Record<
|
|
46
|
+
string,
|
|
47
|
+
unknown
|
|
48
|
+
>
|
|
49
|
+
)?.code as string) ??
|
|
50
|
+
(nestedError?.code as string) ??
|
|
51
|
+
(causeError?.code as string) ??
|
|
52
|
+
'';
|
|
53
|
+
|
|
54
|
+
// Also check error message for the exact fiat selection message
|
|
55
|
+
const errorMessage =
|
|
56
|
+
(errObj?.message as string) ??
|
|
57
|
+
((errObj?.error as Record<string, unknown>)?.message as string) ??
|
|
58
|
+
((
|
|
59
|
+
(errObj?.error as Record<string, unknown>)?.error as Record<
|
|
60
|
+
string,
|
|
61
|
+
unknown
|
|
62
|
+
>
|
|
63
|
+
)?.message as string) ??
|
|
64
|
+
((errObj?.data as Record<string, unknown>)?.message as string) ??
|
|
65
|
+
((errObj?.cause as Record<string, unknown>)?.message as string) ??
|
|
66
|
+
((
|
|
67
|
+
(errObj?.cause as Record<string, unknown>)?.error as Record<
|
|
68
|
+
string,
|
|
69
|
+
unknown
|
|
70
|
+
>
|
|
71
|
+
)?.message as string) ??
|
|
72
|
+
(nestedError?.message as string) ??
|
|
73
|
+
(causeError?.message as string) ??
|
|
74
|
+
'';
|
|
75
|
+
|
|
76
|
+
// Also do a JSON stringify check specifically for the code
|
|
77
|
+
const fullErrorStr = JSON.stringify(err);
|
|
78
|
+
const hasSetuFiatCode =
|
|
79
|
+
fullErrorStr.includes('"code":"SETU_FIAT_SELECTED"') ||
|
|
80
|
+
fullErrorStr.includes("'code':'SETU_FIAT_SELECTED'");
|
|
81
|
+
|
|
82
|
+
// Only match if the error code is SETU_FIAT_SELECTED OR the exact error message
|
|
83
|
+
const isFiatSelected =
|
|
84
|
+
errorCode === 'SETU_FIAT_SELECTED' ||
|
|
85
|
+
errorMessage === 'Setu: fiat payment selected' ||
|
|
86
|
+
hasSetuFiatCode;
|
|
87
|
+
|
|
88
|
+
// Handle fiat payment selected - this is not an error, just a signal to pause
|
|
89
|
+
if (isFiatSelected) {
|
|
90
|
+
debugLog('[stream-handlers] Fiat payment selected, pausing request');
|
|
91
|
+
clearPendingTopup(opts.sessionId);
|
|
92
|
+
|
|
93
|
+
// Add a helpful message part telling user to complete payment
|
|
94
|
+
const partId = crypto.randomUUID();
|
|
95
|
+
await db.insert(messageParts).values({
|
|
96
|
+
id: partId,
|
|
97
|
+
messageId: opts.assistantMessageId,
|
|
98
|
+
index: await sharedCtx.nextIndex(),
|
|
99
|
+
stepIndex: getStepIndex(),
|
|
100
|
+
type: 'error',
|
|
101
|
+
content: JSON.stringify({
|
|
102
|
+
message: 'Balance too low — Complete your top-up, then retry.',
|
|
103
|
+
type: 'balance_low',
|
|
104
|
+
errorType: 'balance_low',
|
|
105
|
+
isRetryable: true,
|
|
106
|
+
}),
|
|
107
|
+
agent: opts.agent,
|
|
108
|
+
provider: opts.provider,
|
|
109
|
+
model: opts.model,
|
|
110
|
+
startedAt: Date.now(),
|
|
111
|
+
completedAt: Date.now(),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Mark the message as completed (not error, not pending)
|
|
115
|
+
await db
|
|
116
|
+
.update(messages)
|
|
117
|
+
.set({
|
|
118
|
+
status: 'complete',
|
|
119
|
+
completedAt: Date.now(),
|
|
120
|
+
error: null,
|
|
121
|
+
errorType: null,
|
|
122
|
+
errorDetails: null,
|
|
123
|
+
})
|
|
124
|
+
.where(eq(messages.id, opts.assistantMessageId));
|
|
125
|
+
|
|
126
|
+
// Emit the message part
|
|
127
|
+
publish({
|
|
128
|
+
type: 'message.part.delta',
|
|
129
|
+
sessionId: opts.sessionId,
|
|
130
|
+
payload: {
|
|
131
|
+
messageId: opts.assistantMessageId,
|
|
132
|
+
partId,
|
|
133
|
+
type: 'error',
|
|
134
|
+
content: JSON.stringify({
|
|
135
|
+
message: 'Balance too low — Complete your top-up, then retry.',
|
|
136
|
+
type: 'balance_low',
|
|
137
|
+
errorType: 'balance_low',
|
|
138
|
+
isRetryable: true,
|
|
139
|
+
}),
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Emit message completed
|
|
144
|
+
publish({
|
|
145
|
+
type: 'message.completed',
|
|
146
|
+
sessionId: opts.sessionId,
|
|
147
|
+
payload: {
|
|
148
|
+
id: opts.assistantMessageId,
|
|
149
|
+
fiatTopupRequired: true,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Emit a special event so UI knows to show topup modal
|
|
154
|
+
publish({
|
|
155
|
+
type: 'setu.fiat.checkout_created',
|
|
156
|
+
sessionId: opts.sessionId,
|
|
157
|
+
payload: {
|
|
158
|
+
messageId: opts.assistantMessageId,
|
|
159
|
+
needsTopup: true,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
31
166
|
const errorType =
|
|
32
167
|
(errObj?.apiErrorType as string) ?? (nestedError?.type as string) ?? '';
|
|
33
|
-
const
|
|
168
|
+
const fullErrorStrLower = JSON.stringify(err).toLowerCase();
|
|
34
169
|
|
|
35
170
|
const isPromptTooLong =
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
171
|
+
fullErrorStrLower.includes('prompt is too long') ||
|
|
172
|
+
fullErrorStrLower.includes('maximum context length') ||
|
|
173
|
+
fullErrorStrLower.includes('too many tokens') ||
|
|
174
|
+
fullErrorStrLower.includes('context_length_exceeded') ||
|
|
175
|
+
fullErrorStrLower.includes('request too large') ||
|
|
176
|
+
fullErrorStrLower.includes('exceeds the model') ||
|
|
177
|
+
fullErrorStrLower.includes('context window') ||
|
|
178
|
+
fullErrorStrLower.includes('input is too long') ||
|
|
44
179
|
errorCode === 'context_length_exceeded' ||
|
|
45
180
|
errorType === 'invalid_request_error';
|
|
46
181
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { logger } from '@agi-cli/sdk';
|
|
2
|
+
|
|
3
|
+
export type TopupMethod = 'crypto' | 'fiat';
|
|
4
|
+
|
|
5
|
+
export interface PendingTopup {
|
|
6
|
+
sessionId: string;
|
|
7
|
+
messageId: string;
|
|
8
|
+
amountUsd: number;
|
|
9
|
+
currentBalance: number;
|
|
10
|
+
resolve: (method: TopupMethod) => void;
|
|
11
|
+
reject: (error: Error) => void;
|
|
12
|
+
createdAt: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const TOPUP_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
16
|
+
|
|
17
|
+
const pendingTopups = new Map<string, PendingTopup>();
|
|
18
|
+
const timeoutIds = new Map<string, ReturnType<typeof setTimeout>>();
|
|
19
|
+
|
|
20
|
+
export function waitForTopupMethodSelection(
|
|
21
|
+
sessionId: string,
|
|
22
|
+
messageId: string,
|
|
23
|
+
amountUsd: number,
|
|
24
|
+
currentBalance: number,
|
|
25
|
+
): Promise<TopupMethod> {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const existing = pendingTopups.get(sessionId);
|
|
28
|
+
if (existing) {
|
|
29
|
+
existing.reject(new Error('Superseded by new topup request'));
|
|
30
|
+
clearPendingTopup(sessionId);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const pending: PendingTopup = {
|
|
34
|
+
sessionId,
|
|
35
|
+
messageId,
|
|
36
|
+
amountUsd,
|
|
37
|
+
currentBalance,
|
|
38
|
+
resolve,
|
|
39
|
+
reject,
|
|
40
|
+
createdAt: Date.now(),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
pendingTopups.set(sessionId, pending);
|
|
44
|
+
|
|
45
|
+
const timeoutId = setTimeout(() => {
|
|
46
|
+
const p = pendingTopups.get(sessionId);
|
|
47
|
+
if (p) {
|
|
48
|
+
logger.warn(`Topup selection timeout for session ${sessionId}`);
|
|
49
|
+
p.reject(new Error('Topup selection timeout'));
|
|
50
|
+
clearPendingTopup(sessionId);
|
|
51
|
+
}
|
|
52
|
+
}, TOPUP_TIMEOUT_MS);
|
|
53
|
+
|
|
54
|
+
timeoutIds.set(sessionId, timeoutId);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function resolveTopupMethodSelection(
|
|
59
|
+
sessionId: string,
|
|
60
|
+
method: TopupMethod,
|
|
61
|
+
): boolean {
|
|
62
|
+
const pending = pendingTopups.get(sessionId);
|
|
63
|
+
if (!pending) {
|
|
64
|
+
logger.warn(
|
|
65
|
+
`No pending topup found for session ${sessionId} when trying to resolve`,
|
|
66
|
+
);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
pending.resolve(method);
|
|
71
|
+
clearPendingTopup(sessionId);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function rejectTopupSelection(
|
|
76
|
+
sessionId: string,
|
|
77
|
+
reason: string,
|
|
78
|
+
): boolean {
|
|
79
|
+
const pending = pendingTopups.get(sessionId);
|
|
80
|
+
if (!pending) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
pending.reject(new Error(reason));
|
|
85
|
+
clearPendingTopup(sessionId);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function getPendingTopup(sessionId: string): PendingTopup | undefined {
|
|
90
|
+
return pendingTopups.get(sessionId);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function hasPendingTopup(sessionId: string): boolean {
|
|
94
|
+
return pendingTopups.has(sessionId);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function clearPendingTopup(sessionId: string): void {
|
|
98
|
+
const timeoutId = timeoutIds.get(sessionId);
|
|
99
|
+
if (timeoutId) {
|
|
100
|
+
clearTimeout(timeoutId);
|
|
101
|
+
timeoutIds.delete(sessionId);
|
|
102
|
+
}
|
|
103
|
+
pendingTopups.delete(sessionId);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function clearAllPendingTopups(): void {
|
|
107
|
+
for (const sessionId of pendingTopups.keys()) {
|
|
108
|
+
clearPendingTopup(sessionId);
|
|
109
|
+
}
|
|
110
|
+
}
|