mixdog 0.9.15 → 0.9.17
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/package.json +2 -1
- package/scripts/atomic-lock-tryonce-test.mjs +66 -0
- package/scripts/explore-bench.mjs +5 -4
- package/scripts/ingest-pure-conversation-smoke.mjs +27 -0
- package/scripts/output-style-smoke.mjs +3 -3
- package/scripts/provider-toolcall-test.mjs +79 -2
- package/scripts/termio-input-smoke.mjs +199 -0
- package/scripts/tool-efficiency-diag.mjs +88 -0
- package/scripts/tool-smoke.mjs +40 -24
- package/scripts/tui-frame-harness-shim.mjs +32 -0
- package/scripts/tui-frame-harness.mjs +306 -0
- package/src/agents/heavy-worker/AGENT.md +6 -7
- package/src/agents/worker/AGENT.md +6 -7
- package/src/lib/keychain-cjs.cjs +28 -11
- package/src/lib/rules-builder.cjs +6 -10
- package/src/mixdog-session-runtime.mjs +102 -20
- package/src/output-styles/simple.md +22 -24
- package/src/rules/agent/00-core.md +12 -11
- package/src/rules/agent/30-explorer.md +22 -21
- package/src/rules/lead/01-general.md +8 -8
- package/src/rules/lead/lead-brief.md +11 -12
- package/src/rules/shared/01-tool.md +25 -14
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +4 -3
- package/src/runtime/agent/orchestrator/config.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +33 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +31 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +29 -8
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +13 -5
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +30 -2
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +19 -0
- package/src/runtime/agent/orchestrator/session/compact/constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +37 -15
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +62 -0
- package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +177 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +12 -19
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +30 -1
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +13 -18
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +37 -19
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +142 -3
- package/src/runtime/agent/orchestrator/session/manager.mjs +59 -2
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +118 -22
- package/src/runtime/agent/orchestrator/session/store.mjs +38 -25
- package/src/runtime/agent/orchestrator/stall-policy.mjs +22 -12
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +7 -5
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +38 -1
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +17 -16
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +14 -1
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +25 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +73 -3
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -10
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +55 -0
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +10 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +8 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +21 -13
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +13 -2
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +44 -6
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +5 -0
- package/src/runtime/channels/backends/telegram.mjs +29 -9
- package/src/runtime/channels/lib/event-queue.mjs +6 -2
- package/src/runtime/channels/lib/memory-client.mjs +66 -1
- package/src/runtime/channels/lib/runtime-paths.mjs +8 -19
- package/src/runtime/channels/lib/webhook/deliveries.mjs +22 -1
- package/src/runtime/memory/index.mjs +132 -8
- package/src/runtime/memory/lib/cycle-scheduler.mjs +24 -5
- package/src/runtime/memory/lib/embedding-warmup.mjs +3 -0
- package/src/runtime/memory/lib/ko-morph.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle1.mjs +45 -3
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +7 -3
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +54 -10
- package/src/runtime/memory/lib/memory-cycle2.mjs +21 -8
- package/src/runtime/memory/lib/memory-embed.mjs +48 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +57 -13
- package/src/runtime/memory/lib/memory.mjs +88 -1
- package/src/runtime/memory/lib/session-ingest.mjs +34 -3
- package/src/runtime/memory/lib/trace-store.mjs +52 -3
- package/src/runtime/memory/lib/transcript-ingest.mjs +27 -4
- package/src/runtime/shared/atomic-file.mjs +110 -0
- package/src/runtime/shared/child-guardian.mjs +4 -2
- package/src/runtime/shared/open-url.mjs +2 -1
- package/src/runtime/shared/singleton-owner.mjs +26 -0
- package/src/runtime/shared/tool-execution-contract.mjs +25 -0
- package/src/runtime/shared/transcript-writer.mjs +49 -5
- package/src/session-runtime/config-helpers.mjs +7 -2
- package/src/session-runtime/cwd-plugins.mjs +6 -1
- package/src/session-runtime/effort.mjs +6 -1
- package/src/session-runtime/plugin-mcp.mjs +116 -12
- package/src/session-runtime/provider-models.mjs +47 -8
- package/src/session-runtime/settings-api.mjs +7 -1
- package/src/standalone/channel-worker.mjs +25 -3
- package/src/standalone/explore-tool.mjs +5 -5
- package/src/standalone/hook-bus/config.mjs +38 -2
- package/src/standalone/hook-bus/handlers.mjs +103 -11
- package/src/standalone/hook-bus.mjs +20 -6
- package/src/standalone/memory-runtime-proxy.mjs +40 -5
- package/src/tui/App.jsx +214 -30
- package/src/tui/app/core-memory-picker.mjs +6 -6
- package/src/tui/app/model-options.mjs +10 -4
- package/src/tui/app/settings-picker.mjs +5 -30
- package/src/tui/app/slash-commands.mjs +0 -1
- package/src/tui/app/slash-dispatch.mjs +0 -16
- package/src/tui/app/text-layout.mjs +57 -0
- package/src/tui/app/transcript-window.mjs +162 -2
- package/src/tui/app/use-mouse-input.mjs +60 -47
- package/src/tui/app/use-transcript-window.mjs +96 -17
- package/src/tui/components/PromptInput.jsx +18 -1
- package/src/tui/components/StatusLine.jsx +1 -1
- package/src/tui/components/TextEntryPanel.jsx +97 -33
- package/src/tui/dist/index.mjs +856 -288
- package/src/tui/engine/tool-card-results.mjs +22 -5
- package/src/tui/engine/tui-steering-persist.mjs +66 -35
- package/src/tui/engine.mjs +41 -9
- package/src/tui/index.jsx +101 -7
- package/src/ui/statusline-segments.mjs +54 -36
- package/src/ui/statusline.mjs +141 -95
- package/src/workflows/default/WORKFLOW.md +27 -31
- package/vendor/ink/build/components/App.js +62 -17
- package/vendor/ink/build/ink.js +78 -3
- package/vendor/ink/build/input-parser.d.ts +9 -4
- package/vendor/ink/build/input-parser.js +45 -176
- package/vendor/ink/build/log-update.js +47 -2
- package/vendor/ink/build/termio-keypress.js +240 -0
- package/vendor/ink/build/termio-tokenize.js +253 -0
|
@@ -3,6 +3,7 @@ import { homedir } from 'node:os';
|
|
|
3
3
|
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { performance } from 'node:perf_hooks';
|
|
6
|
+
import httpMod from 'node:http';
|
|
6
7
|
import { ensureStandaloneEnvironment } from './standalone/seeds.mjs';
|
|
7
8
|
import { createStandaloneAgent } from './standalone/agent-tool.mjs';
|
|
8
9
|
import { isAgentOwner } from './runtime/agent/orchestrator/agent-owner.mjs';
|
|
@@ -167,6 +168,9 @@ import {
|
|
|
167
168
|
normalizePluginMcpServerConfig,
|
|
168
169
|
pluginManifest,
|
|
169
170
|
pluginMcpServerName,
|
|
171
|
+
pluginRawMcpServers,
|
|
172
|
+
pluginMcpEnableScript,
|
|
173
|
+
resolveContainedPluginPath,
|
|
170
174
|
} from './session-runtime/plugin-mcp.mjs';
|
|
171
175
|
import {
|
|
172
176
|
WORKFLOW_ROUTE_SLOTS,
|
|
@@ -395,6 +399,13 @@ export async function createMixdogSessionRuntime({
|
|
|
395
399
|
]);
|
|
396
400
|
bootProfile('imports:ready', { ms: (performance.now() - importsStartedAt).toFixed(1) });
|
|
397
401
|
const pluginDataDir = cfgMod.getPluginData();
|
|
402
|
+
// Re-wire the idle/tombstone sweep. startIdleCleanup() lost its caller in a
|
|
403
|
+
// refactor, so closed-session tombstones were never deleted after their 24h
|
|
404
|
+
// grace — the store grew unbounded (observed: 1.8k files / 114MB), which
|
|
405
|
+
// made summary-index rebuilds and per-save index rewrites stall boot for
|
|
406
|
+
// seconds. Timer is unref'd and first fires after CLEANUP_INITIAL_DELAY_MS
|
|
407
|
+
// (5min), so this adds zero boot-path cost.
|
|
408
|
+
try { mgr.startIdleCleanup?.(); } catch { /* cleanup is best-effort */ }
|
|
398
409
|
const memoryRuntime = createStandaloneMemoryRuntime({
|
|
399
410
|
entry: join(STANDALONE_ROOT, MEMORY_RUNTIME.replace(/^\.\//, '')),
|
|
400
411
|
dataDir: pluginDataDir,
|
|
@@ -1531,11 +1542,48 @@ export async function createMixdogSessionRuntime({
|
|
|
1531
1542
|
// published to active-instance.json by getMemoryModule().init(). Without
|
|
1532
1543
|
// this, memory only starts on the first turn's getMemoryModule() call —
|
|
1533
1544
|
// so early channel traffic finds no memory_port and gets buffered (or,
|
|
1534
|
-
// pre-drainer, silently dropped).
|
|
1535
|
-
//
|
|
1536
|
-
//
|
|
1537
|
-
|
|
1538
|
-
|
|
1545
|
+
// pre-drainer, silently dropped). Runs BEFORE channel claim so the port is
|
|
1546
|
+
// racing to be live by the time the worker sends its first ingest.
|
|
1547
|
+
//
|
|
1548
|
+
// Not fire-and-forget: init() only resolves the module handle, so a bare
|
|
1549
|
+
// getMemoryModule() could return before /health reports ok — leaving early
|
|
1550
|
+
// ingests to hit a not-yet-listening port. Await the proxy start() and then
|
|
1551
|
+
// poll /health with a bounded retry so the daemon is provably reachable
|
|
1552
|
+
// (or logged failed) rather than assumed-up. Still non-blocking to the
|
|
1553
|
+
// caller: the whole probe is detached, but it internally awaits readiness.
|
|
1554
|
+
void (async () => {
|
|
1555
|
+
try {
|
|
1556
|
+
// Yield one event-loop tick before the heavy chain below (module
|
|
1557
|
+
// resolve, daemon fork/health-poll) starts, so Ink's next render
|
|
1558
|
+
// (scheduled via setImmediate/timers) and any queued keypress
|
|
1559
|
+
// events get a turn first instead of being starved by this
|
|
1560
|
+
// detached chain's synchronous setup work.
|
|
1561
|
+
await new Promise((r) => setImmediate(r));
|
|
1562
|
+
const mod = await getMemoryModule();
|
|
1563
|
+
const started = typeof mod?.start === 'function' ? await mod.start() : null;
|
|
1564
|
+
const port = started?.port;
|
|
1565
|
+
if (!port) { bootProfile('channels:memory-eager-init-failed', { error: 'no port from start()' }); return; }
|
|
1566
|
+
for (let i = 0; i < 30; i++) {
|
|
1567
|
+
try {
|
|
1568
|
+
const ok = await new Promise((res) => {
|
|
1569
|
+
const req = httpMod.request({ hostname: '127.0.0.1', port, path: '/health', timeout: 1500 }, (r) => {
|
|
1570
|
+
let d = ''; r.on('data', (c) => { d += c; }); r.on('end', () => {
|
|
1571
|
+
try { res(JSON.parse(d)?.status === 'ok'); } catch { res(false); }
|
|
1572
|
+
});
|
|
1573
|
+
});
|
|
1574
|
+
req.on('error', () => res(false));
|
|
1575
|
+
req.on('timeout', () => { req.destroy(); res(false); });
|
|
1576
|
+
req.end();
|
|
1577
|
+
});
|
|
1578
|
+
if (ok) { bootProfile('channels:memory-eager-init-ready', { port }); return; }
|
|
1579
|
+
} catch {}
|
|
1580
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1581
|
+
}
|
|
1582
|
+
bootProfile('channels:memory-eager-init-failed', { error: `health not ok after retries (port ${port})` });
|
|
1583
|
+
} catch (error) {
|
|
1584
|
+
bootProfile('channels:memory-eager-init-failed', { error: error?.message || String(error) });
|
|
1585
|
+
}
|
|
1586
|
+
})();
|
|
1539
1587
|
// Publish this session's record + transcript file BEFORE the worker's
|
|
1540
1588
|
// activate-time discovery polls, so output forwarding binds to this
|
|
1541
1589
|
// terminal session immediately instead of waiting for the first turn.
|
|
@@ -1561,6 +1609,12 @@ export async function createMixdogSessionRuntime({
|
|
|
1561
1609
|
}
|
|
1562
1610
|
bootProfile('channels:start-scheduled', { delayMs: 0, immediate: true });
|
|
1563
1611
|
void (async () => {
|
|
1612
|
+
// Yield before the createCurrentSession/transcript/fork chain below —
|
|
1613
|
+
// same rationale as the memory-eager-init yield above: this detached
|
|
1614
|
+
// chain runs synchronous config/fs work (createCurrentSession, backend
|
|
1615
|
+
// flush, transcript writer) back-to-back, and without a tick break it
|
|
1616
|
+
// can run ahead of Ink's queued render/input handling.
|
|
1617
|
+
await new Promise((r) => setImmediate(r));
|
|
1564
1618
|
// Immediate-occupancy guarantee: make sure a session + transcript
|
|
1565
1619
|
// exist BEFORE the worker boots — a freshly-forked worker claims and
|
|
1566
1620
|
// runs transcript discovery inside its own start(), so publishing the
|
|
@@ -1640,8 +1694,38 @@ export async function createMixdogSessionRuntime({
|
|
|
1640
1694
|
// channel; see startRemote()/stopRemote() and the `/remote` toggle.
|
|
1641
1695
|
// `remote.autoStart` in mixdog-config.json makes every session claim remote
|
|
1642
1696
|
// at boot (same force-takeover semantics as `mixdog --remote` / `/remote`).
|
|
1643
|
-
|
|
1644
|
-
|
|
1697
|
+
// The flag lives in the TOP-LEVEL `remote` section of mixdog-config.json
|
|
1698
|
+
// (sibling of agent/ui/channels), not inside the agent section that
|
|
1699
|
+
// cfgMod.loadConfig returns — read it via the shared whole-file reader.
|
|
1700
|
+
// `config?.remote?.autoStart` is kept for back-compat with agent-section
|
|
1701
|
+
// placement.
|
|
1702
|
+
if (!remoteEnabled) {
|
|
1703
|
+
try {
|
|
1704
|
+
if (config?.remote?.autoStart === true
|
|
1705
|
+
|| sharedCfgMod?.readSection?.('remote')?.autoStart === true) {
|
|
1706
|
+
remoteEnabled = true;
|
|
1707
|
+
}
|
|
1708
|
+
} catch { /* unreadable config never blocks boot */ }
|
|
1709
|
+
}
|
|
1710
|
+
// Boot-time remote start (autoStart or --remote) is DEFERRED past the TUI's
|
|
1711
|
+
// first frame. startRemote() front-loads heavy work — memory daemon fork
|
|
1712
|
+
// (PG + forced ONNX embed warmup in the child), eager session create, and
|
|
1713
|
+
// the channel-worker fork — and running it inline here interleaves that
|
|
1714
|
+
// CPU/disk load with engine boot, visibly delaying the first ink frame by
|
|
1715
|
+
// seconds. The deferred timer reuses prewarmTimers.channelStartTimer so an
|
|
1716
|
+
// early /remote (startRemote clears it), stopRemote(), and close() all
|
|
1717
|
+
// cancel it through the existing clearTimeout paths. Runtime /remote calls
|
|
1718
|
+
// still start immediately (user-initiated, UI already painted).
|
|
1719
|
+
if (remoteEnabled) {
|
|
1720
|
+
const remoteAutoStartDelayMs = envDelayMs('MIXDOG_REMOTE_AUTOSTART_DELAY_MS', 1_500, { min: 0, max: 60_000 });
|
|
1721
|
+
bootProfile('channels:autostart-deferred', { delayMs: remoteAutoStartDelayMs });
|
|
1722
|
+
prewarmTimers.channelStartTimer = setTimeout(() => {
|
|
1723
|
+
prewarmTimers.channelStartTimer = null;
|
|
1724
|
+
if (closeRequested || !remoteEnabled) return;
|
|
1725
|
+
startRemote();
|
|
1726
|
+
}, remoteAutoStartDelayMs);
|
|
1727
|
+
prewarmTimers.channelStartTimer.unref?.();
|
|
1728
|
+
}
|
|
1645
1729
|
|
|
1646
1730
|
function contextStatusCacheKeyFor({ messages, tools }) {
|
|
1647
1731
|
const compaction = session?.compaction || {};
|
|
@@ -2769,19 +2853,17 @@ export async function createMixdogSessionRuntime({
|
|
|
2769
2853
|
},
|
|
2770
2854
|
async enablePluginMcp(plugin = {}) {
|
|
2771
2855
|
const root = clean(plugin.root);
|
|
2772
|
-
const script =
|
|
2856
|
+
const script = pluginMcpEnableScript(root, plugin);
|
|
2773
2857
|
if (!root || !script) throw new Error('plugin has no MCP script');
|
|
2774
2858
|
const serverName = pluginMcpServerName(plugin);
|
|
2775
2859
|
const nextConfig = { ...config };
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
const
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
const keys = Object.keys(rawServers).filter((k) => isPlainObject(rawServers[k]));
|
|
2784
|
-
if (!keys.length) throw new Error(`plugin .mcp.json has no mcpServers: ${mcpJsonPath}`);
|
|
2860
|
+
const manifestMcp = pluginRawMcpServers(root, script);
|
|
2861
|
+
if (manifestMcp) {
|
|
2862
|
+
const { rawServers, mcpRoot } = manifestMcp;
|
|
2863
|
+
const keys = Object.keys(rawServers).filter((k) => {
|
|
2864
|
+
const v = rawServers[k];
|
|
2865
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
2866
|
+
});
|
|
2785
2867
|
const ownedPrefix = `${serverName}--`;
|
|
2786
2868
|
const nextServers = {};
|
|
2787
2869
|
for (const [k, v] of Object.entries(nextConfig.mcpServers || {})) {
|
|
@@ -2789,7 +2871,7 @@ export async function createMixdogSessionRuntime({
|
|
|
2789
2871
|
nextServers[k] = v;
|
|
2790
2872
|
}
|
|
2791
2873
|
for (const serverKey of keys) {
|
|
2792
|
-
const cfg = normalizePluginMcpServerConfig(rawServers[serverKey],
|
|
2874
|
+
const cfg = normalizePluginMcpServerConfig(rawServers[serverKey], mcpRoot);
|
|
2793
2875
|
cfg.env = {
|
|
2794
2876
|
...(cfg.env || {}),
|
|
2795
2877
|
MIXDOG_PLUGIN_ROOT: root,
|
|
@@ -2800,8 +2882,8 @@ export async function createMixdogSessionRuntime({
|
|
|
2800
2882
|
}
|
|
2801
2883
|
nextConfig.mcpServers = nextServers;
|
|
2802
2884
|
} else {
|
|
2803
|
-
const scriptPath =
|
|
2804
|
-
if (!existsSync(scriptPath)) throw new Error(`plugin MCP script not found: ${
|
|
2885
|
+
const scriptPath = resolveContainedPluginPath(root, script);
|
|
2886
|
+
if (!scriptPath || !existsSync(scriptPath)) throw new Error(`plugin MCP script not found: ${join(root, script)}`);
|
|
2805
2887
|
nextConfig.mcpServers = {
|
|
2806
2888
|
...(nextConfig.mcpServers || {}),
|
|
2807
2889
|
[serverName]: {
|
|
@@ -12,34 +12,32 @@ Practical concise — outcome-first handoffs for coding work: summarize the
|
|
|
12
12
|
result, do not narrate or explain the change.
|
|
13
13
|
|
|
14
14
|
- Open with the outcome in one sentence: done, blocked, or awaiting a decision.
|
|
15
|
-
- Summarize at the concept level: name the behavior and direction, not the
|
|
16
|
-
path
|
|
15
|
+
- Summarize at the concept level: name the behavior and direction, not the
|
|
16
|
+
code path; cite a symbol/path only as an anchor, never as the explanation.
|
|
17
17
|
- Compress by cutting content (filler, hedging, pleasantries, restated facts),
|
|
18
|
-
not
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
rendered line, whole reply ~5–7 lines. Above that you are writing Default;
|
|
18
|
+
not grammar: keep natural, complete sentences in the user's language — never
|
|
19
|
+
telegraph-style stub endings. Technical terms and code stay exact.
|
|
20
|
+
- Summarize what the change accomplishes, not a per-file changelog. Name a
|
|
21
|
+
path (`file_path:line_number`) only when the reader truly needs it to
|
|
22
|
+
navigate.
|
|
23
|
+
- Controlled detail: usually 1–3 short bullets or 2–3 sentences total; state
|
|
24
|
+
each point once — outcome or fix direction, not both. No step-by-step
|
|
25
|
+
narration or file/line inventory.
|
|
26
|
+
- Size budget: roughly HALF the Default style and TWICE Minimal — one rendered
|
|
27
|
+
line per point, whole reply ~5–7 lines. Above that you are writing Default;
|
|
29
28
|
below ~3 lines consider whether prose (Minimal) reads better.
|
|
30
29
|
- Layout: one idea per bullet, ONE line each (two only when a verbatim
|
|
31
|
-
path/error forces it)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
-
|
|
36
|
-
`남은 리스크/다음 단계`
|
|
37
|
-
|
|
30
|
+
path/error forces it), led with a short bold key phrase; blank line between
|
|
31
|
+
multi-line list items — never a dense wall of text. If a point runs past one
|
|
32
|
+
line, cut the elaboration — detail beyond key phrase + one clause belongs to
|
|
33
|
+
Default.
|
|
34
|
+
- Final handoffs may use labels like `바뀐 점`, `확인한 것`,
|
|
35
|
+
`남은 리스크/다음 단계` for Korean-facing profiles (plain English labels in
|
|
36
|
+
English threads); do not label interim progress.
|
|
38
37
|
- Synthesize agent or retrieval results; never forward raw reports, long file
|
|
39
38
|
lists, tool traces, or session metadata.
|
|
40
|
-
- Do not hide blockers, failed verification, or required follow-up
|
|
41
|
-
in one short clause.
|
|
39
|
+
- Do not hide blockers, failed verification, or required follow-up — state
|
|
40
|
+
them in one short clause; if verification was not run, say so once.
|
|
42
41
|
- Keep paths, commands, symbols, API names, code, and exact errors verbatim.
|
|
43
|
-
- Skip filler, acknowledgments, hedging, and repeated conclusions
|
|
44
|
-
verification was not run, say so once.
|
|
42
|
+
- Skip filler, acknowledgments, hedging, and repeated conclusions.
|
|
45
43
|
- Never name this style unless asked.
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
# Agent Constraints
|
|
2
2
|
|
|
3
3
|
- Use English for agent task communication.
|
|
4
|
-
-
|
|
5
|
-
|
|
6
|
-
-
|
|
7
|
-
|
|
4
|
+
- ONE TURN = ONE BATCH: emit all independent tool calls of a step in the same
|
|
5
|
+
turn; a lone read-only call that could have ridden the previous turn is a
|
|
6
|
+
wasted round-trip.
|
|
7
|
+
- NEVER PREAMBLE: no status/progress narration, "I will..." setup, or any text
|
|
8
|
+
before tool calls — call needed tools immediately; emit text only for the
|
|
9
|
+
final handoff after tool work is done.
|
|
8
10
|
- Final handoff: minimum characters, maximum information for Lead. Follow the
|
|
9
|
-
role's stricter output contract if defined; else
|
|
10
|
-
|
|
11
|
+
role's stricter output contract if defined; else fragments — outcome (1
|
|
12
|
+
line), key `file:line`(s), verification result, material risks (only if
|
|
11
13
|
any).
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- Handoff cap ~30 lines unless `Deliver:` raises it — a
|
|
16
|
-
a target.
|
|
14
|
+
- Never repeat what Lead already knows (the brief, the process, how you
|
|
15
|
+
searched); state only what changed and where to look. Verification =
|
|
16
|
+
command + result in one line. Same fact twice = delete one.
|
|
17
|
+
- Handoff cap ~30 lines unless `Deliver:` raises it — a ceiling, not a target.
|
|
17
18
|
- Banned as pure cost: report headings, markdown tables (unless requested),
|
|
18
19
|
prose narration, raw logs/tool traces, speculative next-checks, restated
|
|
19
20
|
brief, articles/politeness.
|
|
@@ -6,28 +6,29 @@ kind: retrieval
|
|
|
6
6
|
|
|
7
7
|
# Role: explorer
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
Coordinate locator. Deliverable = WHERE as `path:line`, never WHY — no
|
|
10
|
+
explaining or tracing. You ARE the `explore` tool: never call it; shared
|
|
11
|
+
rules pointing at `explore` don't apply — use grep/code_graph/find/glob
|
|
12
|
+
directly.
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
Credibility is mechanical: hit = line/path contains ANY query token or
|
|
15
|
+
obvious synonym. No other standard — "is this the real implementation" is
|
|
16
|
+
the caller's question. Sole exception: a generic-word-only match (schema,
|
|
17
|
+
handler, config, resolver…) with no SPECIFIC query token (product/library/
|
|
18
|
+
domain name) = zero, not a hit.
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
Rule zero — after EVERY tool result: ≥1 `path:line` matching a query
|
|
21
|
+
token → answer NOW with those coordinates; no verify, no re-grep, no
|
|
22
|
+
ranking; mark weak anchors `?`. Zero → one more batch if budget remains.
|
|
23
|
+
No third branch: "hits exist but I want better ones" IS branch one.
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
(
|
|
28
|
-
after all 5 turns found zero anchors — giving up early is as wrong as
|
|
29
|
-
over-searching; at turn 5 answer best-so-far.
|
|
25
|
+
Turns: 3 tool turns HARD max; start each tool message with `turn N/3`.
|
|
26
|
+
Turn 1 → answer; turns 2-3 are miss-recovery only, and must change tokens
|
|
27
|
+
OR scope — never reword. One turn = ONE batch of AT MOST 5 calls: a single
|
|
28
|
+
grep packing ALL variants in pattern[] plus code_graph/find/glob; a 6th
|
|
29
|
+
call = packing failure. Flow/how questions: return first matching entry
|
|
30
|
+
anchors (≤5); the caller traces the chain.
|
|
30
31
|
|
|
31
|
-
Answer
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
Answer, nothing else: up to 5 lines `path:line — symbol — short reason`
|
|
33
|
+
(`?` if weak), or `EXPLORATION_FAILED` — only after 3 turns of zero
|
|
34
|
+
anchors, or when every anchor matches only generic words.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# General
|
|
2
2
|
|
|
3
|
-
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
- Preambles are optional: one short sentence only when it adds user-visible
|
|
4
|
+
value; no direct names, honorifics, headings, labels, or routine lookup
|
|
5
|
+
narration.
|
|
6
6
|
- Destructive/hard-to-reverse actions require explicit confirmation.
|
|
7
|
-
- Never push, build, or deploy without an explicit user request
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
- Never push, build, or deploy without an explicit user request —
|
|
8
|
+
implementation approval is not deploy approval.
|
|
9
|
+
- Handle what you can proactively rather than deferring to the user; propose
|
|
10
|
+
only the parts needing a decision, consultatively ("Shall we proceed this
|
|
11
|
+
way?").
|
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
# Lead Brief Contract
|
|
2
2
|
|
|
3
3
|
- Brief = one-line fragments `Goal:` `Anchors:` `Allow/Forbid:` `Deliver:`
|
|
4
|
-
`Verify:` (+`Stop:` heavy-worker).
|
|
5
|
-
motivation
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
no step-by-step edits) unless the method IS the requirement.
|
|
4
|
+
`Verify:` (+`Stop:` heavy-worker). Minimum characters, maximum information:
|
|
5
|
+
no background/motivation, no role-known rules; never restate prior context
|
|
6
|
+
or the same fact twice. A brief that feels long is a scope problem — split
|
|
7
|
+
the scope, don't pad.
|
|
8
|
+
- Anchors = `file:line` + one-line conclusion each; never paste log/code
|
|
9
|
+
bodies — the agent reads the file itself.
|
|
10
|
+
- Instruct by outcome ("make X behave Y"), not method (no code snippets, no
|
|
11
|
+
step-by-step edits) unless the method IS the requirement.
|
|
13
12
|
- `Deliver:` states output size/shape (e.g. "fragments <=15 lines", "verdict
|
|
14
|
-
+ top-3 risks", "detail to file, path only")
|
|
13
|
+
+ top-3 risks", "detail to file, path only"); never request a long report
|
|
15
14
|
in the handoff itself.
|
|
16
15
|
- Full brief only on fresh spawn or `respawned: true` (dead-tag send = cold
|
|
17
|
-
session; re-supply anchors)
|
|
18
|
-
|
|
16
|
+
session; re-supply anchors); live-session follow-ups = delta only, never
|
|
17
|
+
restating Goal/rules.
|
|
19
18
|
- Never `send` mid-run; batch all adjustments into ONE follow-up after
|
|
20
19
|
completion. Interrupt only to cancel.
|
|
21
20
|
- All agent communication in English.
|
|
@@ -4,18 +4,29 @@
|
|
|
4
4
|
prior result — including edit loops: batch the post-edit verification read
|
|
5
5
|
with the next target's lookup.
|
|
6
6
|
- Pick by target: symbols/callers/deps → `code_graph`; exact text in a verified
|
|
7
|
-
scope → `grep`;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
7
|
+
scope → `grep`; dirs → `list`; verified file → `read`.
|
|
8
|
+
Locating files: concept-level or cross-file questions ("where is X handled",
|
|
9
|
+
"how does Y flow") → `explore`, even when you could guess a keyword — its
|
|
10
|
+
fan-out spends explorer context, not yours, and guess-and-retry keywords
|
|
11
|
+
belong to it, not grep. Confident name fragment → `find`; exact name
|
|
12
|
+
pattern → `glob`; exact content literal → `grep`. Flow/trace with a known
|
|
13
|
+
symbol → `code_graph`, never `explore` — it returns entry-point anchors
|
|
14
|
+
only; follow the chain yourself. Never `grep`/`read` guessed paths.
|
|
15
|
+
- One concept → ONE search call: all synonyms/variants in one
|
|
16
|
+
`grep pattern:[...]` (or `code_graph symbols[]`), scopes as `path[]`;
|
|
17
|
+
single-pattern bursts = packing failure. A second call on the same concept
|
|
18
|
+
is a defect unless the first returned zero — then change tokens or scope,
|
|
19
|
+
never reword.
|
|
20
|
+
- Content-mode `grep` always carries `-C 5`+ ; re-reading the same file after
|
|
21
|
+
a content grep is a wasted round-trip. Bare match lines = existence checks
|
|
22
|
+
only.
|
|
23
|
+
- Any path not returned by a tool this session is a guess: find first, then
|
|
24
|
+
grep/read the returned path. On ENOENT the next call is find on the
|
|
25
|
+
basename — never a second guess or the same path with changed args.
|
|
26
|
+
- Retrieval serves the NEXT action (edit, answer, handoff), not certainty:
|
|
27
|
+
the FIRST anchor ends the search — no verifying, ranking, or re-checking a
|
|
28
|
+
hit already on screen. When acting and looking are both possible, act.
|
|
29
|
+
- `read` uses `offset`/`limit` only; 2+ spans from known file(s) = ONE batched
|
|
30
|
+
`read` with `{path,offset,limit}` regions. Adjacent spans in one file
|
|
31
|
+
(within a few hundred lines) are ONE window, not repeated reads.
|
|
21
32
|
- Don't mix `apply_patch` with shell or other state-changing calls in one turn.
|
|
@@ -207,12 +207,13 @@ function _redactLogText(text) {
|
|
|
207
207
|
function classifyToolFailure(resultText, toolName) {
|
|
208
208
|
const text = String(resultText ?? '').toLowerCase();
|
|
209
209
|
if (/requires either|invalid arguments|unknown parameter|must be|schema|expected|required|old_string is .*>=/.test(text)) return 'schema/args';
|
|
210
|
-
if (/
|
|
210
|
+
if (/not in allow-list|not allowed/.test(text)) return 'permission';
|
|
211
|
+
if (String(toolName || '') === 'shell' || /^\s*\[exit code:\s*\d+\]/i.test(String(resultText ?? ''))) return 'command-exit';
|
|
212
|
+
if (/enoent|cannot find|not found at this path|path does not exist|no such file|file not found in graph|unreadable/.test(text)) return 'path/enoent';
|
|
211
213
|
if (/timed out|timeout|interrupted|aborted/.test(text)) return 'timeout/abort';
|
|
212
214
|
if (/hunk rejected|patch failed|context mismatch|expected first old\/context|context not found/.test(text)) return 'patch/context';
|
|
213
|
-
if (/
|
|
215
|
+
if (/permission|denied|forbidden/.test(text)) return 'permission';
|
|
214
216
|
if (/unknown tool|tool.*not.*available|missing.*tool/.test(text)) return 'tool-surface';
|
|
215
|
-
if (String(toolName || '') === 'shell' || /^\s*\[exit code:\s*\d+\]/i.test(String(resultText ?? ''))) return 'command-exit';
|
|
216
217
|
return 'runtime/failure';
|
|
217
218
|
}
|
|
218
219
|
|
|
@@ -149,7 +149,7 @@ function buildDefaultConfig(options = {}) {
|
|
|
149
149
|
const providers = {};
|
|
150
150
|
// API providers — enabled if env key exists
|
|
151
151
|
for (const [name, envKey] of Object.entries(AGENT_PROVIDER_ENV)) {
|
|
152
|
-
const apiKey = process.env[envKey];
|
|
152
|
+
const apiKey = detectCredentials ? process.env[envKey] : undefined;
|
|
153
153
|
providers[name] = {
|
|
154
154
|
enabled: !!apiKey,
|
|
155
155
|
apiKey: apiKey || undefined,
|
|
@@ -43,6 +43,15 @@ function parseClaudeVersion(model) {
|
|
|
43
43
|
if (pair) {
|
|
44
44
|
return { family: pair[1], major: Number(pair[2]), minor: null };
|
|
45
45
|
}
|
|
46
|
+
// Legacy naming: version comes BEFORE the family (claude-3-7-sonnet,
|
|
47
|
+
// claude-3-5-sonnet, claude-3-opus). These are manual-thinking models —
|
|
48
|
+
// parse them so isLegacyAnthropicReasoningModel excludes them instead of
|
|
49
|
+
// letting modelSupportsEffort's `startsWith('claude-')` fallthrough grant
|
|
50
|
+
// them adaptive thinking + the effort beta (a hard 400).
|
|
51
|
+
const legacy = m.match(/^claude-(\d+)-(\d+)-(haiku|sonnet|opus)/);
|
|
52
|
+
if (legacy) {
|
|
53
|
+
return { family: legacy[3], major: Number(legacy[1]), minor: Number(legacy[2]) };
|
|
54
|
+
}
|
|
46
55
|
return null;
|
|
47
56
|
}
|
|
48
57
|
|
|
@@ -69,7 +78,17 @@ export function modelSupportsEffort(model) {
|
|
|
69
78
|
if (m.includes('sonnet-5') || m.includes('fable-5')) return true;
|
|
70
79
|
if (/^claude-opus-4-(6|7|8)(?:$|[-@])/.test(m)) return true;
|
|
71
80
|
if (/^claude-opus-5-/.test(m)) return true;
|
|
72
|
-
|
|
81
|
+
// Fallthrough for not-yet-enumerated modern models: only grant effort when
|
|
82
|
+
// parseClaudeVersion resolves a family with major>=4 (and not a manual-
|
|
83
|
+
// thinking legacy already excluded above). A bare `startsWith('claude-')`
|
|
84
|
+
// here would hand thinking:adaptive + the effort beta to legacy/unknown
|
|
85
|
+
// IDs (e.g. claude-3-7-sonnet) → hard 400. Unknown-shape IDs (no version
|
|
86
|
+
// parsed) fall through to false: no effort, no adaptive thinking.
|
|
87
|
+
const parsed = parseClaudeVersion(model);
|
|
88
|
+
if (parsed && (parsed.family === 'sonnet' || parsed.family === 'opus' || parsed.family === 'fable')
|
|
89
|
+
&& parsed.major >= 4) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
73
92
|
return false;
|
|
74
93
|
}
|
|
75
94
|
|
|
@@ -180,6 +199,19 @@ export function applyAnthropicEffortToBody(
|
|
|
180
199
|
: {};
|
|
181
200
|
body.output_config = { ...existing, effort: normalized };
|
|
182
201
|
}
|
|
202
|
+
// Adaptive-thinking models (4.6+) require `thinking:{type:"adaptive"}`
|
|
203
|
+
// rather than the legacy budget_tokens shape — sending
|
|
204
|
+
// `thinking:{type:"enabled"}` here 400s on sonnet-5/opus-4-7/4-8.
|
|
205
|
+
// display:"summarized" keeps reasoning blocks populated (4.7+ defaults
|
|
206
|
+
// to "omitted", silently hiding reasoning text). Gated on the same
|
|
207
|
+
// modelSupportsEffort() allowlist so older models never receive it.
|
|
208
|
+
// Set unconditionally (independent of `normalized`) so effort-capable
|
|
209
|
+
// turns always carry adaptive thinking + round-trip signatures.
|
|
210
|
+
body.thinking = { type: 'adaptive', display: 'summarized' };
|
|
211
|
+
// Adaptive/4.7+ models reject any non-default sampling param with a 400.
|
|
212
|
+
delete body.temperature;
|
|
213
|
+
delete body.top_p;
|
|
214
|
+
delete body.top_k;
|
|
183
215
|
return;
|
|
184
216
|
}
|
|
185
217
|
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
resolveAnthropicModelAfter404,
|
|
25
25
|
ensureLatestAnthropicModel,
|
|
26
26
|
} from './anthropic-model-resolve.mjs';
|
|
27
|
-
import { sanitizeToolPairs, sanitizeAnthropicContentPairs } from '../session/context-utils.mjs';
|
|
27
|
+
import { sanitizeToolPairs, sanitizeAnthropicContentPairs, foldUserTextIntoToolResultTail } from '../session/context-utils.mjs';
|
|
28
28
|
import {
|
|
29
29
|
TOKEN_REFRESH_SKEW_MS,
|
|
30
30
|
resolveCliVersion,
|
|
@@ -303,6 +303,11 @@ function nativeAnthropicTools(opts) {
|
|
|
303
303
|
}
|
|
304
304
|
function deferredAnthropicTools(activeTools, opts) {
|
|
305
305
|
if (opts?.session?.deferredNativeTools !== true) return [];
|
|
306
|
+
// A request whose ONLY tools are deferred is rejected by the API with
|
|
307
|
+
// `400: At least one tool must have defer_loading=false` — happens on the
|
|
308
|
+
// iteration-cap final turn (loop sends tools: [] to force a text answer).
|
|
309
|
+
// No active tools ⇒ send no deferred catalog either.
|
|
310
|
+
if (!Array.isArray(activeTools) || activeTools.length === 0) return [];
|
|
306
311
|
const active = new Set((activeTools || []).map((tool) => String(tool?.name || '').trim()).filter(Boolean));
|
|
307
312
|
const catalog = Array.isArray(opts.session.deferredToolCatalog) ? opts.session.deferredToolCatalog : [];
|
|
308
313
|
return catalog
|
|
@@ -328,6 +333,15 @@ function toAnthropicMessages(messages) {
|
|
|
328
333
|
content = m.assistantBlocks.slice();
|
|
329
334
|
} else {
|
|
330
335
|
content = [];
|
|
336
|
+
// Adaptive-thinking round-trip: prior-turn thinking blocks are
|
|
337
|
+
// REQUIRED back, unmodified (signature intact; empty thinking
|
|
338
|
+
// field allowed), and MUST precede tool_use blocks. Emit them
|
|
339
|
+
// first, verbatim as received from the SSE parser.
|
|
340
|
+
if (Array.isArray(m.thinkingBlocks)) {
|
|
341
|
+
for (const tb of m.thinkingBlocks) {
|
|
342
|
+
if (tb && typeof tb === 'object') content.push(tb);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
331
345
|
if (m.content) content.push({ type: 'text', text: m.content });
|
|
332
346
|
for (const tc of m.toolCalls) {
|
|
333
347
|
content.push({
|
|
@@ -362,6 +376,14 @@ function toAnthropicMessages(messages) {
|
|
|
362
376
|
continue;
|
|
363
377
|
}
|
|
364
378
|
|
|
379
|
+
// Claude Code parity: fold a user text turn that directly follows a
|
|
380
|
+
// tool_result turn into that tool_result's content. A sibling text
|
|
381
|
+
// turn after tool_result renders as `</function_results>\n\nHuman:`
|
|
382
|
+
// on the wire and trains the model toward 3-token empty end_turn
|
|
383
|
+
// completions (see foldUserTextIntoToolResultTail).
|
|
384
|
+
if (m.role === 'user' && foldUserTextIntoToolResultTail(result, normalizeContentForAnthropic(m.content))) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
365
387
|
result.push({ role: m.role, content: normalizeContentForAnthropic(m.content) });
|
|
366
388
|
}
|
|
367
389
|
return sanitizeAnthropicContentPairs(result);
|
|
@@ -1055,6 +1077,14 @@ export class AnthropicOAuthProvider {
|
|
|
1055
1077
|
if (attemptIndex > 0 && firstAttemptError) {
|
|
1056
1078
|
try { firstAttemptError.midstreamRetries = attemptIndex; } catch {}
|
|
1057
1079
|
try { firstAttemptError.midstreamClassifier = firstAttemptClassifier; } catch {}
|
|
1080
|
+
// firstAttemptError is what actually propagates here when
|
|
1081
|
+
// live text was emitted this attempt — stamp the unsafe
|
|
1082
|
+
// flags onto IT too, else upstream sees an unmarked error
|
|
1083
|
+
// and retries, duplicating already-streamed output.
|
|
1084
|
+
try {
|
|
1085
|
+
firstAttemptError.liveTextEmitted = true;
|
|
1086
|
+
firstAttemptError.unsafeToRetry = true;
|
|
1087
|
+
} catch {}
|
|
1058
1088
|
throw firstAttemptError;
|
|
1059
1089
|
}
|
|
1060
1090
|
throw err;
|