@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.
- package/LICENSE +190 -0
- package/README.md +256 -0
- package/dist/agent/commands.d.ts +27 -0
- package/dist/agent/commands.js +659 -0
- package/dist/agent/compact.d.ts +31 -0
- package/dist/agent/compact.js +366 -0
- package/dist/agent/context.d.ts +11 -0
- package/dist/agent/context.js +184 -0
- package/dist/agent/error-classifier.d.ts +10 -0
- package/dist/agent/error-classifier.js +61 -0
- package/dist/agent/llm.d.ts +63 -0
- package/dist/agent/llm.js +448 -0
- package/dist/agent/loop.d.ts +12 -0
- package/dist/agent/loop.js +346 -0
- package/dist/agent/optimize.d.ts +53 -0
- package/dist/agent/optimize.js +262 -0
- package/dist/agent/permissions.d.ts +39 -0
- package/dist/agent/permissions.js +226 -0
- package/dist/agent/reduce.d.ts +49 -0
- package/dist/agent/reduce.js +317 -0
- package/dist/agent/streaming-executor.d.ts +36 -0
- package/dist/agent/streaming-executor.js +149 -0
- package/dist/agent/tokens.d.ts +53 -0
- package/dist/agent/tokens.js +185 -0
- package/dist/agent/types.d.ts +125 -0
- package/dist/agent/types.js +5 -0
- package/dist/banner.d.ts +1 -0
- package/dist/banner.js +27 -0
- package/dist/commands/balance.d.ts +1 -0
- package/dist/commands/balance.js +40 -0
- package/dist/commands/config.d.ts +14 -0
- package/dist/commands/config.js +107 -0
- package/dist/commands/daemon.d.ts +3 -0
- package/dist/commands/daemon.js +117 -0
- package/dist/commands/history.d.ts +5 -0
- package/dist/commands/history.js +31 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +92 -0
- package/dist/commands/logs.d.ts +5 -0
- package/dist/commands/logs.js +89 -0
- package/dist/commands/models.d.ts +1 -0
- package/dist/commands/models.js +56 -0
- package/dist/commands/plugin.d.ts +14 -0
- package/dist/commands/plugin.js +176 -0
- package/dist/commands/proxy.d.ts +13 -0
- package/dist/commands/proxy.js +106 -0
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +49 -0
- package/dist/commands/start.d.ts +8 -0
- package/dist/commands/start.js +292 -0
- package/dist/commands/stats.d.ts +10 -0
- package/dist/commands/stats.js +94 -0
- package/dist/commands/uninit.d.ts +1 -0
- package/dist/commands/uninit.js +63 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +41 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +179 -0
- package/dist/mcp/client.d.ts +44 -0
- package/dist/mcp/client.js +147 -0
- package/dist/mcp/config.d.ts +20 -0
- package/dist/mcp/config.js +138 -0
- package/dist/plugin-sdk/channel.d.ts +100 -0
- package/dist/plugin-sdk/channel.js +10 -0
- package/dist/plugin-sdk/index.d.ts +14 -0
- package/dist/plugin-sdk/index.js +9 -0
- package/dist/plugin-sdk/plugin.d.ts +87 -0
- package/dist/plugin-sdk/plugin.js +7 -0
- package/dist/plugin-sdk/search.d.ts +13 -0
- package/dist/plugin-sdk/search.js +4 -0
- package/dist/plugin-sdk/tracker.d.ts +27 -0
- package/dist/plugin-sdk/tracker.js +5 -0
- package/dist/plugin-sdk/workflow.d.ts +126 -0
- package/dist/plugin-sdk/workflow.js +11 -0
- package/dist/plugins/registry.d.ts +33 -0
- package/dist/plugins/registry.js +155 -0
- package/dist/plugins/runner.d.ts +21 -0
- package/dist/plugins/runner.js +453 -0
- package/dist/plugins-bundled/social/index.d.ts +10 -0
- package/dist/plugins-bundled/social/index.js +363 -0
- package/dist/plugins-bundled/social/plugin.json +14 -0
- package/dist/plugins-bundled/social/prompts.d.ts +19 -0
- package/dist/plugins-bundled/social/prompts.js +67 -0
- package/dist/plugins-bundled/social/types.d.ts +58 -0
- package/dist/plugins-bundled/social/types.js +16 -0
- package/dist/pricing.d.ts +21 -0
- package/dist/pricing.js +91 -0
- package/dist/proxy/fallback.d.ts +38 -0
- package/dist/proxy/fallback.js +144 -0
- package/dist/proxy/server.d.ts +18 -0
- package/dist/proxy/server.js +576 -0
- package/dist/proxy/sse-translator.d.ts +29 -0
- package/dist/proxy/sse-translator.js +270 -0
- package/dist/router/index.d.ts +22 -0
- package/dist/router/index.js +269 -0
- package/dist/session/search.d.ts +33 -0
- package/dist/session/search.js +229 -0
- package/dist/session/storage.d.ts +48 -0
- package/dist/session/storage.js +173 -0
- package/dist/stats/insights.d.ts +55 -0
- package/dist/stats/insights.js +195 -0
- package/dist/stats/tracker.d.ts +54 -0
- package/dist/stats/tracker.js +165 -0
- package/dist/tools/askuser.d.ts +6 -0
- package/dist/tools/askuser.js +76 -0
- package/dist/tools/bash.d.ts +5 -0
- package/dist/tools/bash.js +336 -0
- package/dist/tools/edit.d.ts +5 -0
- package/dist/tools/edit.js +148 -0
- package/dist/tools/glob.d.ts +5 -0
- package/dist/tools/glob.js +158 -0
- package/dist/tools/grep.d.ts +5 -0
- package/dist/tools/grep.js +194 -0
- package/dist/tools/imagegen.d.ts +6 -0
- package/dist/tools/imagegen.js +172 -0
- package/dist/tools/index.d.ts +17 -0
- package/dist/tools/index.js +30 -0
- package/dist/tools/read.d.ts +11 -0
- package/dist/tools/read.js +90 -0
- package/dist/tools/subagent.d.ts +5 -0
- package/dist/tools/subagent.js +116 -0
- package/dist/tools/task.d.ts +5 -0
- package/dist/tools/task.js +91 -0
- package/dist/tools/webfetch.d.ts +5 -0
- package/dist/tools/webfetch.js +166 -0
- package/dist/tools/websearch.d.ts +5 -0
- package/dist/tools/websearch.js +103 -0
- package/dist/tools/write.d.ts +5 -0
- package/dist/tools/write.js +114 -0
- package/dist/ui/app.d.ts +26 -0
- package/dist/ui/app.js +545 -0
- package/dist/ui/model-picker.d.ts +14 -0
- package/dist/ui/model-picker.js +161 -0
- package/dist/ui/terminal.d.ts +35 -0
- package/dist/ui/terminal.js +337 -0
- package/dist/wallet/manager.d.ts +10 -0
- package/dist/wallet/manager.js +23 -0
- 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[]>;
|