Skip to content

Commit 2ffbd59

Browse files
kochj23claude
andcommitted
feat(tools,context): Phase 1 tool calling + Phase 2 context management
Phase 1 - Reliable Tool Calling: - Add chat_generate command to Python daemon using tokenizer.apply_chat_template() with ChatML fallback for proper model-specific formatting - Send structured JSON message arrays instead of flat prompt strings - Classify 40+ tools into Core/Development/Media/Advanced tiers, only include 4-7 tools in system prompt instead of all 40+ - New compact JSON tool format: <tool>{"name":"..","args":{..}}</tool> with few-shot examples for model guidance - Tool approval flow: auto-approve read-only, show UI for write/execute - Inline ToolApprovalView with approve/deny buttons (Return/Escape shortcuts) - Restore maxTokens to 2048, maxResponseLength to 16000, maxResponseTokens to 4096 Phase 2 - Context Management: - ContextBudget system allocates tokens based on model's actual context window (reads max_position_embeddings from config.json with name heuristic fallback) - Word-based token estimation (~1.3 tok/word English, ~1.5 code) - Budget-aware assembly: system(fixed) + recent(70%) + project(20%) + summary(10%) - Rule-based compaction for dropped messages (instant, no LLM calls) - Auto-include project file tree and recent files within budget New files: ToolTier.swift, ToolApproval.swift, ToolApprovalView.swift, ContextBudget.swift Modified: 12 files across daemon, services, models, tools, viewmodels, views Version: 4.0.0 build 3 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent cdae5eb commit 2ffbd59

16 files changed

Lines changed: 1239 additions & 171 deletions

MLX Code.xcodeproj/project.pbxproj

Lines changed: 214 additions & 4 deletions
Large diffs are not rendered by default.

MLX Code/MLX_Code.entitlements

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,9 @@
1313
<string>com.apple.dt.Xcode</string>
1414
</array>
1515
</dict>
16+
<key>com.apple.security.application-groups</key>
17+
<array>
18+
<string>group.com.jkoch.mlxcode</string>
19+
</array>
1620
</dict>
1721
</plist>

