flint-agent 1.14.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/.env.example +108 -0
- package/CHANGELOG.md +55 -0
- package/FEATURES.md +298 -0
- package/LICENSE +21 -0
- package/README.md +435 -0
- package/bin/flint.js +47 -0
- package/config/classifier-prompt.md +218 -0
- package/config/models-curated.json +4 -0
- package/config/providers.json +74 -0
- package/package.json +92 -0
- package/patches/ink+6.8.0.patch +78 -0
- package/profiles/desktop.md +65 -0
- package/profiles/generic.md +20 -0
- package/profiles/marketer.md +20 -0
- package/profiles/profiles.json +34 -0
- package/profiles/ux-reviewer.md +25 -0
- package/src/agent/agent.js +1743 -0
- package/src/agent/auto.js +346 -0
- package/src/agent/backoff.js +143 -0
- package/src/agent/compression.js +310 -0
- package/src/agent/content-resolver.js +180 -0
- package/src/agent/flow-controller.js +309 -0
- package/src/agent/intent-manifest.js +231 -0
- package/src/agent/intent-timeout.js +46 -0
- package/src/agent/intent.js +633 -0
- package/src/agent/knowledge.js +114 -0
- package/src/agent/learning.js +180 -0
- package/src/agent/modes.js +187 -0
- package/src/agent/outcome-ask.js +91 -0
- package/src/agent/project-context.js +76 -0
- package/src/agent/prompt-budget.js +117 -0
- package/src/agent/reflection-extractor.js +140 -0
- package/src/agent/steering.js +86 -0
- package/src/agent/supervisor.js +430 -0
- package/src/agent/swap.js +443 -0
- package/src/agent/system-prompt.js +446 -0
- package/src/agent/time-stamp.js +48 -0
- package/src/agent/tool-guard.js +201 -0
- package/src/agent/toolcall-text.js +162 -0
- package/src/agent/usage.js +297 -0
- package/src/agent/vision.js +94 -0
- package/src/agent/watchdog.js +139 -0
- package/src/agent/workspace-changes.js +177 -0
- package/src/api/address.js +14 -0
- package/src/api/client.js +280 -0
- package/src/api/server.js +535 -0
- package/src/api/stream-pipe.js +113 -0
- package/src/app-state.js +39 -0
- package/src/bootstrap.js +501 -0
- package/src/bus/drain-loop.js +497 -0
- package/src/bus/index.js +270 -0
- package/src/bus/plugins.js +65 -0
- package/src/child-idle.js +14 -0
- package/src/cli.js +118 -0
- package/src/commands/commands.js +1297 -0
- package/src/commands/registry.js +132 -0
- package/src/components/App.js +491 -0
- package/src/components/CarefulMenu.js +145 -0
- package/src/components/HistoryWriter.js +86 -0
- package/src/components/LineInput.js +69 -0
- package/src/components/LiveZone.js +294 -0
- package/src/components/OverlayMenu.js +179 -0
- package/src/components/SystemPanel.js +156 -0
- package/src/components/Table.js +54 -0
- package/src/config.js +249 -0
- package/src/free-models.js +230 -0
- package/src/index.js +1111 -0
- package/src/input-handler.js +13 -0
- package/src/input-text.js +123 -0
- package/src/launcher.js +129 -0
- package/src/logging/api-log.js +95 -0
- package/src/logging/chat-log-follower.js +113 -0
- package/src/logging/chat-log.js +15 -0
- package/src/logging/log-collector.js +182 -0
- package/src/logging/logger.js +112 -0
- package/src/logging/tool-log.js +20 -0
- package/src/mcp-client.js +314 -0
- package/src/memory/conversation-digest.js +113 -0
- package/src/memory/extract-facts.js +98 -0
- package/src/memory/facts.js +181 -0
- package/src/memory/inbox.js +63 -0
- package/src/memory/markdown.js +38 -0
- package/src/memory/patterns.js +185 -0
- package/src/memory/project.js +66 -0
- package/src/memory/reflections.js +74 -0
- package/src/memory/retrieval.js +84 -0
- package/src/memory/rules.js +105 -0
- package/src/memory/session-facts.js +125 -0
- package/src/memory/skills.js +191 -0
- package/src/memory/sqlite-store.js +653 -0
- package/src/memory/store.js +208 -0
- package/src/memory/tools.js +196 -0
- package/src/memory/user-model.js +86 -0
- package/src/message-handler.js +775 -0
- package/src/model-check.js +218 -0
- package/src/plugins/loader.js +120 -0
- package/src/plugins/manager.js +88 -0
- package/src/production-env.js +22 -0
- package/src/profiles.js +42 -0
- package/src/providers/adapters/anthropic.js +270 -0
- package/src/providers/adapters/openai.js +120 -0
- package/src/providers/keys-dpapi.js +41 -0
- package/src/providers/keys-fallback.js +31 -0
- package/src/providers/keys.js +132 -0
- package/src/providers/models.js +154 -0
- package/src/providers/registry.js +56 -0
- package/src/providers/state.js +56 -0
- package/src/registry.js +96 -0
- package/src/restart.js +29 -0
- package/src/sandbox/backend.js +130 -0
- package/src/security/api-auth.js +132 -0
- package/src/security/audit.js +98 -0
- package/src/security/child-policy.js +41 -0
- package/src/security/command-guard.js +173 -0
- package/src/security/content-fence.js +250 -0
- package/src/security/content-validator.js +132 -0
- package/src/security/index.js +143 -0
- package/src/security/network-guard.js +126 -0
- package/src/security/pairing.js +180 -0
- package/src/security/path-guard.js +140 -0
- package/src/security/persona-guard.js +67 -0
- package/src/security/policies.js +452 -0
- package/src/security/safety-constants.js +34 -0
- package/src/security/watchdog.js +107 -0
- package/src/sessions.js +130 -0
- package/src/spend.js +97 -0
- package/src/startup-watchdog.js +59 -0
- package/src/stdio/args.js +71 -0
- package/src/stdio/guard.js +59 -0
- package/src/stdio/protocol.js +167 -0
- package/src/stdio/run.js +106 -0
- package/src/stdio/session.js +180 -0
- package/src/store/agent-slice.js +306 -0
- package/src/store/dataset-slice.js +73 -0
- package/src/store/index.js +22 -0
- package/src/store/process-slice.js +135 -0
- package/src/store/session-slice.js +191 -0
- package/src/store/ui-slice.js +119 -0
- package/src/tasks/db.js +184 -0
- package/src/tasks/queries.js +589 -0
- package/src/tools/agent-tools.js +473 -0
- package/src/tools/checkpoint.js +152 -0
- package/src/tools/command-approvals.js +180 -0
- package/src/tools/dataset.js +50 -0
- package/src/tools/filesystem.js +682 -0
- package/src/tools/inbox-tools.js +48 -0
- package/src/tools/mesh.js +135 -0
- package/src/tools/own-env.js +136 -0
- package/src/tools/permissions.js +681 -0
- package/src/tools/plugin-tools.js +123 -0
- package/src/tools/process-tools.js +595 -0
- package/src/tools/registry.js +307 -0
- package/src/tools/swap-tools.js +72 -0
- package/src/tools/system.js +662 -0
- package/src/tools/tasks.js +532 -0
- package/src/tools/tool-search.js +171 -0
- package/src/ui/header.js +140 -0
- package/src/ui/input-cursor.js +23 -0
- package/src/ui/last-line.js +25 -0
- package/src/ui/line-edit.js +135 -0
- package/src/ui/output.js +399 -0
- package/src/ui/paste-tokens.js +131 -0
- package/src/ui/prompt-attention.js +134 -0
- package/src/ui/render-options.js +13 -0
- package/src/ui/replay.js +94 -0
- package/src/ui/splash.js +49 -0
- package/src/ui/status-level.js +36 -0
- package/src/ui/tool-ledger.js +203 -0
- package/src/ui/window-title.js +150 -0
- package/src/update.js +205 -0
- package/system.md +63 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import { swapEnabled } from "./swap.js";
|
|
2
|
+
import { getSpendLevel, spendSettings } from "../spend.js";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
|
|
5
|
+
const ECONOMY_ADVICE = [
|
|
6
|
+
"## Spending tokens (economy mode)",
|
|
7
|
+
"",
|
|
8
|
+
"Every call sends the whole conversation again, so keep it short:",
|
|
9
|
+
"- Search before reading (`search_in_files`, `glob`), and read the part of a file you need (offset/limit), not all of it.",
|
|
10
|
+
"- Read a page once and note what matters in your reply; do not fetch it again (`swap_read` brings back what was read).",
|
|
11
|
+
"- Make independent tool calls in one response.",
|
|
12
|
+
"- Answer briefly; no recaps of what you already said.",
|
|
13
|
+
].join("\n");
|
|
14
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { readMemoryMdHead } from "../memory/markdown.js";
|
|
17
|
+
import { getSessionFactsSummary } from "../memory/session-facts.js";
|
|
18
|
+
import { getDigestForPrompt } from "../memory/conversation-digest.js";
|
|
19
|
+
import { loadRecent as loadRecentReflections, formatForPrompt as formatReflections } from "../memory/reflections.js";
|
|
20
|
+
import { compilePreferences, formatForPrompt as formatPatterns } from "../memory/patterns.js";
|
|
21
|
+
import { createBudgetAllocator } from "./prompt-budget.js";
|
|
22
|
+
import { formatForPrompt as formatSkills, syncFromDisk as syncSkills } from "../memory/skills.js";
|
|
23
|
+
import { formatForPrompt as formatFacts } from "../memory/facts.js";
|
|
24
|
+
import { getCurrentProject } from "../memory/project.js";
|
|
25
|
+
import { formatForPrompt as formatUserModel } from "../memory/user-model.js";
|
|
26
|
+
import { compileRules, formatForPrompt as formatRules } from "../memory/rules.js";
|
|
27
|
+
import { getDefinitions } from "../tools/registry.js";
|
|
28
|
+
import { detectInjection } from "../security/content-fence.js";
|
|
29
|
+
import { formatFlintMdBlock } from "./project-context.js";
|
|
30
|
+
import { inboxPromptHint } from "../memory/inbox.js";
|
|
31
|
+
import { getModeForIntent } from "./modes.js";
|
|
32
|
+
import { config } from "../config.js";
|
|
33
|
+
|
|
34
|
+
// --- System context (system.md — ships with Flint) ---
|
|
35
|
+
|
|
36
|
+
function loadSystemMd() {
|
|
37
|
+
// Only Flint's own copy. cwd used to be tried first, so any folder the user
|
|
38
|
+
// started Flint in that held a file called system.md replaced the whole core
|
|
39
|
+
// prompt without a word. A project speaks through FLINT.md instead.
|
|
40
|
+
const p = join(new URL("../..", import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1"), "system.md");
|
|
41
|
+
try {
|
|
42
|
+
if (existsSync(p)) {
|
|
43
|
+
const text = readFileSync(p, "utf-8").trim();
|
|
44
|
+
// With swap off the swap rule describes tools that are not there, and
|
|
45
|
+
// FLINT_SWAP=0 promises the prompt as it was (docs/context-swap.md, A4).
|
|
46
|
+
const base = swapEnabled() ? text : text.split("\n").filter((l) => !l.includes("swap_read")).join("\n");
|
|
47
|
+
// Economy mode only (spend.js): in the other modes the advice would
|
|
48
|
+
// make the agent hold back for no reason.
|
|
49
|
+
return spendSettings(getSpendLevel()).advice ? `${base}\n\n${ECONOMY_ADVICE}` : base;
|
|
50
|
+
}
|
|
51
|
+
} catch {}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- Flint capability summary (from bundled README.md) ---
|
|
56
|
+
// Extracts the "What can it do?" section and the Features headings from
|
|
57
|
+
// Flint's own README. Injected into the system prompt so meta-questions
|
|
58
|
+
// ("what do you support?", "do you work with MCP?") answer from a live source
|
|
59
|
+
// that tracks actual Flint architecture — no hardcoded capability list,
|
|
60
|
+
// no decay, no ambiguity about which README to read.
|
|
61
|
+
|
|
62
|
+
function loadFlintCapabilities() {
|
|
63
|
+
let text = null;
|
|
64
|
+
try {
|
|
65
|
+
const flintRoot = new URL("../..", import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1");
|
|
66
|
+
const readmePath = join(flintRoot, "README.md");
|
|
67
|
+
if (existsSync(readmePath)) text = readFileSync(readmePath, "utf-8");
|
|
68
|
+
} catch {}
|
|
69
|
+
if (!text) return null;
|
|
70
|
+
|
|
71
|
+
// "## What can it do?" section up to next top-level heading
|
|
72
|
+
const canDoMatch = text.match(/## What can it do\?([\s\S]*?)(?=\n## |\n# |$)/);
|
|
73
|
+
const canDo = canDoMatch ? canDoMatch[1].trim() : "";
|
|
74
|
+
|
|
75
|
+
// Features headings + first meaningful prose paragraph per feature.
|
|
76
|
+
// Skips markdown tables (| ... |), code fences (```), and list-only
|
|
77
|
+
// blocks so the captured blurb is actually descriptive. This matters
|
|
78
|
+
// because e.g. the MCP feature leads with a big tool-category table
|
|
79
|
+
// and only explains itself in a prose paragraph below it — without
|
|
80
|
+
// skipping the table the agent has no signal about what MCP means
|
|
81
|
+
// and defaults to hallucinating "Minecraft Coder Pack".
|
|
82
|
+
const featuresMatch = text.match(/## Features([\s\S]*?)(?=\n## |\n# |$)/);
|
|
83
|
+
const featureLines = [];
|
|
84
|
+
if (featuresMatch) {
|
|
85
|
+
const lines = featuresMatch[1].split("\n");
|
|
86
|
+
for (let i = 0; i < lines.length; i++) {
|
|
87
|
+
const l = lines[i].trim();
|
|
88
|
+
if (!l.startsWith("### ")) continue;
|
|
89
|
+
const heading = l.replace(/^### /, "");
|
|
90
|
+
let blurb = "";
|
|
91
|
+
let inCodeFence = false;
|
|
92
|
+
// Scan up to 25 lines after the heading or until next heading
|
|
93
|
+
for (let j = i + 1; j < Math.min(i + 25, lines.length); j++) {
|
|
94
|
+
const raw = lines[j];
|
|
95
|
+
const ln = raw.trim();
|
|
96
|
+
if (ln.startsWith("### ") || ln.startsWith("## ") || ln.startsWith("# ")) break;
|
|
97
|
+
if (ln.startsWith("```")) { inCodeFence = !inCodeFence; continue; }
|
|
98
|
+
if (inCodeFence) continue;
|
|
99
|
+
// Skip markdown table rows and separator lines
|
|
100
|
+
if (ln.startsWith("|")) continue;
|
|
101
|
+
if (!ln) { if (blurb) break; else continue; }
|
|
102
|
+
blurb += (blurb ? " " : "") + ln;
|
|
103
|
+
if (blurb.length > 320) { blurb = blurb.slice(0, 320) + "..."; break; }
|
|
104
|
+
}
|
|
105
|
+
featureLines.push(blurb ? `- ${heading} — ${blurb}` : `- ${heading}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const parts = [];
|
|
110
|
+
if (canDo) parts.push(canDo);
|
|
111
|
+
if (featureLines.length) parts.push("Feature highlights:\n" + featureLines.join("\n"));
|
|
112
|
+
return parts.length ? parts.join("\n\n") : null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// FLINT.md loading moved to ./project-context.js — shared across classifier,
|
|
116
|
+
// agent loop, and any other LLM-call site. See that module for the loader
|
|
117
|
+
// and the trusted-context wrapping.
|
|
118
|
+
|
|
119
|
+
const MAX_CONTEXT_CHARS = config.maxContextChars;
|
|
120
|
+
|
|
121
|
+
// --- OS detection ---
|
|
122
|
+
|
|
123
|
+
function getOsInfo() {
|
|
124
|
+
const platform = process.platform;
|
|
125
|
+
if (platform === "win32") {
|
|
126
|
+
return {
|
|
127
|
+
name: "Windows",
|
|
128
|
+
shell: "bash (via run_command)",
|
|
129
|
+
shellTips: "run_command uses bash even on Windows. Use Unix commands (ls, cat, grep, find). Do NOT use cmd.exe or PowerShell commands.",
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (platform === "darwin") {
|
|
133
|
+
return {
|
|
134
|
+
name: "macOS",
|
|
135
|
+
shell: "zsh",
|
|
136
|
+
shellTips: "Standard Unix commands. Paths use forward slashes.",
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
name: "Linux",
|
|
141
|
+
shell: "bash",
|
|
142
|
+
shellTips: "Standard Unix commands. Paths use forward slashes.",
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// --- Dynamic tool list from registry ---
|
|
147
|
+
|
|
148
|
+
function getToolList() {
|
|
149
|
+
const tools = getDefinitions();
|
|
150
|
+
if (!tools.length) return "";
|
|
151
|
+
|
|
152
|
+
// Group tools by category based on name patterns
|
|
153
|
+
const categories = {
|
|
154
|
+
filesystem: { label: "LOCAL FILESYSTEM (this machine)", names: new Set(["read_file", "write_file", "list_directory", "create_directory", "copy_file", "move_file", "delete_file", "search_in_files", "view_image", "edit_file", "glob"]) },
|
|
155
|
+
process: { label: "LOCAL PROCESS (this machine)", names: new Set(["run_command", "run_background_command", "kill_process", "list_processes", "peek_process"]) },
|
|
156
|
+
web: { label: "WEB", names: new Set(["web_fetch", "web_search"]) },
|
|
157
|
+
agent: { label: "MULTI-AGENT", names: new Set(["spawn_agent", "ask_agent", "list_agents"]) },
|
|
158
|
+
memory: { label: "MEMORY", names: new Set(["memory_write", "memory_search", "memory_get", "memory_delete"]) },
|
|
159
|
+
planning: { label: "PLANNING", names: new Set(["create_plan", "update_task", "list_tasks", "add_task", "add_task_note", "link_task_file"]) },
|
|
160
|
+
desktop: { label: "LOCAL DESKTOP (built-in, this machine)", names: new Set(["desktop_screenshot", "desktop_look", "desktop_click", "desktop_type", "desktop_key", "desktop_chrome", "desktop_resume", "desktop_batch", "desktop_shell", "desktop_window", "desktop_manage", "desktop_file", "desktop_wait_stable", "desktop_wait_change"]) },
|
|
161
|
+
mesh: { label: "MESH MEMORY", names: new Set(["mesh_search", "mesh_add", "mesh_recent"]) },
|
|
162
|
+
dataset: { label: "DATASETS", names: new Set(["show_dataset"]) },
|
|
163
|
+
system: { label: "SYSTEM", names: new Set(["restart_agent", "clear_context", "check_balance", "think", "list_models", "switch_model", "switch_provider", "list_providers", "check_inbox"]) },
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const grouped = {};
|
|
167
|
+
const uncategorized = [];
|
|
168
|
+
|
|
169
|
+
for (const tool of tools) {
|
|
170
|
+
const name = tool.function.name;
|
|
171
|
+
let found = false;
|
|
172
|
+
|
|
173
|
+
// Check exact name match in built-in categories
|
|
174
|
+
for (const [key, cat] of Object.entries(categories)) {
|
|
175
|
+
if (cat.names.has(name)) {
|
|
176
|
+
if (!grouped[key]) grouped[key] = { label: cat.label, tools: [] };
|
|
177
|
+
grouped[key].tools.push(name);
|
|
178
|
+
found = true;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// MCP tools: auto-group by server name (mcp_<server>_<tool>)
|
|
184
|
+
if (!found && name.startsWith("mcp_")) {
|
|
185
|
+
const parts = name.split("_");
|
|
186
|
+
const serverName = parts[1] || "unknown";
|
|
187
|
+
const mcpKey = `mcp_${serverName}`;
|
|
188
|
+
if (!grouped[mcpKey]) grouped[mcpKey] = { label: `MCP: ${serverName} (remote)`, tools: [] };
|
|
189
|
+
grouped[mcpKey].tools.push(name);
|
|
190
|
+
found = true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!found) {
|
|
194
|
+
uncategorized.push(name);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const sections = [];
|
|
199
|
+
for (const key of Object.keys(categories)) {
|
|
200
|
+
if (grouped[key]) {
|
|
201
|
+
sections.push(`${grouped[key].label}: ${grouped[key].tools.join(", ")}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (uncategorized.length) {
|
|
205
|
+
sections.push(`OTHER: ${uncategorized.join(", ")}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return sections.join("\n\n");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// --- Build system prompt ---
|
|
212
|
+
|
|
213
|
+
function buildPrompt(opts = {}) {
|
|
214
|
+
const { profile, profileContent } = opts;
|
|
215
|
+
const osInfo = getOsInfo();
|
|
216
|
+
|
|
217
|
+
const parts = [];
|
|
218
|
+
|
|
219
|
+
// Core prompt loaded from system.md (no hardcoded instructions in code)
|
|
220
|
+
const systemMd = loadSystemMd();
|
|
221
|
+
if (systemMd) {
|
|
222
|
+
parts.push(systemMd);
|
|
223
|
+
} else {
|
|
224
|
+
parts.push("You are FLINT, a technical AI agent that executes tasks using available tools.");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Project rules (FLINT.md) — immediately after system.md, before profile.
|
|
228
|
+
// This is the agent's own project notebook: facts about the current
|
|
229
|
+
// project, conventions, server names, workspace identifiers, shortcuts.
|
|
230
|
+
// Trusted because it's maintained by the agent itself (scanned for
|
|
231
|
+
// injection). Placed high in the prompt so it's not lost in context
|
|
232
|
+
// middle. See src/agent/project-context.js for the loader.
|
|
233
|
+
const flintMdBlock = formatFlintMdBlock(process.cwd());
|
|
234
|
+
if (flintMdBlock) {
|
|
235
|
+
parts.push("");
|
|
236
|
+
parts.push(flintMdBlock);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Profile prompt (injected from profile .md file)
|
|
240
|
+
if (profileContent) {
|
|
241
|
+
parts.push("");
|
|
242
|
+
parts.push(`--- ROLE: ${profile || "custom"} ---`);
|
|
243
|
+
parts.push(profileContent);
|
|
244
|
+
parts.push("--- END ROLE ---");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Environment, the part of it that does not move. The clock used to live on
|
|
248
|
+
// this line, in the middle of the section the layout calls FIXED, at minute
|
|
249
|
+
// resolution: it changed the prefix every later token is cached against
|
|
250
|
+
// roughly once a minute. It is now a per-turn section below the boundary.
|
|
251
|
+
parts.push("");
|
|
252
|
+
parts.push(`ENVIRONMENT: ${osInfo.name} (${os.arch()}) | Shell: ${osInfo.shell} | CWD: ${process.cwd()}`);
|
|
253
|
+
if (osInfo.name === "Windows") parts.push("Shell uses bash. Use Unix commands (ls, cat, grep), not PowerShell.");
|
|
254
|
+
|
|
255
|
+
// Task-driven child agent instructions
|
|
256
|
+
if (process.env.AGENT_TASK_ID) {
|
|
257
|
+
parts.push("");
|
|
258
|
+
parts.push([
|
|
259
|
+
`CHILD AGENT MODE: Executing task #${process.env.AGENT_TASK_ID}.`,
|
|
260
|
+
"Focus ONLY on this task. Use update_task to report progress. Auto-completed on exit.",
|
|
261
|
+
].join("\n"));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return parts.join("\n");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// --- Agent-memory prefetch ---
|
|
268
|
+
let _agentMemoryContext = null;
|
|
269
|
+
|
|
270
|
+
export async function prefetchAgentMemory() {
|
|
271
|
+
try {
|
|
272
|
+
const { execFileSync } = await import("node:child_process");
|
|
273
|
+
// Try to find agent-memory binary
|
|
274
|
+
const bins = [
|
|
275
|
+
process.env.AGENT_MEMORY_BIN,
|
|
276
|
+
"agent-memory",
|
|
277
|
+
"agent-memory.exe",
|
|
278
|
+
].filter(Boolean);
|
|
279
|
+
for (const bin of bins) {
|
|
280
|
+
try {
|
|
281
|
+
const result = execFileSync(bin, ["context", "--budget", "3000"], {
|
|
282
|
+
encoding: "utf-8",
|
|
283
|
+
timeout: 5000,
|
|
284
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
285
|
+
}).trim();
|
|
286
|
+
if (result && result.length > 10) {
|
|
287
|
+
_agentMemoryContext = result;
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
290
|
+
} catch {}
|
|
291
|
+
}
|
|
292
|
+
} catch {}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// --- Public API ---
|
|
297
|
+
|
|
298
|
+
export function getSystemMessage(sessionId, opts = {}) {
|
|
299
|
+
const allocator = createBudgetAllocator();
|
|
300
|
+
|
|
301
|
+
// ── FIXED SECTIONS (never truncated) ──
|
|
302
|
+
|
|
303
|
+
// Nothing added here may vary within a session. Everything downstream is
|
|
304
|
+
// cached against this text, so a single moving character in it costs the
|
|
305
|
+
// whole prompt. The mode block used to be appended right here and changes
|
|
306
|
+
// with the intent class, which changes every turn; it is a per-turn section
|
|
307
|
+
// now.
|
|
308
|
+
const coreContent = buildPrompt(opts);
|
|
309
|
+
|
|
310
|
+
allocator.addSection("core", coreContent, { priority: 1, fixed: true });
|
|
311
|
+
|
|
312
|
+
// Flint architectural capabilities (from bundled README) — injected
|
|
313
|
+
// so self-description questions have a live source without the agent
|
|
314
|
+
// needing to guess or read an ambiguous README from the user's cwd.
|
|
315
|
+
const capabilitiesText = loadFlintCapabilities();
|
|
316
|
+
if (capabilitiesText) {
|
|
317
|
+
allocator.addSection(
|
|
318
|
+
"flint-capabilities",
|
|
319
|
+
`<trusted-context name="flint-capabilities" source="README.md">\n${capabilitiesText}\n</trusted-context>`,
|
|
320
|
+
{ priority: 2, min: 300, max: 2500 },
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ── SESSION-STABLE ZONE (cached across turns, budgeted) ──
|
|
325
|
+
|
|
326
|
+
const memoryContent = readMemoryMdHead(50);
|
|
327
|
+
if (memoryContent) {
|
|
328
|
+
const scan = detectInjection(memoryContent);
|
|
329
|
+
const memText = (scan.detected && scan.score >= 3)
|
|
330
|
+
? `[BLOCKED: MEMORY.md failed injection scan (score ${scan.score})]`
|
|
331
|
+
: memoryContent;
|
|
332
|
+
allocator.addSection("memory-md", `<untrusted-context name="memory" source="MEMORY.md">\n${memText}\n</untrusted-context>`, { priority: 3, min: 200, max: 2000 });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (_agentMemoryContext) {
|
|
336
|
+
allocator.addSection("agent-memory", `<trusted-context name="agent-memory">\n${_agentMemoryContext}\n</trusted-context>`, { priority: 3, min: 500, max: 3000 });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Memory Layer 1: reflections
|
|
340
|
+
try {
|
|
341
|
+
const recentReflections = loadRecentReflections(5);
|
|
342
|
+
const reflectionsText = formatReflections(recentReflections);
|
|
343
|
+
if (reflectionsText) {
|
|
344
|
+
allocator.addSection("reflections", `<trusted-context name="past-lessons">\n${reflectionsText}\n</trusted-context>`, { priority: 4, min: 200, max: 1500 });
|
|
345
|
+
}
|
|
346
|
+
} catch {}
|
|
347
|
+
|
|
348
|
+
// Memory Layer 2: patterns
|
|
349
|
+
try {
|
|
350
|
+
const prefs = compilePreferences();
|
|
351
|
+
const patternsText = formatPatterns(prefs);
|
|
352
|
+
if (patternsText) {
|
|
353
|
+
allocator.addSection("patterns", `<trusted-context name="tool-patterns">\n${patternsText}\n</trusted-context>`, { priority: 4, min: 200, max: 1500 });
|
|
354
|
+
}
|
|
355
|
+
} catch {}
|
|
356
|
+
|
|
357
|
+
// Memory Layer 3: skills (index only)
|
|
358
|
+
try {
|
|
359
|
+
syncSkills();
|
|
360
|
+
const skillsText = formatSkills();
|
|
361
|
+
if (skillsText) {
|
|
362
|
+
const scan = detectInjection(skillsText);
|
|
363
|
+
const text = (scan.detected && scan.score >= 3)
|
|
364
|
+
? `<trusted-context name="skills">\n[BLOCKED: skills content failed injection scan (score ${scan.score}).]\n</trusted-context>`
|
|
365
|
+
: `<untrusted-context name="skills" source="~/.flint/memory/skills/">\n${skillsText}\n</untrusted-context>`;
|
|
366
|
+
allocator.addSection("skills", text, { priority: 5, min: 100, max: 1000 });
|
|
367
|
+
}
|
|
368
|
+
} catch {}
|
|
369
|
+
|
|
370
|
+
// Memory Layer 4: facts
|
|
371
|
+
try {
|
|
372
|
+
const currentProject = getCurrentProject();
|
|
373
|
+
const factsText = formatFacts(500, currentProject);
|
|
374
|
+
if (factsText) {
|
|
375
|
+
const scan = detectInjection(factsText);
|
|
376
|
+
const text = (scan.detected && scan.score >= 3)
|
|
377
|
+
? `<trusted-context name="facts">\n[BLOCKED: facts failed injection scan (score ${scan.score})]\n</trusted-context>`
|
|
378
|
+
: `<untrusted-context name="facts" source="user-extracted">\n${factsText}\n</untrusted-context>`;
|
|
379
|
+
allocator.addSection("facts", text, { priority: 5, min: 100, max: 500 });
|
|
380
|
+
}
|
|
381
|
+
} catch {}
|
|
382
|
+
|
|
383
|
+
// Memory Layer 5: user model
|
|
384
|
+
try {
|
|
385
|
+
const userModelText = formatUserModel(0.5);
|
|
386
|
+
if (userModelText) {
|
|
387
|
+
allocator.addSection("user-profile", `<trusted-context name="user-profile">\n${userModelText}\n</trusted-context>`, { priority: 5, min: 50, max: 300 });
|
|
388
|
+
}
|
|
389
|
+
} catch {}
|
|
390
|
+
|
|
391
|
+
// ── DYNAMIC BOUNDARY ──
|
|
392
|
+
allocator.addSection("boundary", "__DYNAMIC_BOUNDARY__", { priority: 1, fixed: true });
|
|
393
|
+
|
|
394
|
+
// ── PER-TURN ZONE ──
|
|
395
|
+
|
|
396
|
+
// The mode. It moves with the intent class, so it lives below the boundary
|
|
397
|
+
// in the per-turn zone. Fixed, because a truncated half of a mode is worse than a longer
|
|
398
|
+
// prompt.
|
|
399
|
+
//
|
|
400
|
+
// No clock here any more. Even below the boundary a line that moves
|
|
401
|
+
// every minute is still ABOVE the tools on a model whose template renders
|
|
402
|
+
// them after the system message, so every new task paid for the tools and
|
|
403
|
+
// the request again: one benchmark run cached 4096-6144 of ~7500 tokens on Flint's
|
|
404
|
+
// first calls against ~99% for a reference agent, and Flint paid 2.3x. system.md tells
|
|
405
|
+
// the agent to ask the system for the date and time when it needs them.
|
|
406
|
+
let perTurn = "";
|
|
407
|
+
if (opts.intentClass) {
|
|
408
|
+
try {
|
|
409
|
+
const mode = getModeForIntent(opts.intentClass);
|
|
410
|
+
if (mode && mode.promptAddition) {
|
|
411
|
+
perTurn = `<trusted-context name="mode">\n${mode.promptAddition}\n</trusted-context>`;
|
|
412
|
+
}
|
|
413
|
+
} catch {}
|
|
414
|
+
}
|
|
415
|
+
if (perTurn) allocator.addSection("turn-context", perTurn, { priority: 1, fixed: true });
|
|
416
|
+
|
|
417
|
+
if (sessionId) {
|
|
418
|
+
const sessionFacts = getSessionFactsSummary(sessionId, 30);
|
|
419
|
+
if (sessionFacts) {
|
|
420
|
+
allocator.addSection("session-facts", `<trusted-context name="session-facts">\nKey facts extracted from this conversation so far:\n${sessionFacts}\n</trusted-context>`, { priority: 6, min: 100, max: 2000 });
|
|
421
|
+
}
|
|
422
|
+
const digest = getDigestForPrompt(sessionId, 25);
|
|
423
|
+
if (digest) {
|
|
424
|
+
allocator.addSection("digest", `<trusted-context name="conversation-digest">\n${digest}\nUse this to recall what the user asked and what you answered in previous turns.\n</trusted-context>`, { priority: 6, min: 200, max: 3000 });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (opts.datasets) {
|
|
429
|
+
const entries = Object.values(opts.datasets);
|
|
430
|
+
if (entries.length > 0) {
|
|
431
|
+
const lines = entries.map((ds) => {
|
|
432
|
+
const totalPages = Math.ceil(ds.rows.length / ds.pageSize);
|
|
433
|
+
return ` ${ds.id} "${ds.label}" ${ds.rows.length} rows, page ${ds.page}/${totalPages} (source: ${ds.source})`;
|
|
434
|
+
});
|
|
435
|
+
allocator.addSection("datasets", `<trusted-context name="datasets">\n${lines.join("\n")}\nUse show_dataset(id, page) to view pages.\n</trusted-context>`, { priority: 7, min: 50, max: 500 });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const inboxHint = inboxPromptHint();
|
|
440
|
+
if (inboxHint) {
|
|
441
|
+
allocator.addSection("inbox", `<trusted-context name="notifications">\n${inboxHint}\n</trusted-context>`, { priority: 8, min: 20, max: 200 });
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const { prompt } = allocator.build();
|
|
445
|
+
return { role: "system", content: prompt };
|
|
446
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// The local date and time at the start of each message the operator sends.
|
|
2
|
+
//
|
|
3
|
+
// Owner, 2026-10-02: a research session wrote "Compiled 2026-09-30" into
|
|
4
|
+
// four documents on 2026-10-02. The clock is kept out of the system prompt on
|
|
5
|
+
// purpose (a line that moved every minute at the top of the prompt cost the
|
|
6
|
+
// cache everything after it, 2.3x on a benchmark), and system.md asked the
|
|
7
|
+
// agent to run `date` when it needed the time; it took a date from the
|
|
8
|
+
// articles it had read instead.
|
|
9
|
+
//
|
|
10
|
+
// Here the time goes into the new message, at the end of the conversation,
|
|
11
|
+
// which is never cached anyway; everything before it is unchanged. It is
|
|
12
|
+
// written once, when the message is added to the history, so every later call
|
|
13
|
+
// of the turn and every later turn sends the same bytes. Nothing on screen
|
|
14
|
+
// shows it.
|
|
15
|
+
|
|
16
|
+
const STAMP_RE = /^\[Local time: [^\]\n]*\]\n?/;
|
|
17
|
+
|
|
18
|
+
/** "[Local time: Fri 2026-10-02 10:16 CDT, UTC-05:00]" for the given moment. */
|
|
19
|
+
export function messageTimeStamp(date = new Date()) {
|
|
20
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
21
|
+
const day = date.toLocaleDateString("en-US", { weekday: "short" });
|
|
22
|
+
const ymd = `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())}`;
|
|
23
|
+
const hm = `${p(date.getHours())}:${p(date.getMinutes())}`;
|
|
24
|
+
let zone = "";
|
|
25
|
+
try {
|
|
26
|
+
zone = new Intl.DateTimeFormat("en-US", { timeZoneName: "short" })
|
|
27
|
+
.formatToParts(date).find((x) => x.type === "timeZoneName")?.value || "";
|
|
28
|
+
} catch {}
|
|
29
|
+
const off = -date.getTimezoneOffset();
|
|
30
|
+
const utc = `UTC${off >= 0 ? "+" : "-"}${p(Math.floor(Math.abs(off) / 60))}:${p(Math.abs(off) % 60)}`;
|
|
31
|
+
return `[Local time: ${day} ${ymd} ${hm}${zone ? ` ${zone}` : ""}, ${utc}]`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The message content with the stamp in front: a string gets a first line, a
|
|
36
|
+
* list of parts gets a first text part.
|
|
37
|
+
*/
|
|
38
|
+
export function withTimeStamp(content, date = new Date()) {
|
|
39
|
+
const stamp = messageTimeStamp(date);
|
|
40
|
+
if (typeof content === "string") return `${stamp}\n${content}`;
|
|
41
|
+
if (Array.isArray(content)) return [{ type: "text", text: stamp }, ...content];
|
|
42
|
+
return content;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The text without a leading stamp, for showing a message back to a person. */
|
|
46
|
+
export function stripTimeStamp(text) {
|
|
47
|
+
return typeof text === "string" ? text.replace(STAMP_RE, "") : text;
|
|
48
|
+
}
|