@blockrun/franklin 3.0.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.
Files changed (138) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +256 -0
  3. package/dist/agent/commands.d.ts +27 -0
  4. package/dist/agent/commands.js +659 -0
  5. package/dist/agent/compact.d.ts +31 -0
  6. package/dist/agent/compact.js +366 -0
  7. package/dist/agent/context.d.ts +11 -0
  8. package/dist/agent/context.js +184 -0
  9. package/dist/agent/error-classifier.d.ts +10 -0
  10. package/dist/agent/error-classifier.js +61 -0
  11. package/dist/agent/llm.d.ts +63 -0
  12. package/dist/agent/llm.js +448 -0
  13. package/dist/agent/loop.d.ts +12 -0
  14. package/dist/agent/loop.js +346 -0
  15. package/dist/agent/optimize.d.ts +53 -0
  16. package/dist/agent/optimize.js +262 -0
  17. package/dist/agent/permissions.d.ts +39 -0
  18. package/dist/agent/permissions.js +226 -0
  19. package/dist/agent/reduce.d.ts +49 -0
  20. package/dist/agent/reduce.js +317 -0
  21. package/dist/agent/streaming-executor.d.ts +36 -0
  22. package/dist/agent/streaming-executor.js +149 -0
  23. package/dist/agent/tokens.d.ts +53 -0
  24. package/dist/agent/tokens.js +185 -0
  25. package/dist/agent/types.d.ts +125 -0
  26. package/dist/agent/types.js +5 -0
  27. package/dist/banner.d.ts +1 -0
  28. package/dist/banner.js +27 -0
  29. package/dist/commands/balance.d.ts +1 -0
  30. package/dist/commands/balance.js +40 -0
  31. package/dist/commands/config.d.ts +14 -0
  32. package/dist/commands/config.js +107 -0
  33. package/dist/commands/daemon.d.ts +3 -0
  34. package/dist/commands/daemon.js +117 -0
  35. package/dist/commands/history.d.ts +5 -0
  36. package/dist/commands/history.js +31 -0
  37. package/dist/commands/init.d.ts +3 -0
  38. package/dist/commands/init.js +92 -0
  39. package/dist/commands/logs.d.ts +5 -0
  40. package/dist/commands/logs.js +89 -0
  41. package/dist/commands/models.d.ts +1 -0
  42. package/dist/commands/models.js +56 -0
  43. package/dist/commands/plugin.d.ts +14 -0
  44. package/dist/commands/plugin.js +176 -0
  45. package/dist/commands/proxy.d.ts +13 -0
  46. package/dist/commands/proxy.js +106 -0
  47. package/dist/commands/setup.d.ts +1 -0
  48. package/dist/commands/setup.js +49 -0
  49. package/dist/commands/start.d.ts +8 -0
  50. package/dist/commands/start.js +292 -0
  51. package/dist/commands/stats.d.ts +10 -0
  52. package/dist/commands/stats.js +94 -0
  53. package/dist/commands/uninit.d.ts +1 -0
  54. package/dist/commands/uninit.js +63 -0
  55. package/dist/config.d.ts +9 -0
  56. package/dist/config.js +41 -0
  57. package/dist/index.d.ts +2 -0
  58. package/dist/index.js +179 -0
  59. package/dist/mcp/client.d.ts +44 -0
  60. package/dist/mcp/client.js +147 -0
  61. package/dist/mcp/config.d.ts +20 -0
  62. package/dist/mcp/config.js +138 -0
  63. package/dist/plugin-sdk/channel.d.ts +100 -0
  64. package/dist/plugin-sdk/channel.js +10 -0
  65. package/dist/plugin-sdk/index.d.ts +14 -0
  66. package/dist/plugin-sdk/index.js +9 -0
  67. package/dist/plugin-sdk/plugin.d.ts +87 -0
  68. package/dist/plugin-sdk/plugin.js +7 -0
  69. package/dist/plugin-sdk/search.d.ts +13 -0
  70. package/dist/plugin-sdk/search.js +4 -0
  71. package/dist/plugin-sdk/tracker.d.ts +27 -0
  72. package/dist/plugin-sdk/tracker.js +5 -0
  73. package/dist/plugin-sdk/workflow.d.ts +126 -0
  74. package/dist/plugin-sdk/workflow.js +11 -0
  75. package/dist/plugins/registry.d.ts +33 -0
  76. package/dist/plugins/registry.js +155 -0
  77. package/dist/plugins/runner.d.ts +21 -0
  78. package/dist/plugins/runner.js +453 -0
  79. package/dist/plugins-bundled/social/index.d.ts +10 -0
  80. package/dist/plugins-bundled/social/index.js +363 -0
  81. package/dist/plugins-bundled/social/plugin.json +14 -0
  82. package/dist/plugins-bundled/social/prompts.d.ts +19 -0
  83. package/dist/plugins-bundled/social/prompts.js +67 -0
  84. package/dist/plugins-bundled/social/types.d.ts +58 -0
  85. package/dist/plugins-bundled/social/types.js +16 -0
  86. package/dist/pricing.d.ts +21 -0
  87. package/dist/pricing.js +91 -0
  88. package/dist/proxy/fallback.d.ts +38 -0
  89. package/dist/proxy/fallback.js +144 -0
  90. package/dist/proxy/server.d.ts +18 -0
  91. package/dist/proxy/server.js +576 -0
  92. package/dist/proxy/sse-translator.d.ts +29 -0
  93. package/dist/proxy/sse-translator.js +270 -0
  94. package/dist/router/index.d.ts +22 -0
  95. package/dist/router/index.js +269 -0
  96. package/dist/session/search.d.ts +33 -0
  97. package/dist/session/search.js +229 -0
  98. package/dist/session/storage.d.ts +48 -0
  99. package/dist/session/storage.js +173 -0
  100. package/dist/stats/insights.d.ts +55 -0
  101. package/dist/stats/insights.js +195 -0
  102. package/dist/stats/tracker.d.ts +54 -0
  103. package/dist/stats/tracker.js +165 -0
  104. package/dist/tools/askuser.d.ts +6 -0
  105. package/dist/tools/askuser.js +76 -0
  106. package/dist/tools/bash.d.ts +5 -0
  107. package/dist/tools/bash.js +336 -0
  108. package/dist/tools/edit.d.ts +5 -0
  109. package/dist/tools/edit.js +148 -0
  110. package/dist/tools/glob.d.ts +5 -0
  111. package/dist/tools/glob.js +158 -0
  112. package/dist/tools/grep.d.ts +5 -0
  113. package/dist/tools/grep.js +194 -0
  114. package/dist/tools/imagegen.d.ts +6 -0
  115. package/dist/tools/imagegen.js +172 -0
  116. package/dist/tools/index.d.ts +17 -0
  117. package/dist/tools/index.js +30 -0
  118. package/dist/tools/read.d.ts +11 -0
  119. package/dist/tools/read.js +90 -0
  120. package/dist/tools/subagent.d.ts +5 -0
  121. package/dist/tools/subagent.js +116 -0
  122. package/dist/tools/task.d.ts +5 -0
  123. package/dist/tools/task.js +91 -0
  124. package/dist/tools/webfetch.d.ts +5 -0
  125. package/dist/tools/webfetch.js +166 -0
  126. package/dist/tools/websearch.d.ts +5 -0
  127. package/dist/tools/websearch.js +103 -0
  128. package/dist/tools/write.d.ts +5 -0
  129. package/dist/tools/write.js +114 -0
  130. package/dist/ui/app.d.ts +26 -0
  131. package/dist/ui/app.js +545 -0
  132. package/dist/ui/model-picker.d.ts +14 -0
  133. package/dist/ui/model-picker.js +161 -0
  134. package/dist/ui/terminal.d.ts +35 -0
  135. package/dist/ui/terminal.js +337 -0
  136. package/dist/wallet/manager.d.ts +10 -0
  137. package/dist/wallet/manager.js +23 -0
  138. package/package.json +79 -0
