@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.
- package/dist/agent/bash-guard.js +8 -2
- package/dist/agent/compact.d.ts +14 -0
- package/dist/agent/compact.js +57 -1
- package/dist/agent/context.js +6 -4
- package/dist/agent/llm.d.ts +25 -0
- package/dist/agent/llm.js +27 -12
- package/dist/agent/loop.js +88 -18
- package/dist/agent/optimize.js +4 -0
- package/dist/agent/tokens.d.ts +7 -3
- package/dist/agent/tokens.js +14 -7
- package/dist/agent/tool-guard.js +64 -26
- package/dist/content/image-pricing.d.ts +14 -0
- package/dist/content/image-pricing.js +32 -0
- package/dist/content/library.d.ts +63 -0
- package/dist/content/library.js +75 -0
- package/dist/content/record-image.d.ts +43 -0
- package/dist/content/record-image.js +50 -0
- package/dist/content/store.d.ts +15 -0
- package/dist/content/store.js +55 -0
- package/dist/pricing.d.ts +1 -1
- package/dist/pricing.js +2 -2
- package/dist/router/index.js +17 -6
- package/dist/tools/bash.d.ts +8 -0
- package/dist/tools/bash.js +13 -0
- package/dist/tools/content-execute.d.ts +26 -0
- package/dist/tools/content-execute.js +212 -0
- package/dist/tools/imagegen.d.ts +14 -0
- package/dist/tools/imagegen.js +164 -101
- package/dist/tools/index.d.ts +6 -0
- package/dist/tools/index.js +91 -5
- package/dist/tools/read.d.ts +13 -0
- package/dist/tools/read.js +17 -0
- package/dist/tools/trading-execute.d.ts +35 -0
- package/dist/tools/trading-execute.js +297 -0
- package/dist/tools/webfetch.d.ts +6 -0
- package/dist/tools/webfetch.js +8 -0
- package/dist/trading/engine.d.ts +51 -0
- package/dist/trading/engine.js +75 -0
- package/dist/trading/live-exchange.d.ts +43 -0
- package/dist/trading/live-exchange.js +48 -0
- package/dist/trading/mock-exchange.d.ts +40 -0
- package/dist/trading/mock-exchange.js +41 -0
- package/dist/trading/portfolio.d.ts +67 -0
- package/dist/trading/portfolio.js +106 -0
- package/dist/trading/risk.d.ts +34 -0
- package/dist/trading/risk.js +64 -0
- package/dist/trading/store.d.ts +9 -0
- package/dist/trading/store.js +32 -0
- package/dist/trading/trade-log.d.ts +39 -0
- package/dist/trading/trade-log.js +81 -0
- package/package.json +1 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export type ContentType = 'x-thread' | 'blog' | 'podcast' | 'video' | 'ad-copy' | 'image';
|
|
2
|
+
export type ContentStatus = 'outline' | 'drafting' | 'assets' | 'review' | 'published';
|
|
3
|
+
export type AssetKind = 'image' | 'audio' | 'video' | 'text';
|
|
4
|
+
export interface ContentAsset {
|
|
5
|
+
kind: AssetKind;
|
|
6
|
+
/** Producer of the asset: model ID like "openai/dall-e-3", or "manual". */
|
|
7
|
+
source: string;
|
|
8
|
+
/** USD actually spent producing this asset. 0 is valid (free models). */
|
|
9
|
+
costUsd: number;
|
|
10
|
+
/** Optional payload reference — URL, file path, or short inline text. */
|
|
11
|
+
data?: string;
|
|
12
|
+
createdAt: number;
|
|
13
|
+
}
|
|
14
|
+
export interface ContentDraft {
|
|
15
|
+
text: string;
|
|
16
|
+
createdAt: number;
|
|
17
|
+
}
|
|
18
|
+
export interface DistributionEntry {
|
|
19
|
+
channel: string;
|
|
20
|
+
url?: string;
|
|
21
|
+
at: number;
|
|
22
|
+
}
|
|
23
|
+
export interface Content {
|
|
24
|
+
id: string;
|
|
25
|
+
type: ContentType;
|
|
26
|
+
title: string;
|
|
27
|
+
status: ContentStatus;
|
|
28
|
+
outline?: string;
|
|
29
|
+
drafts: ContentDraft[];
|
|
30
|
+
assets: ContentAsset[];
|
|
31
|
+
spentUsd: number;
|
|
32
|
+
budgetUsd: number;
|
|
33
|
+
createdAt: number;
|
|
34
|
+
publishedAt?: number;
|
|
35
|
+
distribution: DistributionEntry[];
|
|
36
|
+
}
|
|
37
|
+
export interface CreateContentOptions {
|
|
38
|
+
type: ContentType;
|
|
39
|
+
title: string;
|
|
40
|
+
budgetUsd: number;
|
|
41
|
+
}
|
|
42
|
+
export declare class ContentLibrary {
|
|
43
|
+
private byId;
|
|
44
|
+
create(opts: CreateContentOptions): Content;
|
|
45
|
+
get(id: string): Content | undefined;
|
|
46
|
+
list(): Content[];
|
|
47
|
+
/** Replace a content record wholesale — used by the persistence layer. */
|
|
48
|
+
restore(content: Content): void;
|
|
49
|
+
/**
|
|
50
|
+
* Record a generated asset against a content, enforcing the budget cap.
|
|
51
|
+
* Returns `{ ok: false, reason }` on rejection so callers (including the
|
|
52
|
+
* agent-facing capability) can surface the reason instead of catching an
|
|
53
|
+
* exception. On the happy path mutates the Content in place and returns
|
|
54
|
+
* the updated spendUsd.
|
|
55
|
+
*/
|
|
56
|
+
addAsset(id: string, asset: Omit<ContentAsset, 'createdAt'>): {
|
|
57
|
+
ok: true;
|
|
58
|
+
spentUsd: number;
|
|
59
|
+
} | {
|
|
60
|
+
ok: false;
|
|
61
|
+
reason: string;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ContentLibrary — persistent, cross-session state for content generation work.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the Trading vertical: Library is the state object, Budget is the
|
|
5
|
+
* risk engine, store.ts is the persistence adapter, and tools/content-execute.ts
|
|
6
|
+
* wires everything into agent-facing capabilities.
|
|
7
|
+
*
|
|
8
|
+
* Why this exists: Claude Code / Cursor cannot carry a content project across
|
|
9
|
+
* sessions. If you start drafting a podcast episode Monday and come back
|
|
10
|
+
* Wednesday, there's no concept of *the same* piece of work. Franklin tracks
|
|
11
|
+
* outline → drafts → assets → distribution as one durable object and lets
|
|
12
|
+
* the agent spend USDC (image generation, audio, stock footage) against a
|
|
13
|
+
* budget that survives session boundaries.
|
|
14
|
+
*/
|
|
15
|
+
import { randomUUID } from 'node:crypto';
|
|
16
|
+
export class ContentLibrary {
|
|
17
|
+
byId = new Map();
|
|
18
|
+
create(opts) {
|
|
19
|
+
if (opts.budgetUsd < 0) {
|
|
20
|
+
throw new Error('budgetUsd must be non-negative');
|
|
21
|
+
}
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
const content = {
|
|
24
|
+
id: randomUUID(),
|
|
25
|
+
type: opts.type,
|
|
26
|
+
title: opts.title,
|
|
27
|
+
status: 'outline',
|
|
28
|
+
drafts: [],
|
|
29
|
+
assets: [],
|
|
30
|
+
spentUsd: 0,
|
|
31
|
+
budgetUsd: opts.budgetUsd,
|
|
32
|
+
createdAt: now,
|
|
33
|
+
distribution: [],
|
|
34
|
+
};
|
|
35
|
+
this.byId.set(content.id, content);
|
|
36
|
+
return content;
|
|
37
|
+
}
|
|
38
|
+
get(id) {
|
|
39
|
+
return this.byId.get(id);
|
|
40
|
+
}
|
|
41
|
+
list() {
|
|
42
|
+
return [...this.byId.values()].sort((a, b) => b.createdAt - a.createdAt);
|
|
43
|
+
}
|
|
44
|
+
/** Replace a content record wholesale — used by the persistence layer. */
|
|
45
|
+
restore(content) {
|
|
46
|
+
this.byId.set(content.id, content);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Record a generated asset against a content, enforcing the budget cap.
|
|
50
|
+
* Returns `{ ok: false, reason }` on rejection so callers (including the
|
|
51
|
+
* agent-facing capability) can surface the reason instead of catching an
|
|
52
|
+
* exception. On the happy path mutates the Content in place and returns
|
|
53
|
+
* the updated spendUsd.
|
|
54
|
+
*/
|
|
55
|
+
addAsset(id, asset) {
|
|
56
|
+
const content = this.byId.get(id);
|
|
57
|
+
if (!content) {
|
|
58
|
+
return { ok: false, reason: `Content ${id} not found` };
|
|
59
|
+
}
|
|
60
|
+
if (asset.costUsd < 0) {
|
|
61
|
+
return { ok: false, reason: 'costUsd must be non-negative' };
|
|
62
|
+
}
|
|
63
|
+
const projected = content.spentUsd + asset.costUsd;
|
|
64
|
+
if (projected > content.budgetUsd + 1e-9) {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
reason: `Exceeds budget: spent $${content.spentUsd.toFixed(2)} + proposed ` +
|
|
68
|
+
`$${asset.costUsd.toFixed(2)} > cap $${content.budgetUsd.toFixed(2)}`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
content.assets.push({ ...asset, createdAt: Date.now() });
|
|
72
|
+
content.spentUsd = projected;
|
|
73
|
+
return { ok: true, spentUsd: projected };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recordImageAsset — wire an ImageGen result into a Content piece's budget.
|
|
3
|
+
*
|
|
4
|
+
* The ImageGen tool closes the loop between spending USDC and tracking it
|
|
5
|
+
* against a content project. When the agent generates a hero image for a
|
|
6
|
+
* piece of content, this helper:
|
|
7
|
+
* 1. Estimates the USD cost from model + size (see image-pricing.ts).
|
|
8
|
+
* 2. Calls ContentLibrary.addAsset, which enforces the per-piece budget.
|
|
9
|
+
* 3. Returns a structured decision so the caller (imagegen.ts) can format
|
|
10
|
+
* a human-readable summary without swallowing budget refusals.
|
|
11
|
+
*
|
|
12
|
+
* This lives in `content/` on purpose — it's content-bookkeeping logic that
|
|
13
|
+
* happens to be triggered from the ImageGen tool, not an ImageGen detail.
|
|
14
|
+
*/
|
|
15
|
+
import type { ContentLibrary } from './library.js';
|
|
16
|
+
export interface RecordImageAssetInput {
|
|
17
|
+
contentId: string;
|
|
18
|
+
imagePath: string;
|
|
19
|
+
model: string;
|
|
20
|
+
size: string;
|
|
21
|
+
}
|
|
22
|
+
export type RecordImageDecision = {
|
|
23
|
+
ok: true;
|
|
24
|
+
costUsd: number;
|
|
25
|
+
spentUsd: number;
|
|
26
|
+
} | {
|
|
27
|
+
ok: false;
|
|
28
|
+
reason: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Pre-flight budget check. Run this BEFORE hitting the image-generation
|
|
32
|
+
* endpoint so the agent doesn't spend real USDC on a fill it then can't
|
|
33
|
+
* book against the content's budget. Returns `{ ok: true }` when the
|
|
34
|
+
* estimated cost fits; `{ ok: false, reason }` when it doesn't or the
|
|
35
|
+
* content doesn't exist. Non-mutating.
|
|
36
|
+
*/
|
|
37
|
+
export declare function checkImageBudget(library: ContentLibrary, contentId: string, model: string, size: string): {
|
|
38
|
+
ok: true;
|
|
39
|
+
} | {
|
|
40
|
+
ok: false;
|
|
41
|
+
reason: string;
|
|
42
|
+
};
|
|
43
|
+
export declare function recordImageAsset(library: ContentLibrary, input: RecordImageAssetInput): RecordImageDecision;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recordImageAsset — wire an ImageGen result into a Content piece's budget.
|
|
3
|
+
*
|
|
4
|
+
* The ImageGen tool closes the loop between spending USDC and tracking it
|
|
5
|
+
* against a content project. When the agent generates a hero image for a
|
|
6
|
+
* piece of content, this helper:
|
|
7
|
+
* 1. Estimates the USD cost from model + size (see image-pricing.ts).
|
|
8
|
+
* 2. Calls ContentLibrary.addAsset, which enforces the per-piece budget.
|
|
9
|
+
* 3. Returns a structured decision so the caller (imagegen.ts) can format
|
|
10
|
+
* a human-readable summary without swallowing budget refusals.
|
|
11
|
+
*
|
|
12
|
+
* This lives in `content/` on purpose — it's content-bookkeeping logic that
|
|
13
|
+
* happens to be triggered from the ImageGen tool, not an ImageGen detail.
|
|
14
|
+
*/
|
|
15
|
+
import { estimateImageCostUsd } from './image-pricing.js';
|
|
16
|
+
/**
|
|
17
|
+
* Pre-flight budget check. Run this BEFORE hitting the image-generation
|
|
18
|
+
* endpoint so the agent doesn't spend real USDC on a fill it then can't
|
|
19
|
+
* book against the content's budget. Returns `{ ok: true }` when the
|
|
20
|
+
* estimated cost fits; `{ ok: false, reason }` when it doesn't or the
|
|
21
|
+
* content doesn't exist. Non-mutating.
|
|
22
|
+
*/
|
|
23
|
+
export function checkImageBudget(library, contentId, model, size) {
|
|
24
|
+
const content = library.get(contentId);
|
|
25
|
+
if (!content) {
|
|
26
|
+
return { ok: false, reason: `Content ${contentId} not found` };
|
|
27
|
+
}
|
|
28
|
+
const cost = estimateImageCostUsd(model, size);
|
|
29
|
+
if (content.spentUsd + cost > content.budgetUsd + 1e-9) {
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
reason: `Would exceed budget: spent $${content.spentUsd.toFixed(2)} + estimated ` +
|
|
33
|
+
`$${cost.toFixed(2)} > cap $${content.budgetUsd.toFixed(2)}`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return { ok: true };
|
|
37
|
+
}
|
|
38
|
+
export function recordImageAsset(library, input) {
|
|
39
|
+
const costUsd = estimateImageCostUsd(input.model, input.size);
|
|
40
|
+
const result = library.addAsset(input.contentId, {
|
|
41
|
+
kind: 'image',
|
|
42
|
+
source: input.model,
|
|
43
|
+
costUsd,
|
|
44
|
+
data: input.imagePath,
|
|
45
|
+
});
|
|
46
|
+
if (!result.ok) {
|
|
47
|
+
return { ok: false, reason: result.reason };
|
|
48
|
+
}
|
|
49
|
+
return { ok: true, costUsd, spentUsd: result.spentUsd };
|
|
50
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence for ContentLibrary. Whole-library JSON snapshot (vs the
|
|
3
|
+
* trade log's append-only JSONL) because content records are mutable
|
|
4
|
+
* throughout their life — new drafts, new assets, status transitions —
|
|
5
|
+
* so append-only would require replaying every edit on load. A single
|
|
6
|
+
* re-written JSON file is simpler and content volume is orders of
|
|
7
|
+
* magnitude smaller than a trade log.
|
|
8
|
+
*
|
|
9
|
+
* Disk failures and corruption are always survivable: save is
|
|
10
|
+
* best-effort, load returns `null` on any error so the agent falls back
|
|
11
|
+
* to a fresh in-memory library rather than refusing to start.
|
|
12
|
+
*/
|
|
13
|
+
import { ContentLibrary } from './library.js';
|
|
14
|
+
export declare function saveLibrary(lib: ContentLibrary, filePath: string): void;
|
|
15
|
+
export declare function loadLibrary(filePath: string): ContentLibrary | null;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence for ContentLibrary. Whole-library JSON snapshot (vs the
|
|
3
|
+
* trade log's append-only JSONL) because content records are mutable
|
|
4
|
+
* throughout their life — new drafts, new assets, status transitions —
|
|
5
|
+
* so append-only would require replaying every edit on load. A single
|
|
6
|
+
* re-written JSON file is simpler and content volume is orders of
|
|
7
|
+
* magnitude smaller than a trade log.
|
|
8
|
+
*
|
|
9
|
+
* Disk failures and corruption are always survivable: save is
|
|
10
|
+
* best-effort, load returns `null` on any error so the agent falls back
|
|
11
|
+
* to a fresh in-memory library rather than refusing to start.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
|
|
14
|
+
import { dirname } from 'node:path';
|
|
15
|
+
import { ContentLibrary } from './library.js';
|
|
16
|
+
export function saveLibrary(lib, filePath) {
|
|
17
|
+
try {
|
|
18
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
19
|
+
const payload = { version: 1, contents: lib.list() };
|
|
20
|
+
writeFileSync(filePath, JSON.stringify(payload, null, 2), 'utf-8');
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Persistence is best-effort; never block a capability call on disk
|
|
24
|
+
// failure. The in-memory library stays authoritative until the next
|
|
25
|
+
// successful save.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function loadLibrary(filePath) {
|
|
29
|
+
if (!existsSync(filePath))
|
|
30
|
+
return null;
|
|
31
|
+
try {
|
|
32
|
+
const raw = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
33
|
+
if (raw?.version !== 1 || !Array.isArray(raw.contents))
|
|
34
|
+
return null;
|
|
35
|
+
const lib = new ContentLibrary();
|
|
36
|
+
for (const c of raw.contents) {
|
|
37
|
+
// Defensive — skip records missing the load-bearing fields rather than
|
|
38
|
+
// importing partially-shaped data.
|
|
39
|
+
if (typeof c?.id !== 'string' ||
|
|
40
|
+
typeof c?.title !== 'string' ||
|
|
41
|
+
typeof c?.budgetUsd !== 'number' ||
|
|
42
|
+
typeof c?.spentUsd !== 'number' ||
|
|
43
|
+
!Array.isArray(c?.assets) ||
|
|
44
|
+
!Array.isArray(c?.drafts) ||
|
|
45
|
+
!Array.isArray(c?.distribution)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
lib.restore(c);
|
|
49
|
+
}
|
|
50
|
+
return lib;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
package/dist/pricing.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export declare const MODEL_PRICING: Record<string, {
|
|
|
7
7
|
output: number;
|
|
8
8
|
perCall?: number;
|
|
9
9
|
}>;
|
|
10
|
-
/** Opus pricing for savings calculations */
|
|
10
|
+
/** Opus pricing for savings calculations — tracks the current flagship. */
|
|
11
11
|
export declare const OPUS_PRICING: {
|
|
12
12
|
input: number;
|
|
13
13
|
output: number;
|
package/dist/pricing.js
CHANGED
|
@@ -22,8 +22,8 @@ export const MODEL_PRICING = {
|
|
|
22
22
|
'nvidia/llama-4-maverick': { input: 0, output: 0 },
|
|
23
23
|
// Anthropic
|
|
24
24
|
'anthropic/claude-sonnet-4.6': { input: 3.0, output: 15.0 },
|
|
25
|
-
'anthropic/claude-opus-4.6': { input: 5.0, output: 25.0 },
|
|
26
25
|
'anthropic/claude-opus-4.7': { input: 5.0, output: 25.0 },
|
|
26
|
+
'anthropic/claude-opus-4.6': { input: 5.0, output: 25.0 },
|
|
27
27
|
'anthropic/claude-haiku-4.5': { input: 1.0, output: 5.0 },
|
|
28
28
|
'anthropic/claude-haiku-4.5-20251001': { input: 1.0, output: 5.0 },
|
|
29
29
|
// OpenAI
|
|
@@ -76,7 +76,7 @@ export const MODEL_PRICING = {
|
|
|
76
76
|
'zai/glm-5-turbo': { input: 0, output: 0, perCall: 0.001 },
|
|
77
77
|
'zai/glm-5.1-turbo': { input: 0, output: 0, perCall: 0.001 }, // client alias for zai/glm-5-turbo
|
|
78
78
|
};
|
|
79
|
-
/** Opus pricing for savings calculations */
|
|
79
|
+
/** Opus pricing for savings calculations — tracks the current flagship. */
|
|
80
80
|
export const OPUS_PRICING = MODEL_PRICING['anthropic/claude-opus-4.7'];
|
|
81
81
|
/**
|
|
82
82
|
* Estimate cost in USD for a request.
|
package/dist/router/index.js
CHANGED
|
@@ -35,6 +35,8 @@ function loadLearnedWeights() {
|
|
|
35
35
|
// ─── Tier Model Configs ───
|
|
36
36
|
// Agent-first defaults. Claude Sonnet 4.6 is the industry standard for multi-step
|
|
37
37
|
// tool-use agent work; cheap models keep derailing on simple agent loops.
|
|
38
|
+
// Each tier's fallback ends with a cheaper option so payment/quota failures
|
|
39
|
+
// don't strand users on equally expensive alternatives.
|
|
38
40
|
const AUTO_TIERS = {
|
|
39
41
|
SIMPLE: {
|
|
40
42
|
primary: 'google/gemini-2.5-flash',
|
|
@@ -42,15 +44,24 @@ const AUTO_TIERS = {
|
|
|
42
44
|
},
|
|
43
45
|
MEDIUM: {
|
|
44
46
|
primary: 'anthropic/claude-sonnet-4.6',
|
|
45
|
-
fallback: ['openai/gpt-5.4', 'google/gemini-3.1-pro'],
|
|
47
|
+
fallback: ['openai/gpt-5.4', 'google/gemini-3.1-pro', 'moonshot/kimi-k2.5'],
|
|
46
48
|
},
|
|
47
49
|
COMPLEX: {
|
|
48
50
|
primary: 'anthropic/claude-sonnet-4.6',
|
|
49
|
-
fallback: ['openai/gpt-5.4', 'anthropic/claude-opus-4.7'],
|
|
51
|
+
fallback: ['openai/gpt-5.4', 'anthropic/claude-opus-4.7', 'moonshot/kimi-k2.5'],
|
|
50
52
|
},
|
|
51
53
|
REASONING: {
|
|
54
|
+
// Opus 4.7: step-change improvement in agentic coding over 4.6 per
|
|
55
|
+
// Anthropic. Same price, same 200k ctx in Franklin's baseline, so
|
|
56
|
+
// swap is cost-neutral. 4.6 stays in the fallback chain in case of
|
|
57
|
+
// rollout delays on the gateway side.
|
|
52
58
|
primary: 'anthropic/claude-opus-4.7',
|
|
53
|
-
fallback: [
|
|
59
|
+
fallback: [
|
|
60
|
+
'anthropic/claude-opus-4.6',
|
|
61
|
+
'openai/o3',
|
|
62
|
+
'xai/grok-4-1-fast-reasoning',
|
|
63
|
+
'deepseek/deepseek-reasoner',
|
|
64
|
+
],
|
|
54
65
|
},
|
|
55
66
|
};
|
|
56
67
|
const ECO_TIERS = {
|
|
@@ -82,11 +93,11 @@ const PREMIUM_TIERS = {
|
|
|
82
93
|
},
|
|
83
94
|
COMPLEX: {
|
|
84
95
|
primary: 'anthropic/claude-opus-4.7',
|
|
85
|
-
fallback: ['openai/gpt-5.4', 'anthropic/claude-sonnet-4.6'],
|
|
96
|
+
fallback: ['anthropic/claude-opus-4.6', 'openai/gpt-5.4', 'anthropic/claude-sonnet-4.6'],
|
|
86
97
|
},
|
|
87
98
|
REASONING: {
|
|
88
|
-
primary: 'anthropic/claude-
|
|
89
|
-
fallback: ['anthropic/claude-opus-4.
|
|
99
|
+
primary: 'anthropic/claude-opus-4.7',
|
|
100
|
+
fallback: ['anthropic/claude-opus-4.6', 'anthropic/claude-sonnet-4.6', 'openai/o3'],
|
|
90
101
|
},
|
|
91
102
|
};
|
|
92
103
|
// ─── Keywords for Classification ───
|
package/dist/tools/bash.d.ts
CHANGED
|
@@ -14,5 +14,13 @@ interface BackgroundTask {
|
|
|
14
14
|
export declare function getBackgroundTask(id: string): BackgroundTask | undefined;
|
|
15
15
|
/** List all background tasks. */
|
|
16
16
|
export declare function listBackgroundTasks(): BackgroundTask[];
|
|
17
|
+
/**
|
|
18
|
+
* Drop completed/failed task records. Running tasks are left in place —
|
|
19
|
+
* we have no safe way to terminate a spawned process from here, and killing
|
|
20
|
+
* one out from under a caller that may still be polling it would silently
|
|
21
|
+
* lose output. Callers starting a fresh session can still query prior
|
|
22
|
+
* running tasks by id if they need to; anything finished is gone.
|
|
23
|
+
*/
|
|
24
|
+
export declare function clearSessionState(): void;
|
|
17
25
|
export declare const bashCapability: CapabilityHandler;
|
|
18
26
|
export {};
|
package/dist/tools/bash.js
CHANGED
|
@@ -226,6 +226,19 @@ export function getBackgroundTask(id) {
|
|
|
226
226
|
export function listBackgroundTasks() {
|
|
227
227
|
return [...backgroundTasks.values()];
|
|
228
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Drop completed/failed task records. Running tasks are left in place —
|
|
231
|
+
* we have no safe way to terminate a spawned process from here, and killing
|
|
232
|
+
* one out from under a caller that may still be polling it would silently
|
|
233
|
+
* lose output. Callers starting a fresh session can still query prior
|
|
234
|
+
* running tasks by id if they need to; anything finished is gone.
|
|
235
|
+
*/
|
|
236
|
+
export function clearSessionState() {
|
|
237
|
+
for (const [id, task] of backgroundTasks) {
|
|
238
|
+
if (task.status !== 'running')
|
|
239
|
+
backgroundTasks.delete(id);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
229
242
|
const MAX_OUTPUT_BYTES = 512 * 1024; // 512KB capture buffer (prevents OOM)
|
|
230
243
|
const MAX_RETURN_CHARS = 32_000; // 32KB return cap (~8,000 tokens) — prevents context bloat
|
|
231
244
|
const DEFAULT_TIMEOUT_MS = 120_000; // 2 minutes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-facing Content capabilities. Four tools, deliberately mirroring the
|
|
3
|
+
* Trading vertical's shape:
|
|
4
|
+
*
|
|
5
|
+
* ContentCreate — start a new piece with type/title/budgetUsd
|
|
6
|
+
* ContentAddAsset — record a generated asset (image/audio/etc.) with its
|
|
7
|
+
* cost; rejected (as normal text, not an error) if it
|
|
8
|
+
* would breach the per-piece budget
|
|
9
|
+
* ContentShow — dump a single piece's state as actionable markdown
|
|
10
|
+
* ContentList — summary of every piece, newest first
|
|
11
|
+
*
|
|
12
|
+
* This is the surface Claude Code and Cursor cannot cover: durable content
|
|
13
|
+
* state with autonomous spending gating. An agent can spin up "Franklin
|
|
14
|
+
* launch thread" with a $3 budget, hit DALL-E twice for hero images, blow
|
|
15
|
+
* the budget, read the refusal, pick a cheaper model, and continue — all
|
|
16
|
+
* without human intervention and with the project surviving to the next
|
|
17
|
+
* session.
|
|
18
|
+
*/
|
|
19
|
+
import type { CapabilityHandler } from '../agent/types.js';
|
|
20
|
+
import type { ContentLibrary } from '../content/library.js';
|
|
21
|
+
export interface ContentCapabilitiesDeps {
|
|
22
|
+
library: ContentLibrary;
|
|
23
|
+
/** Invoked after mutating calls so callers can persist to disk. */
|
|
24
|
+
onStateChange?: () => void | Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
export declare function createContentCapabilities(deps: ContentCapabilitiesDeps): CapabilityHandler[];
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-facing Content capabilities. Four tools, deliberately mirroring the
|
|
3
|
+
* Trading vertical's shape:
|
|
4
|
+
*
|
|
5
|
+
* ContentCreate — start a new piece with type/title/budgetUsd
|
|
6
|
+
* ContentAddAsset — record a generated asset (image/audio/etc.) with its
|
|
7
|
+
* cost; rejected (as normal text, not an error) if it
|
|
8
|
+
* would breach the per-piece budget
|
|
9
|
+
* ContentShow — dump a single piece's state as actionable markdown
|
|
10
|
+
* ContentList — summary of every piece, newest first
|
|
11
|
+
*
|
|
12
|
+
* This is the surface Claude Code and Cursor cannot cover: durable content
|
|
13
|
+
* state with autonomous spending gating. An agent can spin up "Franklin
|
|
14
|
+
* launch thread" with a $3 budget, hit DALL-E twice for hero images, blow
|
|
15
|
+
* the budget, read the refusal, pick a cheaper model, and continue — all
|
|
16
|
+
* without human intervention and with the project surviving to the next
|
|
17
|
+
* session.
|
|
18
|
+
*/
|
|
19
|
+
const VALID_TYPES = [
|
|
20
|
+
'x-thread', 'blog', 'podcast', 'video', 'ad-copy', 'image',
|
|
21
|
+
];
|
|
22
|
+
const VALID_ASSET_KINDS = ['image', 'audio', 'video', 'text'];
|
|
23
|
+
function formatUsd(n) {
|
|
24
|
+
const sign = n < 0 ? '-' : '';
|
|
25
|
+
return `${sign}$${Math.abs(n).toFixed(2)}`;
|
|
26
|
+
}
|
|
27
|
+
function formatAssetLine(a) {
|
|
28
|
+
const when = new Date(a.createdAt).toISOString().replace('T', ' ').slice(0, 16);
|
|
29
|
+
const data = a.data ? ` · ${a.data}` : '';
|
|
30
|
+
return `- ${when} ${a.kind} via ${a.source} (${formatUsd(a.costUsd)})${data}`;
|
|
31
|
+
}
|
|
32
|
+
function formatContent(c) {
|
|
33
|
+
const lines = [];
|
|
34
|
+
lines.push(`## ${c.title}`);
|
|
35
|
+
lines.push(`- id: \`${c.id}\``);
|
|
36
|
+
lines.push(`- type: ${c.type} status: ${c.status}`);
|
|
37
|
+
lines.push(`- budget: ${formatUsd(c.spentUsd)} spent / ${formatUsd(c.budgetUsd)} cap`);
|
|
38
|
+
lines.push(`- drafts: ${c.drafts.length} assets: ${c.assets.length}`);
|
|
39
|
+
if (c.publishedAt) {
|
|
40
|
+
lines.push(`- published: ${new Date(c.publishedAt).toISOString()}`);
|
|
41
|
+
}
|
|
42
|
+
if (c.assets.length > 0) {
|
|
43
|
+
lines.push('');
|
|
44
|
+
lines.push('### Assets');
|
|
45
|
+
for (const a of c.assets)
|
|
46
|
+
lines.push(formatAssetLine(a));
|
|
47
|
+
}
|
|
48
|
+
return lines.join('\n');
|
|
49
|
+
}
|
|
50
|
+
export function createContentCapabilities(deps) {
|
|
51
|
+
const { library, onStateChange } = deps;
|
|
52
|
+
const contentCreate = {
|
|
53
|
+
spec: {
|
|
54
|
+
name: 'ContentCreate',
|
|
55
|
+
description: 'Start a new content piece with a budget. The piece is durable across ' +
|
|
56
|
+
'sessions. Use types: x-thread, blog, podcast, video, ad-copy, image. ' +
|
|
57
|
+
'Budget is a hard USD cap on asset spending for this piece.',
|
|
58
|
+
input_schema: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
required: ['type', 'title', 'budgetUsd'],
|
|
61
|
+
properties: {
|
|
62
|
+
type: { type: 'string', description: 'Content kind (x-thread, blog, podcast, video, ad-copy, image)' },
|
|
63
|
+
title: { type: 'string', description: 'Human-readable title' },
|
|
64
|
+
budgetUsd: { type: 'number', description: 'Hard USD spending cap for asset generation' },
|
|
65
|
+
},
|
|
66
|
+
additionalProperties: false,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
concurrent: false,
|
|
70
|
+
async execute(input, _ctx) {
|
|
71
|
+
const rawType = String(input.type ?? '');
|
|
72
|
+
const title = String(input.title ?? '').trim();
|
|
73
|
+
const budgetUsd = Number(input.budgetUsd);
|
|
74
|
+
if (!VALID_TYPES.includes(rawType)) {
|
|
75
|
+
return {
|
|
76
|
+
output: `Error: type must be one of ${VALID_TYPES.join(', ')}. Got "${rawType}".`,
|
|
77
|
+
isError: true,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (!title) {
|
|
81
|
+
return { output: 'Error: title is required.', isError: true };
|
|
82
|
+
}
|
|
83
|
+
if (!Number.isFinite(budgetUsd) || budgetUsd < 0) {
|
|
84
|
+
return { output: 'Error: budgetUsd must be a non-negative number.', isError: true };
|
|
85
|
+
}
|
|
86
|
+
const c = library.create({ type: rawType, title, budgetUsd });
|
|
87
|
+
if (onStateChange)
|
|
88
|
+
await onStateChange();
|
|
89
|
+
return {
|
|
90
|
+
output: `## Content created\n` +
|
|
91
|
+
`- id: \`${c.id}\`\n` +
|
|
92
|
+
`- title: ${c.title}\n` +
|
|
93
|
+
`- type: ${c.type}\n` +
|
|
94
|
+
`- budget: ${formatUsd(c.budgetUsd)} (spent ${formatUsd(c.spentUsd)})\n` +
|
|
95
|
+
`- status: ${c.status}\n\n` +
|
|
96
|
+
`Use this id with ContentAddAsset / ContentShow.`,
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
const contentAddAsset = {
|
|
101
|
+
spec: {
|
|
102
|
+
name: 'ContentAddAsset',
|
|
103
|
+
description: 'Record a generated asset (image, audio, video, or text) against a ' +
|
|
104
|
+
'content piece, debiting its budget. If the asset would exceed the ' +
|
|
105
|
+
'budget cap the call is refused and returned as a normal text result ' +
|
|
106
|
+
'— not an error — so the agent can read the refusal and try a cheaper ' +
|
|
107
|
+
'model. Cost must reflect what was actually spent (zero for free ' +
|
|
108
|
+
'models, positive for paid).',
|
|
109
|
+
input_schema: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
required: ['id', 'kind', 'source', 'costUsd'],
|
|
112
|
+
properties: {
|
|
113
|
+
id: { type: 'string', description: 'Content id returned by ContentCreate' },
|
|
114
|
+
kind: { type: 'string', description: 'image, audio, video, or text' },
|
|
115
|
+
source: { type: 'string', description: 'Generator model or "manual" (e.g. "openai/dall-e-3")' },
|
|
116
|
+
costUsd: { type: 'number', description: 'Actual USD spent producing this asset' },
|
|
117
|
+
data: { type: 'string', description: 'Optional URL or inline text reference to the asset' },
|
|
118
|
+
},
|
|
119
|
+
additionalProperties: false,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
concurrent: false,
|
|
123
|
+
async execute(input, _ctx) {
|
|
124
|
+
const id = String(input.id ?? '').trim();
|
|
125
|
+
const kind = String(input.kind ?? '');
|
|
126
|
+
const source = String(input.source ?? '').trim();
|
|
127
|
+
const costUsd = Number(input.costUsd);
|
|
128
|
+
const data = input.data == null ? undefined : String(input.data);
|
|
129
|
+
if (!id)
|
|
130
|
+
return { output: 'Error: id is required.', isError: true };
|
|
131
|
+
if (!VALID_ASSET_KINDS.includes(kind)) {
|
|
132
|
+
return {
|
|
133
|
+
output: `Error: kind must be one of ${VALID_ASSET_KINDS.join(', ')}. Got "${kind}".`,
|
|
134
|
+
isError: true,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (!source)
|
|
138
|
+
return { output: 'Error: source is required.', isError: true };
|
|
139
|
+
if (!Number.isFinite(costUsd) || costUsd < 0) {
|
|
140
|
+
return { output: 'Error: costUsd must be a non-negative number.', isError: true };
|
|
141
|
+
}
|
|
142
|
+
const decision = library.addAsset(id, { kind, source, costUsd, data });
|
|
143
|
+
if (!decision.ok) {
|
|
144
|
+
// Normal text: the agent should read this and adapt, not retry.
|
|
145
|
+
return { output: `## Asset rejected\n- ${decision.reason}` };
|
|
146
|
+
}
|
|
147
|
+
if (onStateChange)
|
|
148
|
+
await onStateChange();
|
|
149
|
+
const c = library.get(id);
|
|
150
|
+
return {
|
|
151
|
+
output: `## Asset recorded\n` +
|
|
152
|
+
`- ${kind} via ${source}: ${formatUsd(costUsd)}\n` +
|
|
153
|
+
`- Spent so far: ${formatUsd(c.spentUsd)} / ${formatUsd(c.budgetUsd)} cap\n` +
|
|
154
|
+
`- Remaining: ${formatUsd(c.budgetUsd - c.spentUsd)}`,
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
const contentShow = {
|
|
159
|
+
spec: {
|
|
160
|
+
name: 'ContentShow',
|
|
161
|
+
description: 'Dump a single content piece\'s full state as markdown: title, budget, ' +
|
|
162
|
+
'assets, drafts, distribution, status. Use before deciding the next step ' +
|
|
163
|
+
'on a piece (another asset, draft revision, publish).',
|
|
164
|
+
input_schema: {
|
|
165
|
+
type: 'object',
|
|
166
|
+
required: ['id'],
|
|
167
|
+
properties: {
|
|
168
|
+
id: { type: 'string', description: 'Content id' },
|
|
169
|
+
},
|
|
170
|
+
additionalProperties: false,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
concurrent: true,
|
|
174
|
+
async execute(input, _ctx) {
|
|
175
|
+
const id = String(input.id ?? '').trim();
|
|
176
|
+
if (!id)
|
|
177
|
+
return { output: 'Error: id is required.', isError: true };
|
|
178
|
+
const c = library.get(id);
|
|
179
|
+
if (!c)
|
|
180
|
+
return { output: `No content with id ${id}.` };
|
|
181
|
+
return { output: formatContent(c) };
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
const contentList = {
|
|
185
|
+
spec: {
|
|
186
|
+
name: 'ContentList',
|
|
187
|
+
description: 'List every content piece in the library (newest first) with its title, ' +
|
|
188
|
+
'status, and budget utilization. No inputs.',
|
|
189
|
+
input_schema: {
|
|
190
|
+
type: 'object',
|
|
191
|
+
properties: {},
|
|
192
|
+
additionalProperties: false,
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
concurrent: true,
|
|
196
|
+
async execute(_input, _ctx) {
|
|
197
|
+
const contents = library.list();
|
|
198
|
+
if (contents.length === 0) {
|
|
199
|
+
return { output: '_No content pieces yet. Use ContentCreate to start one._' };
|
|
200
|
+
}
|
|
201
|
+
const lines = ['## Content library', ''];
|
|
202
|
+
for (const c of contents) {
|
|
203
|
+
const pct = c.budgetUsd > 0 ? (c.spentUsd / c.budgetUsd) * 100 : 0;
|
|
204
|
+
lines.push(`- \`${c.id}\` · **${c.title}** · ${c.type}/${c.status} · ` +
|
|
205
|
+
`${formatUsd(c.spentUsd)} / ${formatUsd(c.budgetUsd)} (${pct.toFixed(0)}%) ` +
|
|
206
|
+
`· ${c.assets.length} assets`);
|
|
207
|
+
}
|
|
208
|
+
return { output: lines.join('\n') };
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
return [contentCreate, contentAddAsset, contentShow, contentList];
|
|
212
|
+
}
|