mixdog 0.9.2 → 0.9.3
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/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/build-tui.mjs +13 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/tool-smoke.mjs +7 -4
- package/src/agents/debugger/AGENT.md +2 -2
- package/src/agents/heavy-worker/AGENT.md +20 -11
- package/src/agents/reviewer/AGENT.md +2 -2
- package/src/agents/worker/AGENT.md +17 -11
- package/src/mixdog-session-runtime.mjs +424 -1812
- package/src/repl.mjs +5 -5
- package/src/rules/agent/30-explorer.md +8 -11
- package/src/rules/lead/lead-tool.md +9 -5
- package/src/rules/shared/01-tool.md +11 -5
- package/src/runtime/agent/orchestrator/context/collect.mjs +51 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +22 -68
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +46 -7
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +1 -13
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +19 -12
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +7 -5
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +33 -23
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +31 -14
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +38 -12
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +7 -8
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +169 -918
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +65 -1032
- package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +36 -4277
- package/src/runtime/agent/orchestrator/tools/patch.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/channels/index.mjs +14 -233
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +8 -0
- package/src/runtime/channels/lib/telegram-format.mjs +19 -22
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/memory/index.mjs +314 -359
- package/src/runtime/memory/lib/core-memory-store.mjs +351 -1
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +56 -3
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +20 -0
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/recall-format.mjs +183 -0
- package/src/runtime/memory/tool-defs.mjs +4 -4
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- package/src/runtime/shared/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/transcript-writer.mjs +29 -2
- package/src/session-runtime/config-helpers.mjs +209 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/output-styles.mjs +124 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool.mjs +49 -10
- package/src/standalone/channel-worker.mjs +3 -2
- package/src/standalone/explore-tool.mjs +10 -3
- package/src/standalone/hook-bus.mjs +165 -8
- package/src/standalone/opencode-go-login.mjs +121 -0
- package/src/standalone/provider-admin.mjs +14 -3
- package/src/tui/App.jsx +884 -143
- package/src/tui/components/PromptInput.jsx +272 -15
- package/src/tui/components/ToolExecution.jsx +20 -10
- package/src/tui/components/tool-output-format.mjs +2 -2
- package/src/tui/dist/index.mjs +1771 -1947
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/boot-profile.mjs +21 -0
- package/src/tui/engine/labels.mjs +67 -0
- package/src/tui/engine/notice-text.mjs +112 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +311 -851
- package/src/tui/input-editing.mjs +58 -8
- package/src/tui/input-editing.selection.test.mjs +75 -0
- package/src/tui/keyboard-protocol.mjs +2 -2
- package/src/tui/lib/voice-recorder.mjs +35 -19
- package/src/tui/markdown/format-token.mjs +7 -8
- package/src/tui/markdown/format-token.test.mjs +3 -3
- package/src/tui/paste-attachments.mjs +38 -0
- package/src/tui/paste-fix.test.mjs +119 -0
- package/src/tui/themes/base.mjs +2 -2
- package/src/tui/themes/kanagawa.mjs +4 -4
- package/src/tui/themes/teal.mjs +4 -5
- package/src/tui/themes/utils.mjs +1 -1
- package/src/ui/statusline.mjs +49 -0
- package/src/workflows/default/WORKFLOW.md +16 -9
- package/src/workflows/sequential/WORKFLOW.md +16 -11
- package/src/workflows/solo/WORKFLOW.md +5 -1
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
// Workflow/agent pack loading + route resolution, and search-route
|
|
2
|
+
// normalization. Roots/dataDir and config-dependent helpers are injected to
|
|
3
|
+
// keep this module free of the runtime's path/provider constants.
|
|
4
|
+
import { basename, join } from 'node:path';
|
|
5
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
6
|
+
import { clean } from './session-text.mjs';
|
|
7
|
+
import { normalizeEffortInput } from './effort.mjs';
|
|
8
|
+
import { isLikelyRawModelId } from './config-helpers.mjs';
|
|
9
|
+
import { readTextSafe, readJsonSafe } from './fs-utils.mjs';
|
|
10
|
+
|
|
11
|
+
export const WORKFLOW_ROUTE_SLOTS = ['lead', 'agent', 'explorer', 'memory'];
|
|
12
|
+
export const FIXED_AGENT_SLOTS = Object.freeze([
|
|
13
|
+
{ id: 'explore', label: 'Explore', description: 'Broad repository exploration', workflowSlot: 'explorer' },
|
|
14
|
+
{ id: 'maintainer', label: 'Maintainer', description: 'Background memory and upkeep', workflowSlot: 'memory' },
|
|
15
|
+
{ id: 'worker', label: 'Worker', description: 'Scoped implementation' },
|
|
16
|
+
{ id: 'heavy-worker', label: 'Heavy Worker', description: 'Broad or multi-file implementation' },
|
|
17
|
+
{ id: 'reviewer', label: 'Reviewer', description: 'Diff review and risk checks' },
|
|
18
|
+
{ id: 'debugger', label: 'Debugger', description: 'Root-cause analysis and failure tracing' },
|
|
19
|
+
]);
|
|
20
|
+
const AGENT_ROLE_IDS = new Set(FIXED_AGENT_SLOTS.map((agent) => agent.id));
|
|
21
|
+
const DEFAULT_WORKFLOW_ID = 'default';
|
|
22
|
+
|
|
23
|
+
const SEARCH_CAPABLE_PROVIDERS = new Set([
|
|
24
|
+
'openai-oauth', 'openai', 'grok-oauth', 'xai', 'gemini', 'anthropic', 'anthropic-oauth',
|
|
25
|
+
]);
|
|
26
|
+
export const SEARCH_DEFAULT_PROVIDER = 'default';
|
|
27
|
+
export const SEARCH_DEFAULT_MODEL = 'default';
|
|
28
|
+
const SEARCH_PROVIDER_ALIASES = Object.freeze({
|
|
29
|
+
'openai-api': 'openai',
|
|
30
|
+
'xai-api': 'xai',
|
|
31
|
+
'gemini-api': 'gemini',
|
|
32
|
+
'anthropic-api': 'anthropic',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const agentDefinitionCache = new Map();
|
|
36
|
+
const AGENT_DEFINITION_CACHE_LIMIT = 64;
|
|
37
|
+
function setAgentDefinitionCache(key, value) {
|
|
38
|
+
if (!agentDefinitionCache.has(key) && agentDefinitionCache.size >= AGENT_DEFINITION_CACHE_LIMIT) {
|
|
39
|
+
const oldestKey = agentDefinitionCache.keys().next().value;
|
|
40
|
+
agentDefinitionCache.delete(oldestKey);
|
|
41
|
+
}
|
|
42
|
+
agentDefinitionCache.set(key, value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function workflowPresetId(slot) {
|
|
46
|
+
return `workflow-${slot}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function workflowPresetName(slot) {
|
|
50
|
+
return `WORKFLOW ${String(slot || '').toUpperCase()}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function agentPresetSlot(agentId) {
|
|
54
|
+
return `agent-${String(agentId || '').replace(/[^a-z0-9_.-]+/gi, '-').toLowerCase()}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function normalizeAgentId(value) {
|
|
58
|
+
const id = clean(value).toLowerCase().replace(/[\s_]+/g, '-');
|
|
59
|
+
if (id === 'explorer') return 'explore';
|
|
60
|
+
if (id === 'maint' || id === 'maintenance' || id === 'memory') return 'maintainer';
|
|
61
|
+
if (id === 'heavy' || id === 'heavyworker') return 'heavy-worker';
|
|
62
|
+
if (id === 'review') return 'reviewer';
|
|
63
|
+
if (id === 'debug') return 'debugger';
|
|
64
|
+
return AGENT_ROLE_IDS.has(id) ? id : '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function normalizeWorkflowId(value, fallback = '') {
|
|
68
|
+
const id = clean(value).toLowerCase().replace(/[\s_]+/g, '-');
|
|
69
|
+
return /^[a-z0-9][a-z0-9_.-]*$/.test(id) ? id : fallback;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// A workflow/agent pack loader is created per data/root layout via
|
|
73
|
+
// createWorkflowHelpers, and the config-aware route helpers via
|
|
74
|
+
// createWorkflowRouteHelpers.
|
|
75
|
+
export function createWorkflowHelpers({ rootDir, dataDir, readMarkdownDocument, normalizeAgentPermissionOrNone }) {
|
|
76
|
+
function workflowSourceDirs(dir) {
|
|
77
|
+
return [
|
|
78
|
+
{ root: join(rootDir, 'workflows'), source: 'built-in' },
|
|
79
|
+
{ root: join(dir || dataDir, 'workflows'), source: 'user' },
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function agentSourceDirs(dir, id) {
|
|
84
|
+
return [
|
|
85
|
+
join(dir || dataDir, 'agents', id),
|
|
86
|
+
join(rootDir, 'agents', id),
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readWorkflowPackFromDir(dir, source = 'built-in', dirName = '') {
|
|
91
|
+
const entry = 'WORKFLOW.md';
|
|
92
|
+
const doc = readMarkdownDocument(readTextSafe(join(dir, entry)));
|
|
93
|
+
const body = doc.body;
|
|
94
|
+
if (!body) return null;
|
|
95
|
+
const fm = doc.frontmatter || {};
|
|
96
|
+
const id = normalizeWorkflowId(clean(fm.id) || dirName || basename(dir));
|
|
97
|
+
if (!id) return null;
|
|
98
|
+
const agentsConfigured = Object.prototype.hasOwnProperty.call(fm, 'agents');
|
|
99
|
+
return {
|
|
100
|
+
id,
|
|
101
|
+
name: clean(fm.name) || id,
|
|
102
|
+
description: clean(fm.description),
|
|
103
|
+
entry,
|
|
104
|
+
agentsConfigured,
|
|
105
|
+
agents: agentsConfigured
|
|
106
|
+
? String(fm.agents || '')
|
|
107
|
+
.split(',')
|
|
108
|
+
.map((agent) => normalizeAgentId(agent) || normalizeWorkflowId(agent))
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
: [],
|
|
111
|
+
body,
|
|
112
|
+
source,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function listWorkflowPacks(dir) {
|
|
117
|
+
const byId = new Map();
|
|
118
|
+
for (const { root, source } of workflowSourceDirs(dir)) {
|
|
119
|
+
if (!existsSync(root)) continue;
|
|
120
|
+
let entries = [];
|
|
121
|
+
try { entries = readdirSync(root, { withFileTypes: true }); } catch { entries = []; }
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
if (!entry.isDirectory()) continue;
|
|
124
|
+
const d = join(root, entry.name);
|
|
125
|
+
if (!existsSync(join(d, 'WORKFLOW.md'))) continue;
|
|
126
|
+
const pack = readWorkflowPackFromDir(d, source, entry.name);
|
|
127
|
+
if (pack) byId.set(pack.id, pack);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return [...byId.values()].sort((a, b) => {
|
|
131
|
+
if (a.id === DEFAULT_WORKFLOW_ID) return -1;
|
|
132
|
+
if (b.id === DEFAULT_WORKFLOW_ID) return 1;
|
|
133
|
+
return a.name.localeCompare(b.name);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function activeWorkflowId(config) {
|
|
138
|
+
return normalizeWorkflowId(config?.workflow?.active, DEFAULT_WORKFLOW_ID);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function loadWorkflowPack(dir, id) {
|
|
142
|
+
const wanted = normalizeWorkflowId(id, DEFAULT_WORKFLOW_ID);
|
|
143
|
+
for (const { root, source } of workflowSourceDirs(dir).reverse()) {
|
|
144
|
+
const pack = readWorkflowPackFromDir(join(root, wanted), source, wanted);
|
|
145
|
+
if (pack) return pack;
|
|
146
|
+
}
|
|
147
|
+
return readWorkflowPackFromDir(join(rootDir, 'workflows', DEFAULT_WORKFLOW_ID), 'built-in', DEFAULT_WORKFLOW_ID);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function workflowSummary(pack) {
|
|
151
|
+
const id = normalizeWorkflowId(pack?.id, DEFAULT_WORKFLOW_ID);
|
|
152
|
+
return {
|
|
153
|
+
id,
|
|
154
|
+
name: clean(pack?.name) || (id === DEFAULT_WORKFLOW_ID ? 'Default' : id),
|
|
155
|
+
description: clean(pack?.description),
|
|
156
|
+
source: clean(pack?.source),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function activeWorkflowSummary(config, dir) {
|
|
161
|
+
return workflowSummary(loadWorkflowPack(dir, activeWorkflowId(config)));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function loadAgentDefinition(dir, id) {
|
|
165
|
+
const agentId = normalizeAgentId(id) || normalizeWorkflowId(id);
|
|
166
|
+
if (!agentId) return null;
|
|
167
|
+
const cacheKey = `${dir || dataDir}\n${agentId}`;
|
|
168
|
+
if (agentDefinitionCache.has(cacheKey)) return agentDefinitionCache.get(cacheKey);
|
|
169
|
+
for (const d of agentSourceDirs(dir, agentId)) {
|
|
170
|
+
const manifest = readJsonSafe(join(d, 'agent.json')) || {};
|
|
171
|
+
const entry = clean(manifest.entry) || 'AGENT.md';
|
|
172
|
+
const doc = readMarkdownDocument(readTextSafe(join(d, entry)));
|
|
173
|
+
const body = doc.body;
|
|
174
|
+
if (!body) continue;
|
|
175
|
+
const definition = {
|
|
176
|
+
id: agentId,
|
|
177
|
+
name: clean(manifest.name) || FIXED_AGENT_SLOTS.find((agent) => agent.id === agentId)?.label || agentId,
|
|
178
|
+
description: clean(manifest.description) || FIXED_AGENT_SLOTS.find((agent) => agent.id === agentId)?.description || '',
|
|
179
|
+
permission: normalizeAgentPermissionOrNone(doc.frontmatter.permission),
|
|
180
|
+
frontmatter: doc.frontmatter,
|
|
181
|
+
body,
|
|
182
|
+
};
|
|
183
|
+
setAgentDefinitionCache(cacheKey, definition);
|
|
184
|
+
return definition;
|
|
185
|
+
}
|
|
186
|
+
const legacyDoc = readMarkdownDocument(readTextSafe(join(rootDir, 'agents', `${agentId}.md`)));
|
|
187
|
+
if (!legacyDoc.body) {
|
|
188
|
+
setAgentDefinitionCache(cacheKey, null);
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
const definition = {
|
|
192
|
+
id: agentId,
|
|
193
|
+
name: FIXED_AGENT_SLOTS.find((agent) => agent.id === agentId)?.label || agentId,
|
|
194
|
+
description: '',
|
|
195
|
+
permission: normalizeAgentPermissionOrNone(legacyDoc.frontmatter.permission),
|
|
196
|
+
frontmatter: legacyDoc.frontmatter,
|
|
197
|
+
body: legacyDoc.body,
|
|
198
|
+
};
|
|
199
|
+
setAgentDefinitionCache(cacheKey, definition);
|
|
200
|
+
return definition;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function workflowContextBlock(config, dir) {
|
|
204
|
+
const pack = loadWorkflowPack(dir, activeWorkflowId(config));
|
|
205
|
+
if (!pack) return '';
|
|
206
|
+
const lines = [`# Active Workflow: ${pack.name}`];
|
|
207
|
+
if (pack.description) lines.push(pack.description);
|
|
208
|
+
lines.push(pack.body);
|
|
209
|
+
const agentIds = pack.agentsConfigured ? pack.agents : FIXED_AGENT_SLOTS.map((agent) => agent.id);
|
|
210
|
+
const agentBlocks = agentIds.map((id) => loadAgentDefinition(dir, id)).filter(Boolean);
|
|
211
|
+
if (agentBlocks.length) {
|
|
212
|
+
lines.push('# Available Agents');
|
|
213
|
+
for (const agent of agentBlocks) {
|
|
214
|
+
lines.push(`## ${agent.name} (${agent.id})`);
|
|
215
|
+
if (agent.description) lines.push(agent.description);
|
|
216
|
+
lines.push(agent.body);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return lines.join('\n\n');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
listWorkflowPacks,
|
|
224
|
+
activeWorkflowId,
|
|
225
|
+
loadWorkflowPack,
|
|
226
|
+
workflowSummary,
|
|
227
|
+
activeWorkflowSummary,
|
|
228
|
+
loadAgentDefinition,
|
|
229
|
+
workflowContextBlock,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function normalizeSearchProviderId(provider) {
|
|
234
|
+
const id = clean(provider);
|
|
235
|
+
return SEARCH_PROVIDER_ALIASES[id] || id;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function isDefaultSearchRouteConfig(routeLike = {}) {
|
|
239
|
+
return normalizeSearchProviderId(routeLike?.provider) === SEARCH_DEFAULT_PROVIDER
|
|
240
|
+
&& clean(routeLike?.model).toLowerCase() === SEARCH_DEFAULT_MODEL;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function isSearchCapableProvider(provider) {
|
|
244
|
+
return SEARCH_CAPABLE_PROVIDERS.has(normalizeSearchProviderId(provider));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function normalizeSearchRouteConfig(routeLike, fallback = {}) {
|
|
248
|
+
const provider = normalizeSearchProviderId(routeLike?.provider || fallback.provider);
|
|
249
|
+
const model = clean(routeLike?.model || fallback.model);
|
|
250
|
+
if (!provider || !model) return null;
|
|
251
|
+
let effort = null;
|
|
252
|
+
try {
|
|
253
|
+
effort = normalizeEffortInput(routeLike?.effort ?? fallback.effort);
|
|
254
|
+
} catch {
|
|
255
|
+
effort = null;
|
|
256
|
+
}
|
|
257
|
+
const fast = routeLike?.fast ?? fallback.fast;
|
|
258
|
+
const toolType = clean(routeLike?.toolType || fallback.toolType);
|
|
259
|
+
return {
|
|
260
|
+
provider,
|
|
261
|
+
model,
|
|
262
|
+
...(effort ? { effort } : {}),
|
|
263
|
+
...(fast === true ? { fast: true } : {}),
|
|
264
|
+
...(toolType ? { toolType } : {}),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function normalizeWorkflowRoute(routeLike, fallback = {}) {
|
|
269
|
+
const provider = clean(routeLike?.provider) || clean(fallback.provider);
|
|
270
|
+
const model = clean(routeLike?.model) || clean(fallback.model);
|
|
271
|
+
if (!provider || !model) return null;
|
|
272
|
+
// Defensive: a workflow/agent route must carry a real model id. Reject values
|
|
273
|
+
// that are obviously free-form text (whitespace, prose) so a bad string can
|
|
274
|
+
// never be persisted as a preset/workflow route.
|
|
275
|
+
if (!isLikelyRawModelId(model)) return null;
|
|
276
|
+
const effort = normalizeEffortInput(routeLike?.effort ?? fallback.effort);
|
|
277
|
+
const fast = routeLike?.fast ?? fallback.fast;
|
|
278
|
+
return {
|
|
279
|
+
provider,
|
|
280
|
+
model,
|
|
281
|
+
...(effort ? { effort } : {}),
|
|
282
|
+
...(fast === true ? { fast: true } : {}),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function upsertWorkflowPreset(presets, slot, routeLike) {
|
|
287
|
+
const route = normalizeWorkflowRoute(routeLike);
|
|
288
|
+
if (!route) return presets;
|
|
289
|
+
const id = workflowPresetId(slot);
|
|
290
|
+
const preset = {
|
|
291
|
+
id,
|
|
292
|
+
name: workflowPresetName(slot),
|
|
293
|
+
type: 'agent',
|
|
294
|
+
provider: route.provider,
|
|
295
|
+
model: route.model,
|
|
296
|
+
...(route.effort ? { effort: route.effort } : {}),
|
|
297
|
+
...(route.fast === true ? { fast: true } : {}),
|
|
298
|
+
tools: 'full',
|
|
299
|
+
};
|
|
300
|
+
const next = (Array.isArray(presets) ? presets : []).filter((p) => clean(p?.id) !== id && clean(p?.name) !== preset.name);
|
|
301
|
+
next.push(preset);
|
|
302
|
+
return next;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Config-aware route resolvers need resolveDefaultProvider + findPreset from
|
|
306
|
+
// the runtime; created via this factory.
|
|
307
|
+
export function createWorkflowRouteHelpers({ resolveDefaultProvider, findPreset }) {
|
|
308
|
+
function summarizeWorkflowRoutes(config) {
|
|
309
|
+
const routes = config?.workflowRoutes && typeof config.workflowRoutes === 'object' ? config.workflowRoutes : {};
|
|
310
|
+
const fallbackProvider = resolveDefaultProvider(config);
|
|
311
|
+
const out = {};
|
|
312
|
+
for (const slot of WORKFLOW_ROUTE_SLOTS) {
|
|
313
|
+
const route = routes[slot];
|
|
314
|
+
// Read/interpret path: a route with a model but no provider falls back to
|
|
315
|
+
// config.defaultProvider (then DEFAULT_PROVIDER).
|
|
316
|
+
if (route?.model && (route?.provider || fallbackProvider)) {
|
|
317
|
+
out[slot] = normalizeWorkflowRoute(route, { provider: fallbackProvider });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return out;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function routeFromPreset(config, slotValue) {
|
|
324
|
+
// Maintenance slots now store a direct {provider, model} route. Accept that
|
|
325
|
+
// shape first; fall back to the legacy preset-NAME string lookup so configs
|
|
326
|
+
// written before the route migration still resolve.
|
|
327
|
+
if (slotValue && typeof slotValue === 'object' && !Array.isArray(slotValue)) {
|
|
328
|
+
const direct = normalizeWorkflowRoute(slotValue);
|
|
329
|
+
if (direct) return direct;
|
|
330
|
+
}
|
|
331
|
+
const preset = findPreset(config, slotValue);
|
|
332
|
+
return preset ? normalizeWorkflowRoute(preset) : null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function agentRouteFromConfig(config, agentId, _dataDir) {
|
|
336
|
+
const id = normalizeAgentId(agentId);
|
|
337
|
+
if (!id) return null;
|
|
338
|
+
// Read/interpret path: inject config.defaultProvider (then DEFAULT_PROVIDER)
|
|
339
|
+
// when a stored route omits its provider.
|
|
340
|
+
const fallback = { provider: resolveDefaultProvider(config) };
|
|
341
|
+
const explicit = normalizeWorkflowRoute(config?.agents?.[id], fallback)
|
|
342
|
+
|| (id === 'maintainer' ? normalizeWorkflowRoute(config?.agents?.maintenance, fallback) : null);
|
|
343
|
+
if (explicit) return explicit;
|
|
344
|
+
|
|
345
|
+
const agent = FIXED_AGENT_SLOTS.find((item) => item.id === id);
|
|
346
|
+
if (agent?.workflowSlot) {
|
|
347
|
+
const workflowRoute = normalizeWorkflowRoute(config?.workflowRoutes?.[agent.workflowSlot], fallback);
|
|
348
|
+
if (workflowRoute) return workflowRoute;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (id === 'explore') return routeFromPreset(config, config?.maintenance?.explore);
|
|
352
|
+
if (id === 'maintainer') return routeFromPreset(config, config?.maintenance?.memory);
|
|
353
|
+
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return { summarizeWorkflowRoutes, routeFromPreset, agentRouteFromConfig };
|
|
358
|
+
}
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
taskIdFromArgs,
|
|
15
15
|
} from '../runtime/shared/background-tasks.mjs';
|
|
16
16
|
import { modelVisibleToolCompletionMessage } from '../runtime/shared/tool-execution-contract.mjs';
|
|
17
|
-
import { presentErrorText } from '../runtime/shared/err-text.mjs';
|
|
17
|
+
import { presentErrorText, errorLine } from '../runtime/shared/err-text.mjs';
|
|
18
18
|
import { updateJsonAtomicSync } from '../runtime/shared/atomic-file.mjs';
|
|
19
19
|
import {
|
|
20
20
|
normalizeAgentPermission,
|
|
@@ -135,7 +135,11 @@ function envTimeoutMs(name, fallback) {
|
|
|
135
135
|
// Grace window during which a terminated/idle worker row is kept around so the
|
|
136
136
|
// same terminal can re-use (or cleanly re-spawn) the same tag. Cached as a
|
|
137
137
|
// constant like the other timeouts; override with MIXDOG_AGENT_TERMINAL_REAP_MS.
|
|
138
|
-
|
|
138
|
+
// 5m (was 1h): aligned with the BP4 message-tail cache TTL — past 5m the
|
|
139
|
+
// session's prompt cache is cold anyway, so same-tag reuse pays a full prefix
|
|
140
|
+
// rewrite; a fresh respawn (send dead-tag fallback) starts smaller and cheaper.
|
|
141
|
+
// Trace replay (2026-07): agent tail cost 1h 1.77M -> 5m 1.11M (-37%).
|
|
142
|
+
const TERMINAL_REAP_MS = envTimeoutMs('MIXDOG_AGENT_TERMINAL_REAP_MS', 5 * 60_000);
|
|
139
143
|
// Independent hard cap for the spawn *prep* phase (ensureProvider /
|
|
140
144
|
// prepareAgentSession / catalog+rules load). Kept separate from the
|
|
141
145
|
// first-response watchdog so prep cannot hang a whole fanout before the model
|
|
@@ -387,7 +391,7 @@ function renderResult(value) {
|
|
|
387
391
|
for (const job of jobs) {
|
|
388
392
|
const target = job.tag || job.sessionId || '-';
|
|
389
393
|
const terminal = job.clientHostPid ? ` term=${job.clientHostPid}` : '';
|
|
390
|
-
const base = `- ${job.task_id} ${job.type} ${job.status} target=${target}${terminal}${job.error ? ` error=${
|
|
394
|
+
const base = `- ${job.task_id} ${job.type} ${job.status} target=${target}${terminal}${job.error ? ` error=${job.error}` : ''}`;
|
|
391
395
|
lines.push(appendAgentProgressKv(base, job));
|
|
392
396
|
}
|
|
393
397
|
if (workers.length === 0 && jobs.length === 0) lines.push('(no agents or tasks)');
|
|
@@ -422,7 +426,7 @@ function renderResult(value) {
|
|
|
422
426
|
const elapsed = elapsedFromStamps(value.startedAt, value.finishedAt, value.status);
|
|
423
427
|
if (elapsed) lines.push(`elapsed: ${elapsed}`);
|
|
424
428
|
}
|
|
425
|
-
if (value.error) lines.push(`error: ${
|
|
429
|
+
if (value.error) lines.push(`error: ${value.error}`);
|
|
426
430
|
if (value.status === 'running') lines.push('notification: completion will be delivered to the owner session; use read/status only for manual recovery.');
|
|
427
431
|
if (value.result !== undefined) {
|
|
428
432
|
const result = value.result;
|
|
@@ -466,7 +470,13 @@ function renderResult(value) {
|
|
|
466
470
|
return JSON.stringify(value, null, 2);
|
|
467
471
|
}
|
|
468
472
|
|
|
469
|
-
export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultCwd }) {
|
|
473
|
+
export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultCwd, onSubagentEvent }) {
|
|
474
|
+
// Optional bridge to the standard hook bus for SubagentStart / SubagentStop.
|
|
475
|
+
// Best-effort: a hook error must never affect worker spawn/finish.
|
|
476
|
+
function emitSubagentEvent(phase, agent, extra = {}) {
|
|
477
|
+
if (typeof onSubagentEvent !== 'function') return;
|
|
478
|
+
try { onSubagentEvent(phase, { agent_type: agent || null, ...extra }); } catch { /* best-effort */ }
|
|
479
|
+
}
|
|
470
480
|
const tags = new Map();
|
|
471
481
|
const tagAgents = new Map();
|
|
472
482
|
const tagCwds = new Map();
|
|
@@ -1246,6 +1256,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1246
1256
|
tag: meta.tag || null,
|
|
1247
1257
|
sessionId: meta.sessionId || null,
|
|
1248
1258
|
agent: meta.agent || null,
|
|
1259
|
+
...(meta.respawned === true ? { respawned: true, note: 'previous session reaped — fresh session, no prior context; re-supply anchors if needed' } : {}),
|
|
1249
1260
|
preset: meta.preset || null,
|
|
1250
1261
|
provider: meta.provider || null,
|
|
1251
1262
|
model: meta.model || null,
|
|
@@ -1619,6 +1630,8 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1619
1630
|
const { args, tag, session, agent, preset, presetName, workerCwd, prompt, watchdogPolicy } = prepared;
|
|
1620
1631
|
const watchdog = startProgressIdleWatchdog(session.id, watchdogPolicy);
|
|
1621
1632
|
let finalStatus = 'idle';
|
|
1633
|
+
// SubagentStart: a worker session is about to run its first turn.
|
|
1634
|
+
emitSubagentEvent('start', agent, { session_id: session.id, tag });
|
|
1622
1635
|
upsertWorkerSessionDeferred(session, tag, {
|
|
1623
1636
|
agent,
|
|
1624
1637
|
preset: presetKey(preset) || presetName,
|
|
@@ -1710,7 +1723,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1710
1723
|
try {
|
|
1711
1724
|
reconcileBackgroundTask(job.taskId, {
|
|
1712
1725
|
status: 'failed',
|
|
1713
|
-
error
|
|
1726
|
+
error,
|
|
1714
1727
|
terminalReason: 'agent-stream-stalled',
|
|
1715
1728
|
});
|
|
1716
1729
|
} catch {}
|
|
@@ -1747,6 +1760,8 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1747
1760
|
} catch {}
|
|
1748
1761
|
}
|
|
1749
1762
|
scheduleReap(session.id);
|
|
1763
|
+
// SubagentStop: worker finished (terminal), regardless of outcome.
|
|
1764
|
+
emitSubagentEvent('stop', agent, { session_id: session.id, tag, status: finalStatus });
|
|
1750
1765
|
}
|
|
1751
1766
|
}
|
|
1752
1767
|
|
|
@@ -1836,7 +1851,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1836
1851
|
try {
|
|
1837
1852
|
reconcileBackgroundTask(job.taskId, {
|
|
1838
1853
|
status: 'failed',
|
|
1839
|
-
error
|
|
1854
|
+
error,
|
|
1840
1855
|
terminalReason: 'agent-stream-stalled',
|
|
1841
1856
|
});
|
|
1842
1857
|
} catch {}
|
|
@@ -1996,8 +2011,32 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1996
2011
|
if (type === 'cancel') return renderResult(close(args, scopedContext));
|
|
1997
2012
|
if (type === 'close') return renderResult(close(args, scopedContext));
|
|
1998
2013
|
if (type === 'send') {
|
|
1999
|
-
|
|
2000
|
-
|
|
2014
|
+
try {
|
|
2015
|
+
const prepared = await prepareSend(args, scopedContext);
|
|
2016
|
+
return dispatchToExistingSession(prepared, notifyContext);
|
|
2017
|
+
} catch (err) {
|
|
2018
|
+
// Reaped/dead-tag fallback: with the 5m terminal-reap window a
|
|
2019
|
+
// same-scope follow-up often lands after the session is gone.
|
|
2020
|
+
// Instead of bouncing an error back to Lead (who would just
|
|
2021
|
+
// re-issue the same content as a spawn), respawn a FRESH session
|
|
2022
|
+
// under the same tag with the message as its brief. `respawned:
|
|
2023
|
+
// true` in the result tells Lead the worker has no prior session
|
|
2024
|
+
// context — re-supply anchors on the next send if needed.
|
|
2025
|
+
// Only tag-addressed sends fall back; explicit sessionId sends
|
|
2026
|
+
// keep erroring (caller pinned a specific session on purpose).
|
|
2027
|
+
const fallbackTag = clean(args.tag);
|
|
2028
|
+
const isDeadTarget = /not found|is closed/i.test(String(err?.message || ''));
|
|
2029
|
+
if (!fallbackTag || fallbackTag.startsWith('sess_') || !isDeadTarget) throw err;
|
|
2030
|
+
const prompt = clean(args.message || args.prompt);
|
|
2031
|
+
if (!prompt) throw err;
|
|
2032
|
+
// Clear the terminal trace so the fresh spawn isn't rejected by
|
|
2033
|
+
// the lingering-trace guard, then run the normal deferred spawn.
|
|
2034
|
+
try { forgetTag(fallbackTag); } catch {}
|
|
2035
|
+
try { removeWorkerRow({ tag: fallbackTag }); } catch {}
|
|
2036
|
+
const spawnArgs = { ...args, type: 'spawn', tag: fallbackTag, prompt, message: undefined };
|
|
2037
|
+
const job = startDeferredSpawnJob(spawnArgs, callerCwd, context, notifyContext, { respawned: true });
|
|
2038
|
+
return renderResult(renderJob(job, false));
|
|
2039
|
+
}
|
|
2001
2040
|
}
|
|
2002
2041
|
if (type === 'spawn') {
|
|
2003
2042
|
// Explicit-tag spawn priority (auto nextTag always creates a fresh session):
|
|
@@ -2033,7 +2072,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
2033
2072
|
}
|
|
2034
2073
|
throw new Error(`agent: unknown type "${type}"`);
|
|
2035
2074
|
} catch (err) {
|
|
2036
|
-
return
|
|
2075
|
+
return errorLine(err, { surface: 'agent' });
|
|
2037
2076
|
}
|
|
2038
2077
|
}
|
|
2039
2078
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { execFile, fork, spawnSync } from 'node:child_process';
|
|
2
|
-
import {
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
6
|
import { startChildGuardian } from '../runtime/shared/child-guardian.mjs';
|
|
7
|
+
import { appendBuffered } from '../runtime/shared/buffered-appender.mjs';
|
|
7
8
|
|
|
8
9
|
const CHANNEL_TOOLS = new Set([
|
|
9
10
|
'reply',
|
|
@@ -24,7 +25,7 @@ const WORKER_PRELOAD = fileURLToPath(new URL('./channel-worker-preload.cjs', imp
|
|
|
24
25
|
function logLine(path, line) {
|
|
25
26
|
try {
|
|
26
27
|
mkdirSync(dirname(path), { recursive: true });
|
|
27
|
-
|
|
28
|
+
appendBuffered(path, `[${new Date().toISOString()}] ${line}\n`);
|
|
28
29
|
} catch {
|
|
29
30
|
// Logging must never break the TUI.
|
|
30
31
|
}
|
|
@@ -170,8 +170,12 @@ function findConfigPreset(config, presetName) {
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
async function ensureExploreProviderReady(config) {
|
|
173
|
-
const
|
|
174
|
-
|
|
173
|
+
const route = config?.maintenance?.explore;
|
|
174
|
+
// Route object ({provider,model}) is the current shape; a string is a
|
|
175
|
+
// legacy preset NAME resolved against config.presets.
|
|
176
|
+
const provider = (route && typeof route === 'object')
|
|
177
|
+
? clean(route.provider)
|
|
178
|
+
: clean(findConfigPreset(config, route)?.provider);
|
|
175
179
|
if (!provider) return;
|
|
176
180
|
const providers = { ...(config?.providers || {}) };
|
|
177
181
|
providers[provider] = { ...(providers[provider] || {}), enabled: true };
|
|
@@ -303,7 +307,10 @@ async function runExploreSync(args = {}, ctx = {}) {
|
|
|
303
307
|
scheduleExploreCodeGraphPrewarm(resolvedCwd);
|
|
304
308
|
const config = loadConfig();
|
|
305
309
|
await ensureExploreProviderReady(config);
|
|
306
|
-
const
|
|
310
|
+
const route = config?.maintenance?.explore;
|
|
311
|
+
const presetName = (route && typeof route === 'object')
|
|
312
|
+
? `${clean(route.provider)}/${clean(route.model)}`
|
|
313
|
+
: (route || '');
|
|
307
314
|
const llm = makeAgentDispatch({
|
|
308
315
|
agent: 'explorer',
|
|
309
316
|
cwd: resolvedCwd,
|