@@ -0,0 +1,63 @@
1
+ /**
2
+ * LLM Client for runcode
3
+ * Calls BlockRun API directly with x402 payment handling and streaming.
4
+ * Original implementation — not derived from any existing codebase.
5
+ */
6
+ import { type Chain } from '../config.js';
7
+ import type { Dialogue, CapabilityDefinition, ContentPart, CapabilityInvocation } from './types.js';
8
+ export interface ModelRequest {
9
+ model: string;
10
+ messages: Dialogue[];
11
+ system?: string;
12
+ tools?: CapabilityDefinition[];
13
+ max_tokens?: number;
14
+ stream?: boolean;
15
+ temperature?: number;
16
+ }
17
+ export interface StreamChunk {
18
+ kind: 'content_block_start' | 'content_block_delta' | 'content_block_stop' | 'message_start' | 'message_delta' | 'message_stop' | 'ping' | 'error';
19
+ payload: Record<string, unknown>;
20
+ }
21
+ export interface CompletionUsage {
22
+ inputTokens: number;
23
+ outputTokens: number;
24
+ }
25
+ export interface LLMClientOptions {
26
+ apiUrl: string;
27
+ chain: Chain;
28
+ debug?: boolean;
29
+ }
30
+ export declare class ModelClient {
31
+ private apiUrl;
32
+ private chain;
33
+ private debug;
34
+ private walletAddress;
35
+ private cachedBaseWallet;
36
+ private cachedSolanaWallet;
37
+ private walletCacheTime;
38
+ private static WALLET_CACHE_TTL;
39
+ constructor(opts: LLMClientOptions);
40
+ /**
41
+ * Stream a completion from the BlockRun API.
42
+ * Yields parsed SSE chunks as they arrive.
43
+ * Handles x402 payment automatically on 402 responses.
44
+ */
45
+ streamCompletion(request: ModelRequest, signal?: AbortSignal): AsyncGenerator<StreamChunk>;
46
+ /**
47
+ * Non-streaming completion for simple requests.
48
+ */
49
+ complete(request: ModelRequest, signal?: AbortSignal, onToolReady?: (tool: CapabilityInvocation) => void, onStreamDelta?: (delta: {
50
+ type: 'text' | 'thinking';
51
+ text: string;
52
+ }) => void): Promise<{
53
+ content: ContentPart[];
54
+ usage: CompletionUsage;
55
+ stopReason: string;
56
+ }>;
57
+ private signPayment;
58
+ private signBasePayment;
59
+ private signSolanaPayment;
60
+ private extractPaymentReq;
61
+ private parseSSEStream;
62
+ private mapEventType;
63
+ }
@@ -0,0 +1,448 @@
1
+ /**
2
+ * LLM Client for runcode
3
+ * Calls BlockRun API directly with x402 payment handling and streaming.
4
+ * Original implementation — not derived from any existing codebase.
5
+ */
6
+ import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
7
+ import { USER_AGENT } from '../config.js';
8
+ // ─── Anthropic Prompt Caching ─────────────────────────────────────────────
9
+ /**
10
+ * Apply Anthropic prompt caching using the `system_and_3` strategy.
11
+ * Pattern from nousresearch/hermes-agent `agent/prompt_caching.py`.
12
+ *
13
+ * Places 4 cache_control breakpoints (Anthropic's max):
14
+ * 1. System prompt (stable across all turns)
15
+ * 2-4. Last 3 non-system messages (rolling window)
16
+ *
17
+ * Also caches the last tool definition (tools are stable across turns).
18
+ *
19
+ * This keeps the cache warm: each new turn extends the cached prefix rather
20
+ * than invalidating it. Multi-turn conversations see ~75% input token savings
21
+ * on Anthropic models.
22
+ */
23
+ function applyAnthropicPromptCaching(payload, request) {
24
+ const out = { ...payload };
25
+ const cacheMarker = { type: 'ephemeral' };
26
+ // 1. System prompt → wrap as array with cache_control on the text block
27
+ if (typeof request.system === 'string' && request.system.length > 0) {
28
+ out['system'] = [
29
+ { type: 'text', text: request.system, cache_control: cacheMarker },
30
+ ];
31
+ }
32
+ // 2. Tools → cache_control on the last tool (stable across turns)
33
+ if (request.tools && request.tools.length > 0) {
34
+ const toolsCopy = request.tools.map(t => ({ ...t }));
35
+ toolsCopy[toolsCopy.length - 1]['cache_control'] = cacheMarker;
36
+ out['tools'] = toolsCopy;
37
+ }
38
+ // 3. Messages → rolling cache_control on last 3 messages (user/assistant).
39
+ // System is a separate field in ModelRequest, so all messages here are non-system.
40
+ // Strategy: mark the last 3 messages so the cached prefix extends as the
41
+ // conversation grows. Older cached prefixes expire after 5 min but newer
42
+ // ones keep the cache warm.
43
+ if (request.messages && request.messages.length > 0) {
44
+ const messagesCopy = request.messages.map(m => ({ ...m }));
45
+ // Mark last 3 messages (or fewer if history is shorter)
46
+ const start = Math.max(0, messagesCopy.length - 3);
47
+ for (let idx = start; idx < messagesCopy.length; idx++) {
48
+ const msg = messagesCopy[idx];
49
+ if (typeof msg.content === 'string') {
50
+ messagesCopy[idx]['content'] = [
51
+ { type: 'text', text: msg.content, cache_control: cacheMarker },
52
+ ];
53
+ }
54
+ else if (Array.isArray(msg.content) && msg.content.length > 0) {
55
+ const contentCopy = msg.content.map(c => ({ ...c }));
56
+ // cache_control goes on the last content block
57
+ contentCopy[contentCopy.length - 1]['cache_control'] = cacheMarker;
58
+ messagesCopy[idx]['content'] = contentCopy;
59
+ }
60
+ }
61
+ out['messages'] = messagesCopy;
62
+ }
63
+ return out;
64
+ }
65
+ // ─── Client ────────────────────────────────────────────────────────────────
66
+ export class ModelClient {
67
+ apiUrl;
68
+ chain;
69
+ debug;
70
+ walletAddress = '';
71
+ cachedBaseWallet = null;
72
+ cachedSolanaWallet = null;
73
+ walletCacheTime = 0;
74
+ static WALLET_CACHE_TTL = 30 * 60 * 1000; // 30 min TTL
75
+ constructor(opts) {
76
+ this.apiUrl = opts.apiUrl;
77
+ this.chain = opts.chain;
78
+ this.debug = opts.debug ?? false;
79
+ }
80
+ /**
81
+ * Stream a completion from the BlockRun API.
82
+ * Yields parsed SSE chunks as they arrive.
83
+ * Handles x402 payment automatically on 402 responses.
84
+ */
85
+ async *streamCompletion(request, signal) {
86
+ const isAnthropic = request.model.startsWith('anthropic/');
87
+ const isGLM = request.model.startsWith('zai/') || request.model.includes('glm');
88
+ // Build the request payload, injecting model-specific optimizations
89
+ let requestPayload = { ...request, stream: true };
90
+ // ── GLM-specific optimizations ───────────────────────────────────────────
91
+ // GLM models work best with temperature=0.8 per official zai spec.
92
+ // Enable thinking mode only for explicit reasoning variants (-thinking-).
93
+ if (isGLM) {
94
+ if (requestPayload['temperature'] === undefined) {
95
+ requestPayload['temperature'] = 0.8;
96
+ }
97
+ // Only enable thinking for models that explicitly ship reasoning mode
98
+ if (request.model.includes('-thinking-')) {
99
+ requestPayload['thinking'] = { type: 'enabled' };
100
+ }
101
+ }
102
+ if (isAnthropic) {
103
+ // ─ Anthropic prompt caching: `system_and_3` strategy ─────────────────
104
+ // 4 cache_control breakpoints (Anthropic max):
105
+ // 1. System prompt (stable across turns)
106
+ // 2-4. Last 3 non-system messages (rolling window)
107
+ //
108
+ // This keeps the cache warm across turns: each new turn extends the
109
+ // cache instead of invalidating it. ~75% input token savings on
110
+ // multi-turn conversations. Pattern adopted from nousresearch/hermes-agent.
111
+ requestPayload = applyAnthropicPromptCaching(requestPayload, request);
112
+ }
113
+ const body = JSON.stringify(requestPayload);
114
+ const endpoint = `${this.apiUrl}/v1/messages`;
115
+ const headers = {
116
+ 'Content-Type': 'application/json',
117
+ 'anthropic-version': '2023-06-01',
118
+ 'x-api-key': 'x402-agent-handles-auth',
119
+ 'User-Agent': USER_AGENT,
120
+ };
121
+ // Enable prompt caching beta for Anthropic models
122
+ if (isAnthropic) {
123
+ headers['anthropic-beta'] = 'prompt-caching-2024-07-31';
124
+ }
125
+ if (this.debug) {
126
+ console.error(`[runcode] POST ${endpoint} model=${request.model}`);
127
+ }
128
+ let response = await fetch(endpoint, {
129
+ method: 'POST',
130
+ headers,
131
+ body,
132
+ signal,
133
+ });
134
+ // Handle x402 payment
135
+ if (response.status === 402) {
136
+ if (this.debug)
137
+ console.error('[runcode] Payment required — signing...');
138
+ const paymentHeader = await this.signPayment(response);
139
+ if (!paymentHeader) {
140
+ yield { kind: 'error', payload: { message: 'Payment signing failed' } };
141
+ return;
142
+ }
143
+ response = await fetch(endpoint, {
144
+ method: 'POST',
145
+ headers: { ...headers, ...paymentHeader },
146
+ body,
147
+ signal,
148
+ });
149
+ }
150
+ if (!response.ok) {
151
+ const errorBody = await response.text().catch(() => 'unknown error');
152
+ // Extract human-readable message from JSON error bodies ({"error":{"message":"..."}})
153
+ let message = errorBody;
154
+ try {
155
+ const parsed = JSON.parse(errorBody);
156
+ message = parsed?.error?.message || parsed?.message || errorBody;
157
+ }
158
+ catch { /* not JSON — use raw text */ }
159
+ yield {
160
+ kind: 'error',
161
+ payload: { status: response.status, message },
162
+ };
163
+ return;
164
+ }
165
+ // Parse SSE stream
166
+ yield* this.parseSSEStream(response, signal);
167
+ }
168
+ /**
169
+ * Non-streaming completion for simple requests.
170
+ */
171
+ async complete(request, signal, onToolReady, onStreamDelta) {
172
+ const collected = [];
173
+ let usage = { inputTokens: 0, outputTokens: 0 };
174
+ let stopReason = 'end_turn';
175
+ // Accumulate from stream
176
+ let currentText = '';
177
+ let currentThinking = '';
178
+ let currentToolId = '';
179
+ let currentToolName = '';
180
+ let currentToolInput = '';
181
+ for await (const chunk of this.streamCompletion(request, signal)) {
182
+ switch (chunk.kind) {
183
+ case 'content_block_start': {
184
+ const block = chunk.payload;
185
+ const cblock = block['content_block'];
186
+ if (cblock?.type === 'tool_use') {
187
+ currentToolId = cblock.id || '';
188
+ currentToolName = cblock.name || '';
189
+ currentToolInput = '';
190
+ }
191
+ else if (cblock?.type === 'thinking') {
192
+ currentThinking = '';
193
+ }
194
+ else if (cblock?.type === 'text') {
195
+ currentText = '';
196
+ }
197
+ break;
198
+ }
199
+ case 'content_block_delta': {
200
+ const delta = chunk.payload['delta'];
201
+ if (!delta)
202
+ break;
203
+ if (delta.type === 'text_delta') {
204
+ const text = delta.text || '';
205
+ currentText += text;
206
+ if (text)
207
+ onStreamDelta?.({ type: 'text', text });
208
+ }
209
+ else if (delta.type === 'thinking_delta') {
210
+ const text = delta.thinking || '';
211
+ currentThinking += text;
212
+ if (text)
213
+ onStreamDelta?.({ type: 'thinking', text });
214
+ }
215
+ else if (delta.type === 'input_json_delta') {
216
+ currentToolInput += delta.partial_json || '';
217
+ }
218
+ break;
219
+ }
220
+ case 'content_block_stop': {
221
+ if (currentToolId) {
222
+ let parsedInput = {};
223
+ try {
224
+ parsedInput = JSON.parse(currentToolInput || '{}');
225
+ }
226
+ catch (parseErr) {
227
+ // Log malformed JSON instead of silently defaulting to {}
228
+ if (this.debug) {
229
+ console.error(`[runcode] Malformed tool input JSON for ${currentToolName}: ${parseErr.message}`);
230
+ }
231
+ }
232
+ const toolInvocation = {
233
+ type: 'tool_use',
234
+ id: currentToolId,
235
+ name: currentToolName,
236
+ input: parsedInput,
237
+ };
238
+ collected.push(toolInvocation);
239
+ // Notify caller so concurrent tools can start immediately
240
+ onToolReady?.(toolInvocation);
241
+ currentToolId = '';
242
+ currentToolName = '';
243
+ currentToolInput = '';
244
+ }
245
+ else if (currentThinking) {
246
+ collected.push({
247
+ type: 'thinking',
248
+ thinking: currentThinking,
249
+ });
250
+ currentThinking = '';
251
+ }
252
+ else if (currentText) {
253
+ collected.push({
254
+ type: 'text',
255
+ text: currentText,
256
+ });
257
+ currentText = '';
258
+ }
259
+ break;
260
+ }
261
+ case 'message_delta': {
262
+ const msgUsage = chunk.payload['usage'];
263
+ if (msgUsage) {
264
+ usage.outputTokens = msgUsage['output_tokens'] ?? usage.outputTokens;
265
+ }
266
+ const delta = chunk.payload['delta'];
267
+ if (delta?.['stop_reason']) {
268
+ stopReason = delta['stop_reason'];
269
+ }
270
+ break;
271
+ }
272
+ case 'message_start': {
273
+ const msg = chunk.payload['message'];
274
+ const msgUsage = msg?.['usage'];
275
+ if (msgUsage) {
276
+ usage.inputTokens = msgUsage['input_tokens'] ?? 0;
277
+ usage.outputTokens = msgUsage['output_tokens'] ?? 0;
278
+ }
279
+ break;
280
+ }
281
+ case 'error': {
282
+ const errMsg = chunk.payload['message'] || 'API error';
283
+ throw new Error(errMsg);
284
+ }
285
+ }
286
+ }
287
+ // Flush any remaining text
288
+ if (currentText) {
289
+ collected.push({ type: 'text', text: currentText });
290
+ }
291
+ return { content: collected, usage, stopReason };
292
+ }
293
+ // ─── Payment ───────────────────────────────────────────────────────────
294
+ async signPayment(response) {
295
+ try {
296
+ if (this.chain === 'solana') {
297
+ return await this.signSolanaPayment(response);
298
+ }
299
+ return await this.signBasePayment(response);
300
+ }
301
+ catch (err) {
302
+ const msg = err.message || '';
303
+ if (msg.includes('insufficient') || msg.includes('balance')) {
304
+ console.error(`[runcode] Insufficient USDC balance. Run 'runcode balance' to check.`);
305
+ }
306
+ else if (this.debug) {
307
+ console.error('[runcode] Payment error:', msg);
308
+ }
309
+ else {
310
+ console.error(`[runcode] Payment failed: ${msg.slice(0, 100)}`);
311
+ }
312
+ return null;
313
+ }
314
+ }
315
+ async signBasePayment(response) {
316
+ // Refresh wallet cache after TTL to pick up balance/key changes
317
+ if (!this.cachedBaseWallet || (Date.now() - this.walletCacheTime > ModelClient.WALLET_CACHE_TTL)) {
318
+ const w = getOrCreateWallet();
319
+ this.walletCacheTime = Date.now();
320
+ this.cachedBaseWallet = { privateKey: w.privateKey, address: w.address };
321
+ }
322
+ const wallet = this.cachedBaseWallet;
323
+ this.walletAddress = wallet.address;
324
+ // Extract payment requirements from 402 response
325
+ const paymentHeader = await this.extractPaymentReq(response);
326
+ if (!paymentHeader)
327
+ throw new Error('No payment requirements in 402 response');
328
+ const paymentRequired = parsePaymentRequired(paymentHeader);
329
+ const details = extractPaymentDetails(paymentRequired);
330
+ const payload = await createPaymentPayload(wallet.privateKey, wallet.address, details.recipient, details.amount, details.network || 'eip155:8453', {
331
+ resourceUrl: details.resource?.url || this.apiUrl,
332
+ resourceDescription: details.resource?.description || 'BlockRun AI API call',
333
+ maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
334
+ extra: details.extra,
335
+ });
336
+ return { 'PAYMENT-SIGNATURE': payload };
337
+ }
338
+ async signSolanaPayment(response) {
339
+ if (!this.cachedSolanaWallet || (Date.now() - this.walletCacheTime > ModelClient.WALLET_CACHE_TTL)) {
340
+ const w = await getOrCreateSolanaWallet();
341
+ this.walletCacheTime = Date.now();
342
+ this.cachedSolanaWallet = { privateKey: w.privateKey, address: w.address };
343
+ }
344
+ const wallet = this.cachedSolanaWallet;
345
+ this.walletAddress = wallet.address;
346
+ const paymentHeader = await this.extractPaymentReq(response);
347
+ if (!paymentHeader)
348
+ throw new Error('No payment requirements in 402 response');
349
+ const paymentRequired = parsePaymentRequired(paymentHeader);
350
+ const details = extractPaymentDetails(paymentRequired, SOLANA_NETWORK);
351
+ const secretBytes = await solanaKeyToBytes(wallet.privateKey);
352
+ const feePayer = details.extra?.feePayer || details.recipient;
353
+ const payload = await createSolanaPaymentPayload(secretBytes, wallet.address, details.recipient, details.amount, feePayer, {
354
+ resourceUrl: details.resource?.url || this.apiUrl,
355
+ resourceDescription: details.resource?.description || 'BlockRun AI API call',
356
+ maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
357
+ extra: details.extra,
358
+ });
359
+ return { 'PAYMENT-SIGNATURE': payload };
360
+ }
361
+ async extractPaymentReq(response) {
362
+ let header = response.headers.get('payment-required');
363
+ if (!header) {
364
+ try {
365
+ const body = (await response.json());
366
+ if (body.x402 || body.accepts) {
367
+ header = btoa(JSON.stringify(body));
368
+ }
369
+ }
370
+ catch { /* ignore parse errors */ }
371
+ }
372
+ return header;
373
+ }
374
+ // ─── SSE Parsing ───────────────────────────────────────────────────────
375
+ async *parseSSEStream(response, signal) {
376
+ const reader = response.body?.getReader();
377
+ if (!reader) {
378
+ yield { kind: 'error', payload: { message: 'No response body' } };
379
+ return;
380
+ }
381
+ const decoder = new TextDecoder();
382
+ let buffer = '';
383
+ // Persist across read() calls — event: and data: may arrive in separate chunks
384
+ let currentEvent = '';
385
+ const MAX_BUFFER = 1_000_000; // 1MB buffer cap
386
+ try {
387
+ while (true) {
388
+ if (signal?.aborted)
389
+ break;
390
+ const { done, value } = await reader.read();
391
+ if (done)
392
+ break;
393
+ buffer += decoder.decode(value, { stream: true });
394
+ // Safety: if buffer grows too large without newlines, something is wrong
395
+ if (buffer.length > MAX_BUFFER) {
396
+ if (this.debug) {
397
+ console.error(`[runcode] SSE buffer overflow (${(buffer.length / 1024).toFixed(0)}KB) — truncating to prevent OOM`);
398
+ }
399
+ buffer = buffer.slice(-MAX_BUFFER / 2);
400
+ }
401
+ const lines = buffer.split('\n');
402
+ buffer = lines.pop() || '';
403
+ for (const line of lines) {
404
+ const trimmed = line.trim();
405
+ if (trimmed === '') {
406
+ // Blank line = end of SSE event (reset for next event)
407
+ currentEvent = '';
408
+ continue;
409
+ }
410
+ if (trimmed.startsWith('event:')) {
411
+ currentEvent = trimmed.slice(6).trim();
412
+ }
413
+ else if (trimmed.startsWith('data:')) {
414
+ const data = trimmed.slice(5).trim();
415
+ if (data === '[DONE]')
416
+ return;
417
+ try {
418
+ const parsed = JSON.parse(data);
419
+ const mappedKind = this.mapEventType(currentEvent, parsed);
420
+ if (mappedKind) {
421
+ yield { kind: mappedKind, payload: parsed };
422
+ }
423
+ }
424
+ catch {
425
+ // Skip malformed JSON lines
426
+ }
427
+ }
428
+ }
429
+ }
430
+ }
431
+ finally {
432
+ reader.releaseLock();
433
+ }
434
+ }
435
+ mapEventType(event, _payload) {
436
+ switch (event) {
437
+ case 'message_start': return 'message_start';
438
+ case 'message_delta': return 'message_delta';
439
+ case 'message_stop': return 'message_stop';
440
+ case 'content_block_start': return 'content_block_start';
441
+ case 'content_block_delta': return 'content_block_delta';
442
+ case 'content_block_stop': return 'content_block_stop';
443
+ case 'ping': return 'ping';
444
+ case 'error': return 'error';
445
+ default: return null;
446
+ }
447
+ }
448
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * runcode Agent Loop
3
+ * The core reasoning-action cycle: prompt → model → extract capabilities → execute → repeat.
4
+ * Original implementation with different architecture from any reference codebase.
5
+ */
6
+ import type { AgentConfig, Dialogue, StreamEvent } from './types.js';
7
+ /**
8
+ * Run a multi-turn interactive session.
9
+ * Each user message triggers a full agent loop.
10
+ * Returns the accumulated conversation history.
11
+ */
12
+ export declare function interactiveSession(config: AgentConfig, getUserInput: () => Promise<string | null>, onEvent: (event: StreamEvent) => void, onAbortReady?: (abort: () => void) => void): Promise<Dialogue[]>;