@blockrun/franklin 3.7.9 → 3.8.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 (51) hide show
  1. package/dist/agent/bash-guard.js +8 -2
  2. package/dist/agent/compact.d.ts +14 -0
  3. package/dist/agent/compact.js +57 -1
  4. package/dist/agent/context.js +6 -4
  5. package/dist/agent/llm.d.ts +25 -0
  6. package/dist/agent/llm.js +27 -12
  7. package/dist/agent/loop.js +88 -18
  8. package/dist/agent/optimize.js +4 -0
  9. package/dist/agent/tokens.d.ts +7 -3
  10. package/dist/agent/tokens.js +14 -7
  11. package/dist/agent/tool-guard.js +64 -26
  12. package/dist/content/image-pricing.d.ts +14 -0
  13. package/dist/content/image-pricing.js +32 -0
  14. package/dist/content/library.d.ts +63 -0
  15. package/dist/content/library.js +75 -0
  16. package/dist/content/record-image.d.ts +43 -0
  17. package/dist/content/record-image.js +50 -0
  18. package/dist/content/store.d.ts +15 -0
  19. package/dist/content/store.js +55 -0
  20. package/dist/pricing.d.ts +1 -1
  21. package/dist/pricing.js +2 -2
  22. package/dist/router/index.js +17 -6
  23. package/dist/tools/bash.d.ts +8 -0
  24. package/dist/tools/bash.js +13 -0
  25. package/dist/tools/content-execute.d.ts +26 -0
  26. package/dist/tools/content-execute.js +212 -0
  27. package/dist/tools/imagegen.d.ts +14 -0
  28. package/dist/tools/imagegen.js +164 -101
  29. package/dist/tools/index.d.ts +6 -0
  30. package/dist/tools/index.js +91 -5
  31. package/dist/tools/read.d.ts +13 -0
  32. package/dist/tools/read.js +17 -0
  33. package/dist/tools/trading-execute.d.ts +35 -0
  34. package/dist/tools/trading-execute.js +297 -0
  35. package/dist/tools/webfetch.d.ts +6 -0
  36. package/dist/tools/webfetch.js +8 -0
  37. package/dist/trading/engine.d.ts +51 -0
  38. package/dist/trading/engine.js +75 -0
  39. package/dist/trading/live-exchange.d.ts +43 -0
  40. package/dist/trading/live-exchange.js +48 -0
  41. package/dist/trading/mock-exchange.d.ts +40 -0
  42. package/dist/trading/mock-exchange.js +41 -0
  43. package/dist/trading/portfolio.d.ts +67 -0
  44. package/dist/trading/portfolio.js +106 -0
  45. package/dist/trading/risk.d.ts +34 -0
  46. package/dist/trading/risk.js +64 -0
  47. package/dist/trading/store.d.ts +9 -0
  48. package/dist/trading/store.js +32 -0
  49. package/dist/trading/trade-log.d.ts +39 -0
  50. package/dist/trading/trade-log.js +81 -0
  51. package/package.json +1 -1
@@ -3,4 +3,18 @@
3
3
  * Uses x402 payment on Solana or Base.
4
4
  */
5
5
  import type { CapabilityHandler } from '../agent/types.js';
6
+ import type { ContentLibrary } from '../content/library.js';
7
+ export interface ImageGenDeps {
8
+ /** Optional Content library for auto-recording generations into a piece. */
9
+ library?: ContentLibrary;
10
+ /** Invoked after successful content-linked generation; lets callers persist. */
11
+ onContentChange?: () => void | Promise<void>;
12
+ }
13
+ /**
14
+ * Build the ImageGen capability. Passing `deps.library` enables the
15
+ * contentId flow: pre-flight budget check + post-generation asset
16
+ * recording. With no deps, behavior matches the pre-factory version.
17
+ */
18
+ export declare function createImageGenCapability(deps?: ImageGenDeps): CapabilityHandler;
19
+ /** Back-compat static capability for callers that don't want the Content bridge. */
6
20
  export declare const imageGenCapability: CapabilityHandler;
@@ -6,99 +6,146 @@ import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
8
8
  import { loadChain, API_URLS, VERSION } from '../config.js';
