@blockrun/franklin 3.7.10 → 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 (50) 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.js +2 -1
  6. package/dist/agent/loop.js +88 -18
  7. package/dist/agent/optimize.js +4 -0
  8. package/dist/agent/tokens.d.ts +7 -3
  9. package/dist/agent/tokens.js +14 -7
  10. package/dist/agent/tool-guard.js +64 -26
  11. package/dist/content/image-pricing.d.ts +14 -0
  12. package/dist/content/image-pricing.js +32 -0
  13. package/dist/content/library.d.ts +63 -0
  14. package/dist/content/library.js +75 -0
  15. package/dist/content/record-image.d.ts +43 -0
  16. package/dist/content/record-image.js +50 -0
  17. package/dist/content/store.d.ts +15 -0
  18. package/dist/content/store.js +55 -0
  19. package/dist/pricing.d.ts +1 -1
  20. package/dist/pricing.js +2 -2
  21. package/dist/router/index.js +17 -6
  22. package/dist/tools/bash.d.ts +8 -0
  23. package/dist/tools/bash.js +13 -0
  24. package/dist/tools/content-execute.d.ts +26 -0
  25. package/dist/tools/content-execute.js +212 -0
  26. package/dist/tools/imagegen.d.ts +14 -0
  27. package/dist/tools/imagegen.js +164 -101
  28. package/dist/tools/index.d.ts +6 -0
  29. package/dist/tools/index.js +91 -5
  30. package/dist/tools/read.d.ts +13 -0
  31. package/dist/tools/read.js +17 -0
  32. package/dist/tools/trading-execute.d.ts +35 -0
  33. package/dist/tools/trading-execute.js +297 -0
  34. package/dist/tools/webfetch.d.ts +6 -0
  35. package/dist/tools/webfetch.js +8 -0
  36. package/dist/trading/engine.d.ts +51 -0
  37. package/dist/trading/engine.js +75 -0
  38. package/dist/trading/live-exchange.d.ts +43 -0
  39. package/dist/trading/live-exchange.js +48 -0
  40. package/dist/trading/mock-exchange.d.ts +40 -0
  41. package/dist/trading/mock-exchange.js +41 -0
  42. package/dist/trading/portfolio.d.ts +67 -0
  43. package/dist/trading/portfolio.js +106 -0
  44. package/dist/trading/risk.d.ts +34 -0
  45. package/dist/trading/risk.js +64 -0
  46. package/dist/trading/store.d.ts +9 -0
  47. package/dist/trading/store.js +32 -0
  48. package/dist/trading/trade-log.d.ts +39 -0
  49. package/dist/trading/trade-log.js +81 -0
  50. package/package.json +1 -1
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Paper-trading Portfolio.
3
+ *
4
+ * Tracks cash, positions, and P&L as the agent executes trades. Pure in-memory
5
+ * math — an Exchange (mock or real) produces Fill events; Portfolio applies
6
+ * them. Persistence is handled separately in store.ts so tests don't touch disk.
7
+ *
8
+ * This is the execution substrate for Franklin's Trading Agent vertical —
9
+ * the first place where "the AI agent with a wallet" actually makes autonomous
10
+ * economic decisions and carries real P&L. No live-exchange integration here
11
+ * yet; MockExchange (mock-exchange.ts) gives deterministic fills for testing,
12
+ * and a real ExchangeClient adapter can be dropped in later against the same
13
+ * Fill contract.
14
+ */
15
+ export type Side = 'buy' | 'sell';
16
+ export interface Fill {
17
+ symbol: string;
18
+ side: Side;
19
+ qty: number;
20
+ priceUsd: number;
21
+ feeUsd?: number;
22
+ }
23
+ export interface Position {
24
+ symbol: string;
25
+ qty: number;
26
+ avgPriceUsd: number;
27
+ }
28
+ export interface PortfolioOptions {
29
+ startingCashUsd: number;
30
+ }
31
+ export interface MarketSnapshot {
32
+ equityUsd: number;
33
+ cashUsd: number;
34
+ unrealizedPnlUsd: number;
35
+ realizedPnlUsd: number;
36
+ positions: Array<Position & {
37
+ markUsd: number;
38
+ unrealizedPnlUsd: number;
39
+ }>;
40
+ }
41
+ export declare class Portfolio {
42
+ cashUsd: number;
43
+ realizedPnlUsd: number;
44
+ private positions;
45
+ constructor(opts: PortfolioOptions);
46
+ getPosition(symbol: string): Position | undefined;
47
+ listPositions(): Position[];
48
+ /** Serializable snapshot for persistence; paired with `restore()`. */
49
+ snapshot(): {
50
+ cashUsd: number;
51
+ realizedPnlUsd: number;
52
+ positions: Position[];
53
+ };
54
+ /** Rehydrate state from a prior snapshot; overwrites all current fields. */
55
+ restore(snap: {
56
+ cashUsd: number;
57
+ realizedPnlUsd: number;
58
+ positions: Position[];
59
+ }): void;
60
+ applyFill(fill: Fill): void;
61
+ /**
62
+ * Value the portfolio against a live price table. Callers supply the marks
63
+ * (e.g. from TradingSignal or a live feed) so this stays pure and testable.
64
+ * Symbols with no mark are valued at avgPriceUsd (zero unrealized P&L).
65
+ */
66
+ markToMarket(priceTable: Record<string, number>): MarketSnapshot;
67
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Paper-trading Portfolio.
3
+ *
4
+ * Tracks cash, positions, and P&L as the agent executes trades. Pure in-memory
5
+ * math — an Exchange (mock or real) produces Fill events; Portfolio applies
6
+ * them. Persistence is handled separately in store.ts so tests don't touch disk.
7
+ *
8
+ * This is the execution substrate for Franklin's Trading Agent vertical —
9
+ * the first place where "the AI agent with a wallet" actually makes autonomous
10
+ * economic decisions and carries real P&L. No live-exchange integration here
11
+ * yet; MockExchange (mock-exchange.ts) gives deterministic fills for testing,
12
+ * and a real ExchangeClient adapter can be dropped in later against the same
13
+ * Fill contract.
14
+ */
15
+ export class Portfolio {
16
+ cashUsd;
17
+ realizedPnlUsd = 0;
18
+ positions = new Map();
19
+ constructor(opts) {
20
+ this.cashUsd = opts.startingCashUsd;
21
+ }
22
+ getPosition(symbol) {
23
+ return this.positions.get(symbol);
24
+ }
25
+ listPositions() {
26
+ return [...this.positions.values()];
27
+ }
28
+ /** Serializable snapshot for persistence; paired with `restore()`. */
29
+ snapshot() {
30
+ return {
31
+ cashUsd: this.cashUsd,
32
+ realizedPnlUsd: this.realizedPnlUsd,
33
+ positions: this.listPositions().map((p) => ({ ...p })),
34
+ };
35
+ }
36
+ /** Rehydrate state from a prior snapshot; overwrites all current fields. */
37
+ restore(snap) {
38
+ this.cashUsd = snap.cashUsd;
39
+ this.realizedPnlUsd = snap.realizedPnlUsd;
40
+ this.positions.clear();
41
+ for (const p of snap.positions)
42
+ this.positions.set(p.symbol, { ...p });
43
+ }
44
+ applyFill(fill) {
45
+ const fee = fill.feeUsd ?? 0;
46
+ const notional = fill.qty * fill.priceUsd;
47
+ if (fill.side === 'buy') {
48
+ const existing = this.positions.get(fill.symbol);
49
+ if (!existing) {
50
+ this.positions.set(fill.symbol, {
51
+ symbol: fill.symbol,
52
+ qty: fill.qty,
53
+ avgPriceUsd: fill.priceUsd,
54
+ });
55
+ }
56
+ else {
57
+ // Weighted-average price update.
58
+ const totalQty = existing.qty + fill.qty;
59
+ const totalCost = existing.qty * existing.avgPriceUsd + notional;
60
+ existing.qty = totalQty;
61
+ existing.avgPriceUsd = totalCost / totalQty;
62
+ }
63
+ this.cashUsd -= notional + fee;
64
+ }
65
+ else {
66
+ // sell: close or reduce existing position, realize P&L against avg price
67
+ const existing = this.positions.get(fill.symbol);
68
+ if (!existing) {
69
+ throw new Error(`Cannot sell ${fill.symbol}: no open position`);
70
+ }
71
+ if (fill.qty > existing.qty + 1e-12) {
72
+ throw new Error(`Cannot sell ${fill.qty} ${fill.symbol}: only ${existing.qty} held`);
73
+ }
74
+ const realized = fill.qty * (fill.priceUsd - existing.avgPriceUsd) - fee;
75
+ this.realizedPnlUsd += realized;
76
+ existing.qty -= fill.qty;
77
+ this.cashUsd += notional - fee;
78
+ if (existing.qty <= 1e-12) {
79
+ this.positions.delete(fill.symbol);
80
+ }
81
+ }
82
+ }
83
+ /**
84
+ * Value the portfolio against a live price table. Callers supply the marks
85
+ * (e.g. from TradingSignal or a live feed) so this stays pure and testable.
86
+ * Symbols with no mark are valued at avgPriceUsd (zero unrealized P&L).
87
+ */
88
+ markToMarket(priceTable) {
89
+ let unrealized = 0;
90
+ let marketValue = 0;
91
+ const positions = this.listPositions().map((p) => {
92
+ const mark = priceTable[p.symbol] ?? p.avgPriceUsd;
93
+ const pnl = p.qty * (mark - p.avgPriceUsd);
94
+ unrealized += pnl;
95
+ marketValue += p.qty * mark;
96
+ return { ...p, markUsd: mark, unrealizedPnlUsd: pnl };
97
+ });
98
+ return {
99
+ equityUsd: this.cashUsd + marketValue,
100
+ cashUsd: this.cashUsd,
101
+ unrealizedPnlUsd: unrealized,
102
+ realizedPnlUsd: this.realizedPnlUsd,
103
+ positions,
104
+ };
105
+ }
106
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * RiskEngine — pre-trade guardrails the agent must clear before an order
3
+ * touches the exchange. Pure function style: the engine holds only config;
4
+ * Portfolio state is passed in per call so the same engine is reusable.
5
+ *
6
+ * Guardrails enforced (MVP):
7
+ * - Per-position cap (USD notional any single symbol may hold)
8
+ * - Total exposure cap (sum of all open positions' notional)
9
+ * - Cash sufficiency (can't buy what you can't pay for)
10
+ * - Sell integrity handled by Portfolio itself (no open position → throws)
11
+ *
12
+ * Exit orders (sells of existing positions) bypass exposure caps — a paranoid
13
+ * cap could otherwise trap the agent in a losing position it wants to exit.
14
+ */
15
+ import type { Portfolio, Side } from './portfolio.js';
16
+ export interface RiskConfig {
17
+ maxPositionUsd: number;
18
+ maxTotalExposureUsd: number;
19
+ }
20
+ export interface OrderRequest {
21
+ symbol: string;
22
+ side: Side;
23
+ qty: number;
24
+ priceUsd: number;
25
+ }
26
+ export interface RiskDecision {
27
+ allowed: boolean;
28
+ reason?: string;
29
+ }
30
+ export declare class RiskEngine {
31
+ private config;
32
+ constructor(config: RiskConfig);
33
+ check(portfolio: Portfolio, order: OrderRequest): RiskDecision;
34
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * RiskEngine — pre-trade guardrails the agent must clear before an order
3
+ * touches the exchange. Pure function style: the engine holds only config;
4
+ * Portfolio state is passed in per call so the same engine is reusable.
5
+ *
6
+ * Guardrails enforced (MVP):
7
+ * - Per-position cap (USD notional any single symbol may hold)
8
+ * - Total exposure cap (sum of all open positions' notional)
9
+ * - Cash sufficiency (can't buy what you can't pay for)
10
+ * - Sell integrity handled by Portfolio itself (no open position → throws)
11
+ *
12
+ * Exit orders (sells of existing positions) bypass exposure caps — a paranoid
13
+ * cap could otherwise trap the agent in a losing position it wants to exit.
14
+ */
15
+ export class RiskEngine {
16
+ config;
17
+ constructor(config) {
18
+ this.config = config;
19
+ }
20
+ check(portfolio, order) {
21
+ // Sells of existing positions are always permitted; exposure caps are
22
+ // entry-side only, and Portfolio.applyFill enforces that we don't sell
23
+ // more than we hold.
24
+ if (order.side === 'sell') {
25
+ const pos = portfolio.getPosition(order.symbol);
26
+ if (!pos) {
27
+ return { allowed: false, reason: `No open ${order.symbol} position to sell` };
28
+ }
29
+ return { allowed: true };
30
+ }
31
+ const notional = order.qty * order.priceUsd;
32
+ if (notional > portfolio.cashUsd) {
33
+ return {
34
+ allowed: false,
35
+ reason: `Insufficient cash: order needs $${notional.toFixed(2)} but only $${portfolio.cashUsd.toFixed(2)} available`,
36
+ };
37
+ }
38
+ // Projected position value after fill.
39
+ const existing = portfolio.getPosition(order.symbol);
40
+ const projectedPositionUsd = (existing ? existing.qty * order.priceUsd : 0) + notional;
41
+ if (projectedPositionUsd > this.config.maxPositionUsd) {
42
+ return {
43
+ allowed: false,
44
+ reason: `Exceeds per-position cap: projected $${projectedPositionUsd.toFixed(2)} > cap $${this.config.maxPositionUsd.toFixed(2)}`,
45
+ };
46
+ }
47
+ // Projected total exposure after fill. Marks all other positions at
48
+ // their avg price (live marks would be nicer, but the engine is
49
+ // intentionally pure and doesn't fetch).
50
+ let otherExposure = 0;
51
+ for (const p of portfolio.listPositions()) {
52
+ if (p.symbol !== order.symbol)
53
+ otherExposure += p.qty * p.avgPriceUsd;
54
+ }
55
+ const projectedTotalUsd = otherExposure + projectedPositionUsd;
56
+ if (projectedTotalUsd > this.config.maxTotalExposureUsd) {
57
+ return {
58
+ allowed: false,
59
+ reason: `Exceeds total exposure cap: projected $${projectedTotalUsd.toFixed(2)} > cap $${this.config.maxTotalExposureUsd.toFixed(2)}`,
60
+ };
61
+ }
62
+ return { allowed: true };
63
+ }
64
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Portfolio persistence. Stored as JSON alongside the rest of Franklin's
3
+ * per-user state under `~/.blockrun/portfolio.json` by default. Read/write
4
+ * errors never throw — a missing or corrupt file just returns `null` so the
5
+ * agent can fall back to a fresh portfolio rather than refusing to start.
6
+ */
7
+ import { Portfolio } from './portfolio.js';
8
+ export declare function savePortfolio(pf: Portfolio, filePath: string): void;
9
+ export declare function loadPortfolio(filePath: string): Portfolio | null;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Portfolio persistence. Stored as JSON alongside the rest of Franklin's
3
+ * per-user state under `~/.blockrun/portfolio.json` by default. Read/write
4
+ * errors never throw — a missing or corrupt file just returns `null` so the
5
+ * agent can fall back to a fresh portfolio rather than refusing to start.
6
+ */
7
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
8
+ import { dirname } from 'node:path';
9
+ import { Portfolio } from './portfolio.js';
10
+ export function savePortfolio(pf, filePath) {
11
+ mkdirSync(dirname(filePath), { recursive: true });
12
+ writeFileSync(filePath, JSON.stringify(pf.snapshot(), null, 2), 'utf-8');
13
+ }
14
+ export function loadPortfolio(filePath) {
15
+ if (!existsSync(filePath))
16
+ return null;
17
+ try {
18
+ const raw = JSON.parse(readFileSync(filePath, 'utf-8'));
19
+ if (typeof raw?.cashUsd !== 'number' ||
20
+ typeof raw?.realizedPnlUsd !== 'number' ||
21
+ !Array.isArray(raw?.positions)) {
22
+ return null;
23
+ }
24
+ const pf = new Portfolio({ startingCashUsd: 0 });
25
+ pf.restore(raw);
26
+ return pf;
27
+ }
28
+ catch {
29
+ // Corrupt JSON — start fresh rather than crash.
30
+ return null;
31
+ }
32
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * TradeLog — JSONL persistent record of every fill the agent executes.
3
+ *
4
+ * Purpose: cross-session P&L memory. The Portfolio snapshot tells you
5
+ * current state; the TradeLog tells you how you got there. This is the
6
+ * load-bearing surface for answers to questions like:
7
+ * - "What was my best / worst trade this week?"
8
+ * - "Am I up or down over the last 30 days?"
9
+ * - "How many times did I flip BTC in the last session?"
10
+ *
11
+ * Claude Code and Cursor can't answer any of these — they have no
12
+ * persistent economic memory across sessions. Franklin can.
13
+ *
14
+ * Format: one JSON object per line, append-only. Reads parse lazily and
15
+ * skip malformed lines rather than crash, so a partial write from a
16
+ * prior crash never bricks the log.
17
+ */
18
+ import type { Side } from './portfolio.js';
19
+ export interface TradeLogEntry {
20
+ timestamp: number;
21
+ symbol: string;
22
+ side: Side;
23
+ qty: number;
24
+ priceUsd: number;
25
+ feeUsd: number;
26
+ /** Realized P&L from this specific fill — 0 for opens, ± for closes. */
27
+ realizedPnlUsd: number;
28
+ }
29
+ export declare class TradeLog {
30
+ private filePath;
31
+ constructor(filePath: string);
32
+ append(entry: TradeLogEntry): void;
33
+ /** Read all entries from disk in chronological order. */
34
+ all(): TradeLogEntry[];
35
+ /** Most recent N entries, newest-first. */
36
+ recent(n: number): TradeLogEntry[];
37
+ /** Signed sum of realizedPnlUsd across every entry with timestamp >= since. */
38
+ realizedSince(since: number): number;
39
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * TradeLog — JSONL persistent record of every fill the agent executes.
3
+ *
4
+ * Purpose: cross-session P&L memory. The Portfolio snapshot tells you
5
+ * current state; the TradeLog tells you how you got there. This is the
6
+ * load-bearing surface for answers to questions like:
7
+ * - "What was my best / worst trade this week?"
8
+ * - "Am I up or down over the last 30 days?"
9
+ * - "How many times did I flip BTC in the last session?"
10
+ *
11
+ * Claude Code and Cursor can't answer any of these — they have no
12
+ * persistent economic memory across sessions. Franklin can.
13
+ *
14
+ * Format: one JSON object per line, append-only. Reads parse lazily and
15
+ * skip malformed lines rather than crash, so a partial write from a
16
+ * prior crash never bricks the log.
17
+ */
18
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
19
+ import { dirname } from 'node:path';
20
+ export class TradeLog {
21
+ filePath;
22
+ constructor(filePath) {
23
+ this.filePath = filePath;
24
+ }
25
+ append(entry) {
26
+ try {
27
+ mkdirSync(dirname(this.filePath), { recursive: true });
28
+ appendFileSync(this.filePath, JSON.stringify(entry) + '\n', 'utf-8');
29
+ }
30
+ catch {
31
+ // Best-effort persistence; never block a trade on disk failure.
32
+ }
33
+ }
34
+ /** Read all entries from disk in chronological order. */
35
+ all() {
36
+ if (!existsSync(this.filePath))
37
+ return [];
38
+ let raw;
39
+ try {
40
+ raw = readFileSync(this.filePath, 'utf-8');
41
+ }
42
+ catch {
43
+ return [];
44
+ }
45
+ const out = [];
46
+ for (const line of raw.split('\n')) {
47
+ if (!line.trim())
48
+ continue;
49
+ try {
50
+ const obj = JSON.parse(line);
51
+ if (typeof obj?.timestamp === 'number' &&
52
+ typeof obj?.symbol === 'string' &&
53
+ (obj.side === 'buy' || obj.side === 'sell') &&
54
+ typeof obj.qty === 'number' &&
55
+ typeof obj.priceUsd === 'number' &&
56
+ typeof obj.feeUsd === 'number' &&
57
+ typeof obj.realizedPnlUsd === 'number') {
58
+ out.push(obj);
59
+ }
60
+ }
61
+ catch {
62
+ // Corrupt line — skip, don't crash.
63
+ }
64
+ }
65
+ return out;
66
+ }
67
+ /** Most recent N entries, newest-first. */
68
+ recent(n) {
69
+ const all = this.all();
70
+ return all.slice(-n).reverse();
71
+ }
72
+ /** Signed sum of realizedPnlUsd across every entry with timestamp >= since. */
73
+ realizedSince(since) {
74
+ let total = 0;
75
+ for (const e of this.all()) {
76
+ if (e.timestamp >= since)
77
+ total += e.realizedPnlUsd;
78
+ }
79
+ return total;
80
+ }
81
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.7.10",
3
+ "version": "3.8.0",
4
4
  "description": "Franklin — The AI agent with a wallet. Spends USDC autonomously to get real work done. Pay per action, no subscriptions.",
5
5
  "type": "module",
6
6
  "exports": {