@blockrun/franklin 3.6.21 → 3.6.23

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.
@@ -6,10 +6,10 @@
6
6
  * - Auth errors (401) get special handling (token refresh, not retry)
7
7
  * - EPIPE/connection reset handled as network errors (retryable)
8
8
  */
9
- export type AgentErrorCategory = 'rate_limit' | 'payment' | 'network' | 'timeout' | 'context_limit' | 'overloaded' | 'server' | 'auth' | 'unknown';
9
+ export type AgentErrorCategory = 'rate_limit' | 'payment' | 'network' | 'timeout' | 'context_limit' | 'overloaded' | 'server' | 'auth' | 'schema' | 'unknown';
10
10
  export interface AgentErrorInfo {
11
11
  category: AgentErrorCategory;
12
- label: 'RateLimit' | 'Payment' | 'Network' | 'Timeout' | 'Context' | 'Overloaded' | 'Server' | 'Auth' | 'Unknown';
12
+ label: 'RateLimit' | 'Payment' | 'Network' | 'Timeout' | 'Context' | 'Overloaded' | 'Server' | 'Auth' | 'Schema' | 'Unknown';
13
13
  isTransient: boolean;
14
14
  /** Max retries for this error type (overrides default). undefined = use default. */
15
15
  maxRetries?: number;
@@ -103,6 +103,24 @@ export function classifyAgentError(message) {
103
103
  suggestion: 'The model is overloaded. Try /model to switch, or wait and /retry.',
104
104
  };
105
105
  }
106
+ // Schema / tool-definition errors — NOT transient, retrying won't help.
107
+ // These can be wrapped in 5xx responses (e.g. '503: 400 Invalid schema'),
108
+ // so classify them BEFORE the generic server-error branch below.
109
+ if (includesAny(err, [
110
+ 'invalid schema',
111
+ 'array schema missing items',
112
+ 'schema missing',
113
+ 'invalid tool_use',
114
+ 'invalid function',
115
+ 'tool_use_id',
116
+ 'unsupported parameter',
117
+ 'invalid request',
118
+ ])) {
119
+ return {
120
+ category: 'schema', label: 'Schema', isTransient: false, maxRetries: 0,
121
+ suggestion: 'Tool schema rejected by this model. Try /model to switch to a more permissive model (e.g. sonnet), or upgrade Franklin.',
122
+ };
123
+ }
106
124
  if (includesAny(err, [
107
125
  '500',
108
126
  '502',
@@ -7,6 +7,40 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
7
7
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
8
8
  // ─── Connection Management ────────────────────────────────────────────────
9
9
  const connections = new Map();
10
+ /**
11
+ * Sanitize a JSON schema for strict LLM providers (OpenAI o3, etc.).
12
+ * Walks the schema tree and adds `items` to any array missing it.
13
+ * Without this, models like o3 reject the tool with:
14
+ * "Invalid schema: In context=(...), array schema missing items."
15
+ */
16
+ function sanitizeSchema(schema) {
17
+ if (!schema || typeof schema !== 'object') {
18
+ return { type: 'object', properties: {} };
19
+ }
20
+ const s = schema;
21
+ // If array type without items, add a permissive default
22
+ if (s.type === 'array' && !s.items) {
23
+ s.items = {};
24
+ }
25
+ // Recurse into properties
26
+ if (s.properties && typeof s.properties === 'object') {
27
+ const props = s.properties;
28
+ for (const key of Object.keys(props)) {
29
+ props[key] = sanitizeSchema(props[key]);
30
+ }
31
+ }
32
+ // Recurse into items (nested arrays)
33
+ if (s.items && typeof s.items === 'object') {
34
+ s.items = sanitizeSchema(s.items);
35
+ }
36
+ // Recurse into anyOf / oneOf / allOf
37
+ for (const key of ['anyOf', 'oneOf', 'allOf']) {
38
+ if (Array.isArray(s[key])) {
39
+ s[key] = s[key].map(sanitizeSchema);
40
+ }
41
+ }
42
+ return s;
43
+ }
10
44
  /**
11
45
  * Connect to an MCP server via stdio transport.
12
46
  * Discovers tools and returns them as CapabilityHandlers.
@@ -47,10 +81,7 @@ async function connectStdio(name, config) {
47
81
  spec: {
48
82
  name: toolName,
49
83
  description: toolDescription || `MCP tool from ${name}`,
50
- input_schema: tool.inputSchema || {
51
- type: 'object',
52
- properties: {},
53
- },
84
+ input_schema: sanitizeSchema(tool.inputSchema),
54
85
  },
55
86
  execute: async (input, _ctx) => {
56
87
  const MCP_TOOL_TIMEOUT = 30_000;
@@ -130,10 +130,12 @@ export const taskCapability = {
130
130
  },
131
131
  addBlocks: {
132
132
  type: 'array',
133
+ items: { type: 'number' },
133
134
  description: 'Task IDs that cannot start until this task completes (for update)',
134
135
  },
135
136
  addBlockedBy: {
136
137
  type: 'array',
138
+ items: { type: 'number' },
137
139
  description: 'Task IDs that must complete before this task can start (for update)',
138
140
  },
139
141
  },
package/dist/ui/app.js CHANGED
@@ -657,7 +657,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
657
657
  const elapsed = Math.round((Date.now() - tool.startTime) / 1000);
658
658
  const elapsedStr = elapsed > 0 ? ` ${elapsed}s` : '';
659
659
  return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), ' ', _jsx(Text, { bold: true, color: "cyan", children: tool.name }), tool.preview ? _jsxs(Text, { dimColor: true, children: ["(", tool.preview.slice(0, 70), ")"] }) : null, _jsx(Text, { dimColor: true, children: elapsedStr })] }), tool.liveLines.length > 0 && (_jsx(Box, { flexDirection: "column", marginLeft: 2, children: tool.liveLines.map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ['⎿ ', line.slice(0, 120)] }, i))) }))] }, id));
660
- }), thinking && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { color: "magenta", children: [_jsx(Spinner, { type: "dots" }), ' ', _jsx(Text, { bold: true, children: "thinking" }), completedTools.length > 0 ? _jsxs(Text, { dimColor: true, children: [' ', "\u00B7 step ", completedTools.length + 1] }) : null] }), thinkingText && (() => {
660
+ }), thinking && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { color: "magenta", children: [_jsx(Spinner, { type: "dots" }), ' ', _jsx(Text, { bold: true, children: "thinking" }), completedTools.length > 0 ? _jsxs(Text, { dimColor: true, children: [' ', "\u00B7 step ", completedTools.length + 1] }) : null] }), process.env.FRANKLIN_SHOW_THINKING === '1' && thinkingText && (() => {
661
661
  const lines = thinkingText.split('\n').filter(Boolean).slice(-3);
662
662
  return (_jsx(Box, { flexDirection: "column", marginLeft: 2, children: lines.map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ['⎿ ', line.slice(0, 120)] }, i))) }));
663
663
  })()] })), waiting && !thinking && tools.size === 0 && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), ' ', _jsxs(Text, { dimColor: true, children: [shortModelName(currentModel), completedTools.length > 0 ? ` · step ${completedTools.length + 1}` : ''] })] }) })), streamText && (_jsx(Box, { marginTop: 0, marginBottom: 0, children: _jsx(Text, { wrap: "wrap", children: renderMarkdown(streamText) }) })), responsePreview && !streamText && (_jsx(Box, { flexDirection: "column", marginBottom: 0, children: _jsx(Text, { wrap: "wrap", children: renderMarkdown(responsePreview) }) })), inPicker && (() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.6.21",
3
+ "version": "3.6.23",
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": {