9
- async function execute(input, ctx) {
10
- const { prompt, output_path, size, model } = input;
11
- if (!prompt) {
12
- return { output: 'Error: prompt is required', isError: true };
13
- }
14
- const chain = loadChain();
15
- const apiUrl = API_URLS[chain];
16
- const endpoint = `${apiUrl}/v1/images/generations`;
17
- const imageModel = model || 'openai/gpt-image-1';
18
- const imageSize = size || '1024x1024';
19
- // Default output path
20
- const outPath = output_path
21
- ? (path.isAbsolute(output_path) ? output_path : path.resolve(ctx.workingDir, output_path))
22
- : path.resolve(ctx.workingDir, `generated-${Date.now()}.png`);
23
- const body = JSON.stringify({
24
- model: imageModel,
25
- prompt,
26
- n: 1,
27
- size: imageSize,
28
- response_format: 'b64_json',
29
- });
30
- const headers = {
31
- 'Content-Type': 'application/json',
32
- 'User-Agent': `franklin/${VERSION}`,
33
- };
34
- const controller = new AbortController();
35
- const timeout = setTimeout(() => controller.abort(), 60_000); // 60s timeout
36
- try {
37
- // First request — will get 402
38
- let response = await fetch(endpoint, {
39
- method: 'POST',
40
- signal: controller.signal,
41
- headers,
42
- body,
43
- });
44
- // Handle x402 payment
45
- if (response.status === 402) {
46
- const paymentHeaders = await signPayment(response, chain, endpoint);
47
- if (!paymentHeaders) {
48
- return { output: 'Payment failed. Check wallet balance with: runcode balance', isError: true };
9
+ import { checkImageBudget, recordImageAsset } from '../content/record-image.js';
10
+ function buildExecute(deps) {
11
+ return async function execute(input, ctx) {
12
+ const { prompt, output_path, size, model, contentId } = input;
13
+ if (!prompt) {
14
+ return { output: 'Error: prompt is required', isError: true };
15
+ }
16
+ // ── Content pre-flight: refuse BEFORE paying if budget can't cover this ──
17
+ const imageModel = model || 'openai/gpt-image-1';
18
+ const imageSize = size || '1024x1024';
19
+ if (contentId && deps.library) {
20
+ const decision = checkImageBudget(deps.library, contentId, imageModel, imageSize);
21
+ if (!decision.ok) {
22
+ // Normal text output, not isError — the agent should adapt (smaller
23
+ // size, different model, raise budget) rather than trigger retry.
24
+ return {
25
+ output: `## Image generation skipped\n` +
26
+ `- ${decision.reason}\n\n` +
27
+ `No USDC was spent. Choose a cheaper model/size or raise the ` +
28
+ `content budget before trying again.`,
29
+ };
49
30
  }
50
- response = await fetch(endpoint, {
31
+ }
32
+ const chain = loadChain();
33
+ const apiUrl = API_URLS[chain];
34
+ const endpoint = `${apiUrl}/v1/images/generations`;
35
+ // Default output path
36
+ const outPath = output_path
37
+ ? (path.isAbsolute(output_path) ? output_path : path.resolve(ctx.workingDir, output_path))
38
+ : path.resolve(ctx.workingDir, `generated-${Date.now()}.png`);
39
+ const body = JSON.stringify({
40
+ model: imageModel,
41
+ prompt,
42
+ n: 1,
43
+ size: imageSize,
44
+ response_format: 'b64_json',
45
+ });
46
+ const headers = {
47
+ 'Content-Type': 'application/json',
48
+ 'User-Agent': `franklin/${VERSION}`,
49
+ };
50
+ const controller = new AbortController();
51
+ const timeout = setTimeout(() => controller.abort(), 60_000); // 60s timeout
52
+ try {
53
+ // First request — will get 402
54
+ let response = await fetch(endpoint, {
51
55
  method: 'POST',
52
56
  signal: controller.signal,
53
- headers: { ...headers, ...paymentHeaders },
57
+ headers,
54
58
  body,
55
59
  });
60
+ // Handle x402 payment
61
+ if (response.status === 402) {
62
+ const paymentHeaders = await signPayment(response, chain, endpoint);
63
+ if (!paymentHeaders) {
64
+ return { output: 'Payment failed. Check wallet balance with: runcode balance', isError: true };
65
+ }
66
+ response = await fetch(endpoint, {
67
+ method: 'POST',
68
+ signal: controller.signal,
69
+ headers: { ...headers, ...paymentHeaders },
70
+ body,
71
+ });
72
+ }
73
+ if (!response.ok) {
74
+ const errText = await response.text().catch(() => '');
75
+ return { output: `Image generation failed (${response.status}): ${errText.slice(0, 200)}`, isError: true };
76
+ }
77
+ const result = await response.json();
78
+ const imageData = result.data?.[0];
79
+ if (!imageData) {
80
+ return { output: 'No image data returned from API', isError: true };
81
+ }
82
+ // Save image
83
+ if (imageData.b64_json) {
84
+ const buffer = Buffer.from(imageData.b64_json, 'base64');
85
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
86
+ fs.writeFileSync(outPath, buffer);
87
+ }
88
+ else if (imageData.url) {
89
+ // Download from URL (with 30s timeout)
90
+ const dlCtrl = new AbortController();
91
+ const dlTimeout = setTimeout(() => dlCtrl.abort(), 30_000);
92
+ const imgResp = await fetch(imageData.url, { signal: dlCtrl.signal });
93
+ clearTimeout(dlTimeout);
94
+ const buffer = Buffer.from(await imgResp.arrayBuffer());
95
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
96
+ fs.writeFileSync(outPath, buffer);
97
+ }
98
+ else {
99
+ return { output: 'No image data (b64_json or url) in response', isError: true };
100
+ }
101
+ const fileSize = fs.statSync(outPath).size;
102
+ const sizeKB = (fileSize / 1024).toFixed(1);
103
+ const revisedPrompt = imageData.revised_prompt ? `\nRevised prompt: ${imageData.revised_prompt}` : '';
104
+ let contentSummary = '';
105
+ if (contentId && deps.library) {
106
+ const rec = recordImageAsset(deps.library, {
107
+ contentId,
108
+ imagePath: outPath,
109
+ model: imageModel,
110
+ size: imageSize,
111
+ });
112
+ if (rec.ok) {
113
+ if (deps.onContentChange)
114
+ await deps.onContentChange();
115
+ const c = deps.library.get(contentId);
116
+ contentSummary =
117
+ `\n\n## Content updated\n` +
118
+ `- Attached to \`${contentId}\` at est. $${rec.costUsd.toFixed(2)}\n` +
119
+ (c
120
+ ? `- Spent: $${c.spentUsd.toFixed(2)} / $${c.budgetUsd.toFixed(2)} cap ` +
121
+ `(remaining $${(c.budgetUsd - c.spentUsd).toFixed(2)})`
122
+ : '');
123
+ }
124
+ else {
125
+ // Pre-flight guarded this, but keep defensive — bookkeeping refusal
126
+ // after a successful paid generation is rare (TOCTOU) but possible.
127
+ contentSummary =
128
+ `\n\n## Content NOT updated\n` +
129
+ `- ${rec.reason}\n` +
130
+ `- The image was generated and saved locally; cost was NOT recorded ` +
131
+ `against the content budget.`;
132
+ }
133
+ }
134
+ return {
135
+ output: `Image saved to ${outPath} (${sizeKB}KB, ${imageSize})${revisedPrompt}\n\nOpen with: open ${outPath}${contentSummary}`,
136
+ };
56
137
  }
57
- if (!response.ok) {
58
- const errText = await response.text().catch(() => '');
59
- return { output: `Image generation failed (${response.status}): ${errText.slice(0, 200)}`, isError: true };
60
- }
61
- const result = await response.json();
62
- const imageData = result.data?.[0];
63
- if (!imageData) {
64
- return { output: 'No image data returned from API', isError: true };
65
- }
66
- // Save image
67
- if (imageData.b64_json) {
68
- const buffer = Buffer.from(imageData.b64_json, 'base64');
69
- fs.mkdirSync(path.dirname(outPath), { recursive: true });
70
- fs.writeFileSync(outPath, buffer);
71
- }
72
- else if (imageData.url) {
73
- // Download from URL (with 30s timeout)
74
- const dlCtrl = new AbortController();
75
- const dlTimeout = setTimeout(() => dlCtrl.abort(), 30_000);
76
- const imgResp = await fetch(imageData.url, { signal: dlCtrl.signal });
77
- clearTimeout(dlTimeout);
78
- const buffer = Buffer.from(await imgResp.arrayBuffer());
79
- fs.mkdirSync(path.dirname(outPath), { recursive: true });
80
- fs.writeFileSync(outPath, buffer);
81
- }
82
- else {
83
- return { output: 'No image data (b64_json or url) in response', isError: true };
138
+ catch (err) {
139
+ const msg = err.message || '';
140
+ if (msg.includes('abort')) {
141
+ return { output: 'Image generation timed out (60s limit). Try a simpler prompt.', isError: true };
142
+ }
143
+ return { output: `Error: ${msg}`, isError: true };
84
144
  }
85
- const fileSize = fs.statSync(outPath).size;
86
- const sizeKB = (fileSize / 1024).toFixed(1);
87
- const revisedPrompt = imageData.revised_prompt ? `\nRevised prompt: ${imageData.revised_prompt}` : '';
88
- return {
89
- output: `Image saved to ${outPath} (${sizeKB}KB, ${imageSize})${revisedPrompt}\n\nOpen with: open ${outPath}`,
90
- };
91
- }
92
- catch (err) {
93
- const msg = err.message || '';
94
- if (msg.includes('abort')) {
95
- return { output: 'Image generation timed out (60s limit). Try a simpler prompt.', isError: true };
145
+ finally {
146
+ clearTimeout(timeout);
96
147
  }
97
- return { output: `Error: ${msg}`, isError: true };
98
- }
99
- finally {
100
- clearTimeout(timeout);
101
- }
148
+ };
102
149
  }
103
150
  // ─── Payment ───────────────────────────────────────────────────────────────
104
151
  async function signPayment(response, chain, endpoint) {
@@ -152,21 +199,37 @@ async function extractPaymentReq(response) {
152
199
  return header;
153
200
  }
154
201
  // ─── Export ────────────────────────────────────────────────────────────────
155
- export const imageGenCapability = {
156
- spec: {
157
- name: 'ImageGen',
158
- description: 'Generate an image from a text prompt using DALL-E. Costs USDC from the user\'s wallet — confirm before generating. Saves to a local file. Default size: 1024x1024. Do NOT call repeatedly to iterate on style — ask the user first.',
159
- input_schema: {
160
- type: 'object',
161
- properties: {
162
- prompt: { type: 'string', description: 'Text description of the image to generate' },
163
- output_path: { type: 'string', description: 'Where to save the image. Default: generated-<timestamp>.png in working directory' },
164
- size: { type: 'string', description: 'Image size: 1024x1024, 1792x1024, or 1024x1792. Default: 1024x1024' },
165
- model: { type: 'string', description: 'Image model to use. Default: openai/gpt-image-1' },
202
+ /**
203
+ * Build the ImageGen capability. Passing `deps.library` enables the
204
+ * contentId flow: pre-flight budget check + post-generation asset
205
+ * recording. With no deps, behavior matches the pre-factory version.
206
+ */
207
+ export function createImageGenCapability(deps = {}) {
208
+ return {
209
+ spec: {
210
+ name: 'ImageGen',
211
+ description: "Generate an image from a text prompt. Costs USDC from the user's wallet " +
212
+ "— confirm before generating. Saves to a local file. Default size: " +
213
+ "1024x1024. Do NOT call repeatedly to iterate on style — ask the user " +
214
+ "first. Pass contentId to attach the result to an existing Content " +
215
+ "piece: the content's budget is checked BEFORE paying, and on success " +
216
+ "the image is recorded as an asset with its estimated cost. Skipping " +
217
+ "contentId generates a one-off image with no budget tracking.",
218
+ input_schema: {
219
+ type: 'object',
220
+ properties: {
221
+ prompt: { type: 'string', description: 'Text description of the image to generate' },
222
+ output_path: { type: 'string', description: 'Where to save the image. Default: generated-<timestamp>.png in working directory' },
223
+ size: { type: 'string', description: 'Image size: 1024x1024, 1792x1024, or 1024x1792. Default: 1024x1024' },
224
+ model: { type: 'string', description: 'Image model to use. Default: openai/gpt-image-1' },
225
+ contentId: { type: 'string', description: 'Optional Content id to attach this generation to. Pre-flight budget check + auto-record on success.' },
226
+ },
227
+ required: ['prompt'],
166
228
  },
167
- required: ['prompt'],
168
229
  },
169
- },
170
- execute,
171
- concurrent: false,
172
- };
230
+ execute: buildExecute(deps),
231
+ concurrent: false,
232
+ };
233
+ }
234
+ /** Back-compat static capability for callers that don't want the Content bridge. */
235
+ export const imageGenCapability = createImageGenCapability();
@@ -11,6 +11,12 @@ import { grepCapability } from './grep.js';
11
11
  import { webFetchCapability } from './webfetch.js';
12
12
  import { webSearchCapability } from './websearch.js';
13
13
  import { taskCapability } from './task.js';
14
+ /**
15
+ * Reset module-level tool state that would otherwise leak between sessions
16
+ * when the same process runs `interactiveSession()` more than once (library
17
+ * callers, tests, planned daemon mode). Safe to call before every session.
18
+ */
19
+ export declare function resetToolSessionState(): void;
14
20
  /** All capabilities available to the Franklin agent (excluding sub-agent, which needs config). */
15
21
  export declare const allCapabilities: CapabilityHandler[];
16
22
  export { readCapability, writeCapability, editCapability, bashCapability, globCapability, grepCapability, webFetchCapability, webSearchCapability, taskCapability, };
@@ -1,21 +1,105 @@
1
1
  /**
2
2
  * Tool registry — exports all available capabilities for the agent.
3
3
  */
4
- import { readCapability } from './read.js';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { readCapability, clearSessionState as clearReadSessionState } from './read.js';
5
7
  import { writeCapability } from './write.js';
6
8
  import { editCapability } from './edit.js';
7
- import { bashCapability } from './bash.js';
9
+ import { bashCapability, clearSessionState as clearBashSessionState } from './bash.js';
8
10
  import { globCapability } from './glob.js';
9
11
  import { grepCapability } from './grep.js';
10
- import { webFetchCapability } from './webfetch.js';
12
+ import { webFetchCapability, clearSessionState as clearWebFetchSessionState } from './webfetch.js';
11
13
  import { webSearchCapability } from './websearch.js';
12
14
  import { taskCapability } from './task.js';
13
- import { imageGenCapability } from './imagegen.js';
15
+ import { createImageGenCapability } from './imagegen.js';
14
16
  import { askUserCapability } from './askuser.js';
15
17
  import { tradingSignalCapability, tradingMarketCapability } from './trading.js';
16
18
  import { searchXCapability } from './searchx.js';
17
19
  import { postToXCapability } from './posttox.js';
18
20
  import { moaCapability } from './moa.js';
21
+ import { createTradingCapabilities } from './trading-execute.js';
22
+ import { Portfolio } from '../trading/portfolio.js';
23
+ import { RiskEngine } from '../trading/risk.js';
24
+ import { LiveExchange } from '../trading/live-exchange.js';
25
+ import { TradingEngine } from '../trading/engine.js';
26
+ import { loadPortfolio, savePortfolio } from '../trading/store.js';
27
+ import { TradeLog } from '../trading/trade-log.js';
28
+ import { getPrice as cgGetPrice } from '../trading/data.js';
29
+ import { createContentCapabilities } from './content-execute.js';
30
+ import { ContentLibrary } from '../content/library.js';
31
+ import { loadLibrary as loadContentLibrary, saveLibrary as saveContentLibrary } from '../content/store.js';
32
+ // ─── Default Trading Engine ────────────────────────────────────────────────
33
+ // Paper trading defaults: $1000 starting bankroll, $400 per-position cap
34
+ // (2.5 positions fully loaded), $900 total exposure cap (keep 10% cash buffer).
35
+ // Live prices from CoinGecko; simulated fills at 10 bps. Portfolio persists
36
+ // to ~/.blockrun/portfolio.json across sessions — that persistence is the
37
+ // whole point of this vertical (Claude Code / Cursor cannot carry trading
38
+ // state between runs).
39
+ const DEFAULT_PORTFOLIO_PATH = path.join(os.homedir(), '.blockrun', 'portfolio.json');
40
+ const DEFAULT_TRADE_LOG_PATH = path.join(os.homedir(), '.blockrun', 'trades.jsonl');
41
+ const DEFAULT_STARTING_CASH_USD = 1_000;
42
+ const DEFAULT_RISK_CONFIG = { maxPositionUsd: 400, maxTotalExposureUsd: 900 };
43
+ const DEFAULT_FEE_BPS = 10;
44
+ function buildDefaultTradingCapabilities() {
45
+ const portfolio = loadPortfolio(DEFAULT_PORTFOLIO_PATH) ??
46
+ new Portfolio({ startingCashUsd: DEFAULT_STARTING_CASH_USD });
47
+ const risk = new RiskEngine(DEFAULT_RISK_CONFIG);
48
+ const exchange = new LiveExchange({
49
+ pricing: { getPrice: cgGetPrice },
50
+ feeBps: DEFAULT_FEE_BPS,
51
+ });
52
+ const engine = new TradingEngine({ portfolio, risk, exchange });
53
+ const tradeLog = new TradeLog(DEFAULT_TRADE_LOG_PATH);
54
+ return createTradingCapabilities({
55
+ engine,
56
+ riskConfig: DEFAULT_RISK_CONFIG,
57
+ tradeLog,
58
+ onStateChange: () => {
59
+ try {
60
+ savePortfolio(portfolio, DEFAULT_PORTFOLIO_PATH);
61
+ }
62
+ catch {
63
+ // Persistence best-effort — never block a trade on disk failure.
64
+ }
65
+ },
66
+ });
67
+ }
68
+ const defaultTradingCapabilities = buildDefaultTradingCapabilities();
69
+ // ─── Default Content Library ──────────────────────────────────────────────
70
+ // Durable content projects at ~/.blockrun/content.json. Like the portfolio,
71
+ // this is persistent cross-session state — something Claude Code structurally
72
+ // cannot offer.
73
+ const DEFAULT_CONTENT_PATH = path.join(os.homedir(), '.blockrun', 'content.json');
74
+ // Build a single ContentLibrary instance so both the Content capabilities and
75
+ // the content-aware ImageGen capability share state and persistence.
76
+ const defaultContentLibrary = loadContentLibrary(DEFAULT_CONTENT_PATH) ?? new ContentLibrary();
77
+ const persistContentLibrary = () => {
78
+ try {
79
+ saveContentLibrary(defaultContentLibrary, DEFAULT_CONTENT_PATH);
80
+ }
81
+ catch {
82
+ // Best-effort — in-memory library remains authoritative.
83
+ }
84
+ };
85
+ const defaultContentCapabilities = createContentCapabilities({
86
+ library: defaultContentLibrary,
87
+ onStateChange: persistContentLibrary,
88
+ });
89
+ const defaultImageGenCapability = createImageGenCapability({
90
+ library: defaultContentLibrary,
91
+ onContentChange: persistContentLibrary,
92
+ });
93
+ /**
94
+ * Reset module-level tool state that would otherwise leak between sessions
95
+ * when the same process runs `interactiveSession()` more than once (library
96
+ * callers, tests, planned daemon mode). Safe to call before every session.
97
+ */
98
+ export function resetToolSessionState() {
99
+ clearReadSessionState();
100
+ clearWebFetchSessionState();
101
+ clearBashSessionState();
102
+ }
19
103
  /** All capabilities available to the Franklin agent (excluding sub-agent, which needs config). */
20
104
  export const allCapabilities = [
21
105
  readCapability,
@@ -27,10 +111,12 @@ export const allCapabilities = [
27
111
  webFetchCapability,
28
112
  webSearchCapability,
29
113
  taskCapability,
30
- imageGenCapability,
114
+ defaultImageGenCapability,
31
115
  askUserCapability,
32
116
  tradingSignalCapability,
33
117
  tradingMarketCapability,
118
+ ...defaultTradingCapabilities, // TradingPortfolio, TradingOpenPosition, TradingClosePosition, TradingHistory
119
+ ...defaultContentCapabilities, // ContentCreate, ContentAddAsset, ContentShow, ContentList
34
120
  searchXCapability,
35
121
  postToXCapability,
36
122
  moaCapability,
@@ -24,4 +24,17 @@ export declare const fileReadTracker: Map<string, {
24
24
  }>;
25
25
  /** Invalidate the content cache for a file (call after Edit/Write modifies it). */
26
26
  export declare function invalidateFileCache(resolvedPath: string): void;
27
+ /**
28
+ * Reset all module-level tracking state for a fresh session.
29
+ *
30
+ * These Maps live at module scope (not inside a class) because read/edit/write
31
+ * tools share them to enforce "read-before-edit" and to cache unchanged files.
32
+ * When a library caller invokes `interactiveSession()` a second time in the
33
+ * same process, stale entries from the prior session would:
34
+ * - make Edit/Write falsely believe files were read in this session
35
+ * - serve cached content for files that may have changed externally
36
+ * - keep partial-read bounds from the wrong session
37
+ * Called from the agent loop at session start to guarantee a clean slate.
38
+ */
39
+ export declare function clearSessionState(): void;
27
40
  export declare const readCapability: CapabilityHandler;
@@ -34,6 +34,23 @@ function cacheKey(resolved, offset, limit) {
34
34
  export function invalidateFileCache(resolvedPath) {
35
35
  fileContentCache.delete(resolvedPath);
36
36
  }
37
+ /**
38
+ * Reset all module-level tracking state for a fresh session.
39
+ *
40
+ * These Maps live at module scope (not inside a class) because read/edit/write
41
+ * tools share them to enforce "read-before-edit" and to cache unchanged files.
42
+ * When a library caller invokes `interactiveSession()` a second time in the
43
+ * same process, stale entries from the prior session would:
44
+ * - make Edit/Write falsely believe files were read in this session
45
+ * - serve cached content for files that may have changed externally
46
+ * - keep partial-read bounds from the wrong session
47
+ * Called from the agent loop at session start to guarantee a clean slate.
48
+ */
49
+ export function clearSessionState() {
50
+ fileReadTracker.clear();
51
+ partiallyReadFiles.clear();
52
+ fileContentCache.clear();
53
+ }
37
54
  async function execute(input, ctx) {
38
55
  const { file_path: filePath, offset, limit } = input;
39
56
  if (!filePath) {
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Trading execution capabilities. Exposes Franklin's Portfolio + RiskEngine
3
+ * + Exchange stack to the agent as three tools: TradingPortfolio (read),
4
+ * TradingOpenPosition (buy side), TradingClosePosition (sell side).
5
+ *
6
+ * This is the surface that differentiates Franklin from generic coding
7
+ * agents — Claude Code and Cursor cannot hold a wallet, track positions
8
+ * across sessions, or reason about P&L. Every output here is deliberately
9
+ * information-rich so the agent has the numbers it needs to make the next
10
+ * economic decision (cash left, risk utilization, unrealized vs realized
11
+ * P&L, fill detail) without a follow-up tool call.
12
+ *
13
+ * Factory-style construction (createTradingCapabilities) keeps testing
14
+ * clean: production code calls it with a default disk-backed engine;
15
+ * tests inject a MockExchange-backed engine and assert behavior without
16
+ * touching disk.
17
+ */
18
+ import type { CapabilityHandler } from '../agent/types.js';
19
+ import type { TradingEngine } from '../trading/engine.js';
20
+ import type { RiskConfig } from '../trading/risk.js';
21
+ import type { TradeLog } from '../trading/trade-log.js';
22
+ export interface TradingCapabilitiesDeps {
23
+ engine: TradingEngine;
24
+ /** Risk config used to report "you're using X% of your position cap". */
25
+ riskConfig?: RiskConfig;
26
+ /** Optional hook run after every state-changing call (e.g., persist to disk). */
27
+ onStateChange?: () => void | Promise<void>;
28
+ /**
29
+ * Optional persistent trade log. When provided, opens and closes are
30
+ * appended to it and the TradingHistory capability is registered so the
31
+ * agent can query cross-session P&L.
32
+ */
33
+ tradeLog?: TradeLog;
34
+ }
35
+ export declare function createTradingCapabilities(deps: TradingCapabilitiesDeps): CapabilityHandler[];