MLX Code/Models/MLXModel.swift

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ struct MLXModel: Identifiable, Codable, Equatable, Hashable {
3535
/// Model description
3636
var description: String?
3737

38+
/// Maximum context window size in tokens (detected from model config)
39+
var contextWindowSize: Int?
40+
3841
/// Initializes a new MLX model configuration
3942
/// - Parameters:
4043
/// - id: Unique identifier (defaults to new UUID)
@@ -53,7 +56,8 @@ struct MLXModel: Identifiable, Codable, Equatable, Hashable {
5356
isDownloaded: Bool = false,
5457
sizeInBytes: Int64? = nil,
5558
huggingFaceId: String? = nil,
56-
description: String? = nil
59+
description: String? = nil,
60+
contextWindowSize: Int? = nil
5761
) {
5862
self.id = id
5963
self.name = name
@@ -63,6 +67,7 @@ struct MLXModel: Identifiable, Codable, Equatable, Hashable {
6367
self.sizeInBytes = sizeInBytes
6468
self.huggingFaceId = huggingFaceId
6569
self.description = description
70+
self.contextWindowSize = contextWindowSize
6671
}
6772
}
6873

@@ -96,11 +101,11 @@ struct ModelParameters: Codable, Equatable, Hashable {
96101
/// - repetitionContextSize: Context size for repetition (default: 64, look back farther)
97102
init(
98103
temperature: Double = 0.7,
99-
maxTokens: Int = 512, // Reduced from 2048 to prevent infinite generation
104+
maxTokens: Int = 2048, // Restored: chat templates + RepetitionDetector prevent loops
100105
topP: Double = 0.9,
101106
topK: Int = 40,
102-
repetitionPenalty: Double = 1.8, // Increased from 1.2 to heavily penalize repetition
103-
repetitionContextSize: Int = 256 // Increased from 64 to look back farther for repetition
107+
repetitionPenalty: Double = 1.8, // Keep high to penalize repetition
108+
repetitionContextSize: Int = 256 // Keep wide lookback for repetition detection
104109
) {
105110
self.temperature = temperature
106111
self.maxTokens = maxTokens
@@ -227,86 +232,95 @@ extension MLXModel {
227232
MLXModel(
228233
name: "Qwen 2.5 7B ⭐ RECOMMENDED",
229234
path: "\(modelBasePath)/qwen-2.5-7b",
230-
parameters: ModelParameters(temperature: 0.7, maxTokens: 512, topP: 0.9, topK: 40, repetitionPenalty: 1.8, repetitionContextSize: 256),
235+
parameters: ModelParameters(),
231236
isDownloaded: false,
232237
huggingFaceId: "mlx-community/Qwen2.5-7B-Instruct-4bit",
233-
description: "Alibaba's Qwen 2.5 - Excellent quality, good instruction following. Best for coding. ~4GB"
238+
description: "Alibaba's Qwen 2.5 - Excellent quality, good instruction following. Best for coding. ~4GB",
239+
contextWindowSize: 32768
234240
),
235241

236242
MLXModel(
237243
name: "Mistral 7B v0.3 ⭐ POPULAR",
238244
path: "\(modelBasePath)/mistral-7b",
239-
parameters: ModelParameters(temperature: 0.7, maxTokens: 512, topP: 0.9, topK: 40, repetitionPenalty: 1.8, repetitionContextSize: 256),
245+
parameters: ModelParameters(),
240246
isDownloaded: false,
241247
huggingFaceId: "mlx-community/Mistral-7B-Instruct-v0.3-4bit",
242-
description: "Mistral AI's 7B - Very popular, versatile, good for code. ~4GB"
248+
description: "Mistral AI's 7B - Very popular, versatile, good for code. ~4GB",
249+
contextWindowSize: 32768
243250
),
244251

245252
// Larger Models (Better Quality)
246253
MLXModel(
247254
name: "Qwen 2.5 14B (Best Quality)",
248255
path: "\(modelBasePath)/qwen-2.5-14b",
249-
parameters: ModelParameters(temperature: 0.7, maxTokens: 512, topP: 0.9, topK: 40, repetitionPenalty: 1.8, repetitionContextSize: 256),
256+
parameters: ModelParameters(),
250257
isDownloaded: false,
251258
huggingFaceId: "mlx-community/Qwen2.5-14B-Instruct-4bit",
252-
description: "Qwen 2.5 14B - Best quality, slower but excellent results. ~8GB"
259+
description: "Qwen 2.5 14B - Best quality, slower but excellent results. ~8GB",
260+
contextWindowSize: 32768
253261
),
254262

255263
MLXModel(
256264
name: "Llama 3.1 8B",
257265
path: "\(modelBasePath)/llama-3.1-8b",
258-
parameters: ModelParameters(temperature: 0.7, maxTokens: 512, topP: 0.9, topK: 40, repetitionPenalty: 1.8, repetitionContextSize: 256),
266+
parameters: ModelParameters(),
259267
isDownloaded: false,
260268
huggingFaceId: "mlx-community/Meta-Llama-3.1-8B-Instruct-4bit",
261-
description: "Meta's Llama 3.1 8B - Solid performance, good for general use. ~5GB"
269+
description: "Meta's Llama 3.1 8B - Solid performance, good for general use. ~5GB",
270+
contextWindowSize: 131072
262271
),
263272

264273
// Specialized Models
265274
MLXModel(
266275
name: "DeepSeek Coder 6.7B",
267276
path: "\(modelBasePath)/deepseek-coder",
268-
parameters: ModelParameters(temperature: 0.7, maxTokens: 512, topP: 0.9, topK: 40, repetitionPenalty: 1.8, repetitionContextSize: 256),
277+
parameters: ModelParameters(),
269278
isDownloaded: false,
270279
huggingFaceId: "mlx-community/deepseek-coder-6.7b-instruct",
271-
description: "DeepSeek Coder - Specialized for code, excellent for programming tasks. ~4GB"
280+
description: "DeepSeek Coder - Specialized for code, excellent for programming tasks. ~4GB",
281+
contextWindowSize: 16384
272282
),
273283

274284
MLXModel(
275285
name: "CodeLlama 7B",
276286
path: "\(modelBasePath)/codellama-7b",
277-
parameters: ModelParameters(temperature: 0.7, maxTokens: 512, topP: 0.9, topK: 40, repetitionPenalty: 1.8, repetitionContextSize: 256),
287+
parameters: ModelParameters(),
278288
isDownloaded: false,
279289
huggingFaceId: "mlx-community/CodeLlama-7b-Instruct-hf-4bit-MLX",
280-
description: "Meta's CodeLlama - Trained specifically for code generation. ~4GB"
290+
description: "Meta's CodeLlama - Trained specifically for code generation. ~4GB",
291+
contextWindowSize: 16384
281292
),
282293

283294
// Compact Models (Faster)
284295
MLXModel(
285296
name: "Phi-3.5 Mini (Fast)",
286297
path: "\(modelBasePath)/phi-3.5-mini",
287-
parameters: ModelParameters(temperature: 0.7, maxTokens: 512, topP: 0.9, topK: 40, repetitionPenalty: 1.8, repetitionContextSize: 256),
298+
parameters: ModelParameters(),
288299
isDownloaded: false,
289300
huggingFaceId: "mlx-community/Phi-3.5-mini-instruct-4bit",
290-
description: "Microsoft's Phi-3.5 - Small but capable, very fast. ~2GB"
301+
description: "Microsoft's Phi-3.5 - Small but capable, very fast. ~2GB",
302+
contextWindowSize: 4096
291303
),
292304

293305
MLXModel(
294306
name: "Gemma 2 9B",
295307
path: "\(modelBasePath)/gemma-2-9b",
296-
parameters: ModelParameters(temperature: 0.7, maxTokens: 512, topP: 0.9, topK: 40, repetitionPenalty: 1.8, repetitionContextSize: 256),
308+
parameters: ModelParameters(),
297309
isDownloaded: false,
298310
huggingFaceId: "mlx-community/gemma-2-9b-it-4bit",
299-
description: "Google's Gemma 2 9B - Excellent quality, good instruction following. ~5GB"
311+
description: "Google's Gemma 2 9B - Excellent quality, good instruction following. ~5GB",
312+
contextWindowSize: 8192
300313
),
301314

302315
// Keep original for backwards compatibility
303316
MLXModel(
304317
name: "Llama 3.2 3B (Small - NOT RECOMMENDED)",
305318
path: "\(modelBasePath)/llama-3.2-3b",
306-
parameters: ModelParameters(temperature: 0.7, maxTokens: 512, topP: 0.9, topK: 40, repetitionPenalty: 1.8, repetitionContextSize: 256),
319+
parameters: ModelParameters(),
307320
isDownloaded: false,
308321
huggingFaceId: "mlx-community/Llama-3.2-3B-Instruct-4bit",
309-
description: "Meta's Llama 3.2 3B - Too small, causes loops. Use Qwen or Mistral instead. ~2GB"
322+
description: "Meta's Llama 3.2 3B - Too small, causes loops. Use Qwen or Mistral instead. ~2GB",
323+
contextWindowSize: 8192
310324
)
311325
]
312326
}

MLX Code/Models/ToolApproval.swift

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//
2+
// ToolApproval.swift
3+
// MLX Code
4+
//
5+
// Tool approval types and policy for the agentic loop.
6+
// Created on 2026-02-19.
7+
//
8+
9+
import Foundation
10+
11+
/// Represents a pending tool call awaiting user approval
12+
struct PendingToolCall: Identifiable {
13+
let id = UUID()
14+
let toolName: String
15+
let parameters: [String: Any]
16+
let rawJSON: String
17+
var approved: Bool = false
18+
19+
/// Human-readable summary of the tool call
20+
var summary: String {
21+
if let path = parameters["path"] as? String {
22+
return "\(toolName)\(path)"
23+
} else if let command = parameters["command"] as? String {
24+
return "\(toolName)\(command.prefix(60))"
25+
} else if let pattern = parameters["pattern"] as? String {
26+
return "\(toolName)\(pattern)"
27+
}
28+
return toolName
29+
}
30+
}
31+
32+
/// Tool approval policy
33+
enum ToolApprovalPolicy: String, CaseIterable, Identifiable {
34+
/// Always ask before executing any tool
35+
case alwaysAsk = "always_ask"
36+
/// Auto-approve read-only tools (file read, grep, glob, code navigation)
37+
case autoApproveRead = "auto_approve_read"
38+
/// Auto-approve all tools without asking
39+
case autoApproveAll = "auto_approve_all"
40+
41+
var id: String { rawValue }
42+
43+
var displayName: String {
44+
switch self {
45+
case .alwaysAsk: return "Always Ask"
46+
case .autoApproveRead: return "Auto-approve Read-only"
47+
case .autoApproveAll: return "Auto-approve All"
48+
}
49+
}
50+
51+
var description: String {
52+
switch self {
53+
case .alwaysAsk:
54+
return "Ask before executing any tool"
55+
case .autoApproveRead:
56+
return "Auto-approve file reads, grep, glob. Ask for writes and commands."
57+
case .autoApproveAll:
58+
return "Execute all tools without asking"
59+
}
60+
}
61+
62+
/// Set of tool names considered read-only
63+
static let readOnlyTools: Set<String> = [
64+
"file_operations", // Only when operation=read
65+
"grep",
66+
"glob",
67+
"code_navigation",
68+
"workspace_analysis"
69+
]
70+
71+
/// Check if a tool call should be auto-approved under this policy
72+
func shouldAutoApprove(toolName: String, parameters: [String: Any]) -> Bool {
73+
switch self {
74+
case .alwaysAsk:
75+
return false
76+
case .autoApproveAll:
77+
return true
78+
case .autoApproveRead:
79+
// Special case: file_operations is only read-only for "read" operation
80+
if toolName == "file_operations" {
81+
let operation = parameters["operation"] as? String ?? ""
82+
return operation == "read" || operation == "list"
83+
}
84+
return Self.readOnlyTools.contains(toolName)
85+
}
86+
}
87+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
//
2+
// ContextBudget.swift
3+
// MLX Code
4+
//
5+
// Token budget allocation for context-window-aware message assembly.
6+
// Ensures system prompt, tools, recent messages, and project context
7+
// all fit within the model's context window.
8+
// Created on 2026-02-19.
9+
//
10+
11+
import Foundation
12+
13+
/// Manages token budget allocation across context components
14+
struct ContextBudget {
15+
/// Total token budget (model's context window)
16+
let totalBudget: Int
17+
18+
/// Fixed allocation for system prompt + tool descriptions
19+
let systemPromptBudget: Int
20+
21+
/// Fixed allocation for few-shot examples
22+
let fewShotBudget: Int
23+
24+
/// Reserved tokens for model's response output
25+
let outputReservation: Int
26+
27+
/// Tokens available for conversation content
28+
var conversationBudget: Int {
29+
max(0, totalBudget - systemPromptBudget - fewShotBudget - outputReservation)
30+
}
31+
32+
/// Budget for recent messages (70% of conversation budget)
33+
var recentMessagesBudget: Int {
34+
Int(Double(conversationBudget) * 0.7)
35+
}
36+
37+
/// Budget for project context — file tree, recent files (20%)
38+
var projectContextBudget: Int {
39+
Int(Double(conversationBudget) * 0.2)
40+
}
41+
42+
/// Budget for conversation summary of dropped messages (10%)
43+
var summaryBudget: Int {
44+
Int(Double(conversationBudget) * 0.1)
45+
}
46+
47+
/// Creates a context budget for the given model
48+
static func forModel(_ model: MLXModel?, daemonContextWindow: Int? = nil) -> ContextBudget {
49+
// Prefer daemon-reported context window, then model config, then name-based heuristic
50+
let contextWindow = daemonContextWindow
51+
?? model?.contextWindowSize
52+
?? detectContextWindow(modelName: model?.name ?? "")
53+
54+
return ContextBudget(
55+
totalBudget: contextWindow,
56+
systemPromptBudget: 500,
57+
fewShotBudget: 150,
58+
outputReservation: min(2048, contextWindow / 4)
59+
)
60+
}
61+
62+
/// Heuristic context window detection from model name
63+
private static func detectContextWindow(modelName: String) -> Int {
64+
let name = modelName.lowercased()
65+
if name.contains("qwen") { return 32768 }
66+
if name.contains("llama-3.1") || name.contains("llama 3.1") { return 131072 }
67+
if name.contains("llama-3.2") || name.contains("llama 3.2") { return 8192 }
68+
if name.contains("mistral") { return 32768 }
69+
if name.contains("phi") { return 4096 }
70+
if name.contains("deepseek") { return 16384 }
71+
if name.contains("codellama") { return 16384 }
72+
if name.contains("gemma") { return 8192 }
73+
return 8192 // Conservative default
74+
}
75+
}

0 commit comments

Comments
 (0)