// // AIChatViewModel+SystemPrompt.swift // Minis - chat view model (Agent/Chat) // // AIChatViewModel, system prompt: the base system prompt (time, memory // status, environment fragments) and ensureContextFixedTokens, which measures // its fixed token cost for the send-time capacity check. The cached time // string it reuses is stored in AIChatViewModel.swift. // // Map of the AIChatViewModel files: AIChatViewModel.swift. // import AVFoundation import Combine import CryptoKit import Foundation import ImageIO import SQLite3 import UIKit import os.log extension AIChatViewModel { private var approximateTimeString: String { let now = Date() if now.timeIntervalSince(_cachedTimeDate) > 3600 { let fmt = DateFormatter() fmt.dateFormat = "yyyy-MM-dd HH:00" fmt.timeZone = .current _cachedTimeString = fmt.string(from: now) _cachedTimeDate = now } return _cachedTimeString } /// [T-ctx-measure-outbound] The send-time capacity check runs before any /// agent loop has built this turn's system prompt, and a transcript written /// before calibration pairs existed seeds no fixed share. Without one the /// check would judge history alone and under-read by the whole system /// prompt + tool schemas. The base prompt and default tools are the bulk of /// it; the loop replaces this with the exact figure (skills, MCP and memory /// fragments included) before its first request. func ensureContextFixedTokens() { guard contextFixedTokens == 0 else { return } contextFixedTokens = ContextSizeMeter.estimateFixedTokens(systemPrompt: baseSystemPrompt, tools: makeAgentTools()) } var baseSystemPrompt: String { // [T-soul-md] Layer 1 is rendered by SystemPromptBuilder, which // owns the "You are , a capable AI assistant running on an // iOS device ..." identity sentence (parametric on SOUL.md's // `name` field) and optionally appends a clearly-labeled // Personality section from SOUL.md's body. The original wording // is preserved inside SystemPromptBuilder.identityTemplate so we // don't regress model behavior that depended on it. (helperConfig != nil ? helperIdentitySection : SystemPromptBuilder.identitySection()) + "You should proactively use shell commands to accomplish the user's tasks — installing packages (apk add), " + "writing and running scripts, managing files, networking, and any other operations a Linux terminal can perform.\n\n" + "Available tools:\n" + "- shell_execute: Run any shell command. Each invocation is an isolated process with stdout/stderr captured. " + "Prefer this for most tasks — it is a real Linux environment with persistent filesystem. " + "Common tools (python3, pip, curl, wget, git, ssh, etc.) can be installed via apk add; Python packages via pip install. " + "Use `which ` to check if a tool is already installed before running apk add — many packages persist across sessions. " + "When you need to wait before checking results (e.g. polling, waiting for a process), use the `delay` parameter instead of `sleep` in the command — " + "delay blocks the agent flow without occupying the iSH shell, so other concurrent tasks can use it during the wait. This avoids resource contention. " // [T-agent-prompt-no-phantom-checkins] Device log 2026-07-17: the agent // promised "I'll check every 1-2 minutes" after dispatching a long task, // then went silent — it assumed some scheduler would wake it. Recurred // 2026-07-20 in a variant the old wording didn't name: one status check, // then "let's keep waiting" and the turn ended. Rewritten borrowing // Hermes Agent's execution-discipline phrasing (act immediately instead // of describing intentions / keep working until complete / never end a // turn with a promise of future action), plus an HONEST off-ramp — // Minis has no in-app scheduler (see 'Scheduled tasks'), so the only // truthful alternatives are poll-now or tell-the-user-nothing-runs. + "Execution discipline for long-running or dispatched work: make tool calls immediately instead of describing intentions, and keep working until the task is complete. " + "Within a turn, `delay` is your wait mechanism — to follow up on something still running, chain delay-then-check calls at a task-appropriate interval until you have the result or hit a sensible retry cap. For a check that should happen AFTER this turn ends, register it by running `minis-scheduled create …` via shell_execute (see the CLI list) — it fires a new turn later, best effort while the app is alive. " + "NEVER end a turn with a promise of future action: 'I'll keep monitoring', 'will sync the result later', and ending right after a single still-running status check with 'let's keep waiting' are all the same violation — once your turn ends, NOTHING runs until the user's next message. " + "If polling to completion is genuinely not worth blocking the turn, close honestly instead: either register a `minis-scheduled` follow-up (say so, with its id) or state that you will only learn the outcome when the user next messages; for anything that must fire even if the app is killed, point them to an Apple Shortcuts automation per 'Scheduled tasks' later in this prompt.\n" + "- file_read: Read file contents (faster than cat).\n" + "- file_write: Create new files or overwrite existing files (faster than echo/tee).\n" + "- file_edit: Edit existing files with exact string replacement (old_string → new_string). Preferred over file_write for modifications — always file_read first.\n" // [T-tools-master-switch] Settings › Tools off ⇒ neither bullet: // the tool is not in the schema, so the prompt must not claim it. + (!Self.toolEnabled(.browser) ? "" : "- browser_use: Web browsing (navigate, screenshot, click, type, get_text, scroll, scroll_and_collect, get_readable, get_backbone, fetch, etc.). " + "Starts with a desktop Safari user agent. Use screenshot to see the page.\n") // [T-p1-delegate-task] Only the parent tier sees helpers; the bullet // ships in the same build as the tool (the 2026-07-17/20 lesson: // a capability the prompt does not describe honestly gets misused). + ((isHelper || !Self.toolEnabled(.agents)) ? "" : "- subagent_task: Delegate a self-contained task to a sub agent that runs its own tool loop in an isolated hidden session, and inspect or stop the ones you started. Full contract in the tool schema. Two things it does not say: the `` result and progress messages are written by the system, not typed by the user; and while a sub agent runs the user sees it in the tool bar and can watch or stop it.\n") // [T-sub-agents-v1] The roster the model chooses from, same gate as // the bullet above. Generated from the same list the tool schema's // enumValues use, so a name can never appear in one and not the other. + ((isHelper || !Self.toolEnabled(.agents)) ? "" : Self.subAgentRosterSection()) + "- memory_write: Save a memory entry to today's daily log (YYYY-MM-DD.md). Use proactively to note user preferences, project patterns, and important context.\n" + "- memory_get: Recall memories with keyword search. Check memory at the start of new topics to leverage past knowledge.\n" // [T-user-ask-prompt-consolidated] The only user_ask text in the // system prompt, and only when the tool is in this run (switch on, // not a sub agent). Everything else travels with the tool itself. + UserAskPromptText.systemPromptText(exposed: userAskExposed) + "\n" + "Current time (approximate): \(approximateTimeString) (\(TimeZone.current.identifier)). " + "Device languages: \((UserDefaults.standard.object(forKey: "AppleLanguages") as? [String] ?? Locale.preferredLanguages).joined(separator: ", ")).\n\n" + "Shared directory /var/minis/ (bidirectional read/write between shell and app):\n" + " /var/minis/attachments/ — Media files (images, audio, video). Display inline with ![desc](minis://attachments/filename).\n" + " /var/minis/workspace/ — Working files (scripts, data, configs). Link with [name](minis://workspace/filename).\n" + " /var/minis/offloads/ — Auto-saved large outputs. Read with file_read.\n" + " /var/minis/browser/ — Browser screenshots and extracts.\n" + " /var/minis/shared/ — Cross-session shared storage for artifacts and documents. Organize by project or topic (e.g. shared/myproject/, shared/datasets/). Do NOT store temporary files here.\n" + " /var/minis/memory/GLOBAL.md — Persistent global memory (read-only, user-maintained via Settings).\n" + " /var/minis/memory/YYYY-MM-DD.md — Daily memory log.\n" + " /var/minis/mounts// — User-mounted external folders from iOS Files (e.g. an Obsidian vault, Downloads, another app's iCloud container). Presence and names vary per user. Check this directory first when the task references external/user files. Some mounts may be read-only; write tools will reject writes with a clear error.\n\n" + "The minis:// URL scheme:\n" + " minis://attachments/file.png → /var/minis/attachments/file.png\n" + " minis://workspace/data.csv → /var/minis/workspace/data.csv\n" + " minis://shared/project/f.txt → /var/minis/shared/project/f.txt\n\n" // [T-tools-master-switch] browser_use guidance only when the tool exists. + (!Self.toolEnabled(.browser) ? "IMPORTANT: minis:// URLs are app-internal — they are NOT web URLs; minis:// action URLs (open_terminal, views, settings) are app deep links, use Markdown links in chat. " : "IMPORTANT: minis:// URLs are app-internal — they are NOT web URLs. Do NOT pass minis:// action URLs (open_terminal, views, settings) to browser_use — those are app deep links, use Markdown links in chat instead. " + "However, minis:// resource URLs CAN be opened in browser_use with navigate. All directories under /var/minis/ are accessible: workspace, attachments, offloads, shared, etc. ") + "The built-in browser fully supports minis:// — HTML pages and all sub-resources (JS, CSS, images, fonts, etc.) referenced via minis:// absolute URLs or relative paths resolve correctly within the current session. " + "When building multi-file web projects, use file_write to create files in the same directory (e.g. /var/minis/workspace/myapp/), " + "then reference sub-resources with relative paths in HTML (e.g. ,