@blockrun/franklin 3.15.58 → 3.15.60

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.
@@ -20,6 +20,15 @@ export declare function looksLikeGatewayErrorAsText(parts: ContentPart[]): {
20
20
  match: boolean;
21
21
  message: string;
22
22
  };
23
+ /**
24
+ * Walk a Dialogue and replace large `image.source.data` (base64) blocks
25
+ * inside `tool_result.content` arrays with a tiny placeholder. The
26
+ * accompanying text block already names the file path so the model on
27
+ * resume can re-Read it if it needs to see the image again. Returns a
28
+ * shallow clone so the in-memory history (used for the rest of the
29
+ * current turn) keeps the full image data.
30
+ */
31
+ export declare function stripLargeImageData(message: Dialogue): Dialogue;
23
32
  /**
24
33
  * Identify models known to hallucinate tool calls (invented names, literal
25
34
  * `[TOOLCALL]` / `<tool_call>` text in answers) — they need the explicit
@@ -288,6 +288,64 @@ function getBackoffDelay(attempt, maxDelayMs = 32_000) {
288
288
  const jitter = base * 0.25 * (Math.random() * 2 - 1); // ±25%
289
289
  return Math.max(500, Math.round(base + jitter));
290
290
  }
291
+ /**
292
+ * Threshold for stripping inline base64 image data on session-disk
293
+ * writes. Mirrors `streaming-executor.ts:PERSIST_THRESHOLD` so a Read of
294
+ * a small icon (favicon-sized PNG, ~3 KB base64) round-trips through
295
+ * resume intact, while a Read of a screenshot or generated artwork
296
+ * (typically 200 KB+ base64) gets path-stubbed.
297
+ */
298
+ const SESSION_IMAGE_STRIP_THRESHOLD = 50_000;
299
+ /**
300
+ * Walk a Dialogue and replace large `image.source.data` (base64) blocks
301
+ * inside `tool_result.content` arrays with a tiny placeholder. The
302
+ * accompanying text block already names the file path so the model on
303
+ * resume can re-Read it if it needs to see the image again. Returns a
304
+ * shallow clone so the in-memory history (used for the rest of the
305
+ * current turn) keeps the full image data.
306
+ */
307
+ export function stripLargeImageData(message) {
308
+ if (!Array.isArray(message.content))
309
+ return message;
310
+ let mutated = false;
311
+ // Cast through `unknown` because Dialogue's content union doesn't expose
312
+ // the tool_result shape with image blocks at the type level — they flow
313
+ // in via the loop's outcome-building path. Runtime structure is what
314
+ // matters here; we only mutate when we positively identify the shape.
315
+ const newContent = message.content.map((part) => {
316
+ if (typeof part === 'object' &&
317
+ part !== null &&
318
+ part.type === 'tool_result' &&
319
+ Array.isArray(part.content)) {
320
+ const tr = part;
321
+ let inner = tr.content;
322
+ let innerMutated = false;
323
+ const cleaned = inner.map((block) => {
324
+ if (block &&
325
+ typeof block === 'object' &&
326
+ block.type === 'image' &&
327
+ block.source?.type === 'base64' &&
328
+ (block.source.data?.length ?? 0) > SESSION_IMAGE_STRIP_THRESHOLD) {
329
+ innerMutated = true;
330
+ const sz = (block.source.data ?? '').length;
331
+ return {
332
+ type: 'text',
333
+ text: `<image stripped from session log: ${(sz / 1024).toFixed(1)} KB base64. ` +
334
+ `See accompanying text block for the source path; re-Read to inline again.>`,
335
+ };
336
+ }
337
+ return block;
338
+ });
339
+ if (innerMutated) {
340
+ mutated = true;
341
+ inner = cleaned;
342
+ return { ...tr, content: inner };
343
+ }
344
+ }
345
+ return part;
346
+ });
347
+ return mutated ? { ...message, content: newContent } : message;
348
+ }
291
349
  /**
292
350
  * Format the user-facing "switching model" line. Includes the resolved
293
351
  * concrete model in parentheses when the user-facing alias (e.g.
@@ -483,7 +541,16 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
483
541
  });
484
542
  };
485
543
  const persistSessionMessage = (message) => {
486
- appendToSession(sessionId, message);
544
+ // Strip large base64 image bytes before writing to session jsonl. The
545
+ // tool_result wrap at line ~1788 inlines image data so vision models
546
+ // can see it during the live turn — but PNG bytes can be ~600 KB
547
+ // each, and the inline content bypasses persistLargeResult (which
548
+ // only checks `result.output.length`). Verified 2026-05-05: a single
549
+ // Read of `/tmp/mamba_hd_p9.png` produced an 850 KB session jsonl
550
+ // line; a 5-turn session with multiple .png reads grew to 12 MB.
551
+ // The model already saw the bytes in this turn's in-memory history,
552
+ // so disk only needs the path reference for resume.
553
+ appendToSession(sessionId, stripLargeImageData(message));
487
554
  persistSessionMeta();
488
555
  };
489
556
  pruneOldSessions(sessionId); // Cleanup old sessions on start, protect current
@@ -44,7 +44,7 @@ function estimateVideoCostUsd(durationSeconds = DEFAULT_DURATION) {
44
44
  function buildExecute(deps) {
45
45
  return async function execute(input, ctx) {
46
46
  const rawInput = input;
47
- const { output_path, model, image_url, duration_seconds, contentId } = rawInput;
47
+ const { output_path, model, image_url, duration_seconds, contentId, aspect_ratio } = rawInput;
48
48
  if (!rawInput.prompt)
49
49
  return { output: 'Error: prompt is required', isError: true };
50
50
  // Resolve image_url before sending. The gateway requires a URL (http(s)
@@ -156,6 +156,12 @@ function buildExecute(deps) {
156
156
  prompt: chosenPrompt,
157
157
  ...(resolvedImageUrl ? { image_url: resolvedImageUrl } : {}),
158
158
  ...(duration_seconds ? { duration_seconds } : {}),
159
+ // aspect_ratio passes through to the gateway. Models that support it
160
+ // (newer Seedance / grok variants) honor it; models that ignore it
161
+ // produce their default size. If the gateway rejects an unknown
162
+ // value, the 400 body surfaces via 3.15.45 diagnostic so the agent
163
+ // can drop the param and retry.
164
+ ...(aspect_ratio ? { aspect_ratio } : {}),
159
165
  });
160
166
  const headers = {
161
167
  'Content-Type': 'application/json',
@@ -456,7 +462,15 @@ export function createVideoGenCapability(deps = {}) {
456
462
  "xai/grok-imagine-video bills $0.05/s (8s default ≈ $0.42). Generation " +
457
463
  "takes ~20–60s. ALWAYS confirm with the user before calling — videos " +
458
464
  "are expensive and slow. Pass contentId to attach to a Content piece " +
459
- "(budget is checked before paying; asset is recorded on success).",
465
+ "(budget is checked before paying; asset is recorded on success). " +
466
+ "PLATFORM TARGETING: when the user says they'll post to X / Twitter, " +
467
+ "set aspect_ratio: '16:9' AND plan a follow-up `ffmpeg -vf scale=1280:720` " +
468
+ "step — X rejects videos under 720p with 'aspect ratio too small'. " +
469
+ "TikTok / Reels / Shorts: aspect_ratio '9:16'. Instagram Square: '1:1'. " +
470
+ "MODERATION: bytedance/seedance-* refuses photorealistic human faces " +
471
+ "(`InputImageSensitiveContentDetected.PrivacyInformation`); when the " +
472
+ "seed image has a real-looking person, use xai/grok-imagine-video " +
473
+ "instead, or regenerate the keyframe in a more stylized style first.",
460
474
  input_schema: {
461
475
  type: 'object',
462
476
  properties: {
@@ -471,6 +485,15 @@ export function createVideoGenCapability(deps = {}) {
471
485
  },
472
486
  image_url: { type: 'string', description: 'Optional seed image (image-to-video). Accepts http(s) URL, data: URI, or local file path — local paths get inlined as base64 data URIs automatically.' },
473
487
  duration_seconds: { type: 'number', description: 'Duration billed for. Default depends on model (8s for grok-imagine-video).' },
488
+ aspect_ratio: {
489
+ type: 'string',
490
+ description: 'Optional aspect ratio hint passed to the model. Common values: ' +
491
+ '"16:9" (landscape — X/Twitter, YouTube, TikTok-landscape), ' +
492
+ '"9:16" (vertical — TikTok, Reels, Shorts), ' +
493
+ '"1:1" (square — Instagram feed). Models that don\'t support the ' +
494
+ 'param ignore it; if the gateway 400s on an unknown value, the ' +
495
+ 'error body surfaces — drop the param and retry.',
496
+ },
474
497
  contentId: { type: 'string', description: 'Optional Content id to attach and budget against.' },
475
498
  },
476
499
  required: ['prompt'],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.15.58",
3
+ "version": "3.15.60",
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": {