mixdog 0.9.40 → 0.9.43
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/agent-tag-reuse-smoke.mjs +124 -10
- package/scripts/anthropic-oauth-refresh-race-test.mjs +397 -0
- package/scripts/arg-guard-test.mjs +16 -0
- package/scripts/compact-pressure-test.mjs +383 -0
- package/scripts/compact-trigger-migration-smoke.mjs +8 -8
- package/scripts/internal-comms-bench.mjs +0 -1
- package/scripts/internal-comms-smoke.mjs +54 -31
- package/scripts/internal-tools-normalization-test.mjs +52 -0
- package/scripts/max-output-recovery-persist-test.mjs +81 -0
- package/scripts/max-output-recovery-test.mjs +271 -0
- package/scripts/mcp-client-normalization-test.mjs +45 -0
- package/scripts/provider-toolcall-test.mjs +55 -0
- package/scripts/result-classification-test.mjs +75 -0
- package/scripts/smoke.mjs +1 -1
- package/scripts/submit-commandbusy-race-test.mjs +99 -7
- package/scripts/tui-transcript-perf-test.mjs +56 -1
- package/src/agents/reviewer/AGENT.md +8 -0
- package/src/rules/lead/lead-brief.md +11 -3
- package/src/rules/lead/lead-tool.md +0 -2
- package/src/rules/shared/01-tool.md +4 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +20 -2
- package/src/runtime/agent/orchestrator/context/collect.mjs +7 -3
- package/src/runtime/agent/orchestrator/internal-tools.mjs +16 -3
- package/src/runtime/agent/orchestrator/mcp/client.mjs +17 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +193 -13
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +14 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +59 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +57 -25
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +72 -12
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +144 -84
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +124 -8
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +12 -1
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +25 -7
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +28 -1
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +31 -4
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +45 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +7 -2
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +8 -6
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +15 -3
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -21
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -10
- package/src/runtime/memory/tool-defs.mjs +4 -3
- package/src/runtime/shared/tool-surface.mjs +1 -2
- package/src/session-runtime/context-status.mjs +8 -2
- package/src/standalone/agent-tool/render.mjs +2 -0
- package/src/standalone/agent-tool.mjs +202 -22
- package/src/tui/components/StatusLine.jsx +4 -26
- package/src/tui/dist/index.mjs +46 -31
- package/src/tui/engine/session-api.mjs +3 -0
- package/src/tui/engine/session-flow.mjs +19 -8
- package/src/tui/engine/turn.mjs +24 -2
- package/src/tui/engine.mjs +0 -1
- package/src/ui/statusline-agents.mjs +0 -8
- package/src/ui/statusline.mjs +2 -4
- package/src/workflows/default/WORKFLOW.md +38 -24
- package/src/workflows/solo-review/WORKFLOW.md +47 -0
- package/src/workflows/bench/WORKFLOW.md +0 -36
|
@@ -7,12 +7,39 @@ import { createEngineApiA } from '../src/tui/engine/session-api.mjs';
|
|
|
7
7
|
// command (commandBusy) or an auto-clear. Wires the real session-flow queue
|
|
8
8
|
// (enqueue/drain) + the real submit() against a minimal bag whose set() mirrors
|
|
9
9
|
// engine.mjs's commandBusy-release drain kick.
|
|
10
|
-
function makeEngine({
|
|
10
|
+
function makeEngine({
|
|
11
|
+
autoClearBeforeSubmit,
|
|
12
|
+
autoClearEnabled = false,
|
|
13
|
+
runtimeClear = async () => true,
|
|
14
|
+
} = {}) {
|
|
11
15
|
let seq = 0;
|
|
12
16
|
const executed = [];
|
|
13
|
-
|
|
17
|
+
const providerRequests = [];
|
|
18
|
+
let resolveProviderDispatch;
|
|
19
|
+
const providerDispatch = new Promise((resolve) => { resolveProviderDispatch = resolve; });
|
|
20
|
+
let state = {
|
|
21
|
+
items: [{ kind: 'user', id: 'existing', text: 'existing prompt' }],
|
|
22
|
+
queued: [],
|
|
23
|
+
busy: false,
|
|
24
|
+
commandBusy: false,
|
|
25
|
+
};
|
|
14
26
|
const bag = {
|
|
15
|
-
runtime: {
|
|
27
|
+
runtime: {
|
|
28
|
+
id: 'session_1',
|
|
29
|
+
session: {
|
|
30
|
+
id: 'session_1',
|
|
31
|
+
messages: [{ role: 'user', content: 'existing prompt' }],
|
|
32
|
+
lastUsedAt: Date.now() - 1000,
|
|
33
|
+
},
|
|
34
|
+
getCompactionSettings: () => ({}),
|
|
35
|
+
clear: runtimeClear,
|
|
36
|
+
consumePendingSessionReset: () => null,
|
|
37
|
+
ask: async (text) => {
|
|
38
|
+
providerRequests.push(text);
|
|
39
|
+
resolveProviderDispatch(text);
|
|
40
|
+
return { result: { content: 'ok' }, session: bag.runtime.session };
|
|
41
|
+
},
|
|
42
|
+
},
|
|
16
43
|
nextId: () => `id_${++seq}`,
|
|
17
44
|
tuiDebug: () => {},
|
|
18
45
|
flags: {},
|
|
@@ -30,12 +57,15 @@ function makeEngine({ autoClearBeforeSubmit } = {}) {
|
|
|
30
57
|
if (released) queueMicrotask(() => { void bag.drain?.(); });
|
|
31
58
|
return true;
|
|
32
59
|
},
|
|
33
|
-
pushItem: () => {},
|
|
60
|
+
pushItem: (item) => { state = { ...state, items: [...state.items, item] }; },
|
|
34
61
|
patchItem: () => {},
|
|
35
62
|
replaceItems: (x) => x,
|
|
36
63
|
pushNotice: () => {},
|
|
37
|
-
pushUserOrSyntheticItem: (text) => {
|
|
38
|
-
|
|
64
|
+
pushUserOrSyntheticItem: (text, id) => {
|
|
65
|
+
executed.push(text);
|
|
66
|
+
state = { ...state, items: [...state.items, { kind: 'user', id, text }] };
|
|
67
|
+
},
|
|
68
|
+
autoClearState: () => ({ enabled: autoClearEnabled, idleMs: 0, minContextPercent: 0 }),
|
|
39
69
|
agentStatusState: () => ({}),
|
|
40
70
|
routeState: () => ({}),
|
|
41
71
|
syncContextStats: () => {},
|
|
@@ -44,7 +74,10 @@ function makeEngine({ autoClearBeforeSubmit } = {}) {
|
|
|
44
74
|
requeueEntriesFront: () => {},
|
|
45
75
|
resetStatsAndSyncContext: () => {},
|
|
46
76
|
flushDeferredExecutionPendingResumeKick: () => {},
|
|
47
|
-
runTurn: async () =>
|
|
77
|
+
runTurn: async (text) => {
|
|
78
|
+
await bag.runtime.ask(text);
|
|
79
|
+
return 'ok';
|
|
80
|
+
},
|
|
48
81
|
};
|
|
49
82
|
Object.assign(bag, createSessionFlow(bag));
|
|
50
83
|
if (autoClearBeforeSubmit) bag.autoClearBeforeSubmit = autoClearBeforeSubmit;
|
|
@@ -53,6 +86,14 @@ function makeEngine({ autoClearBeforeSubmit } = {}) {
|
|
|
53
86
|
api,
|
|
54
87
|
bag,
|
|
55
88
|
getExecuted: () => executed,
|
|
89
|
+
getProviderRequests: () => providerRequests,
|
|
90
|
+
waitForProviderDispatch: (timeoutMs = 250) => new Promise((resolve, reject) => {
|
|
91
|
+
const timer = setTimeout(() => reject(new Error(`provider dispatch timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
92
|
+
providerDispatch.then((text) => {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
resolve(text);
|
|
95
|
+
}, reject);
|
|
96
|
+
}),
|
|
56
97
|
getState: () => bag.getState(),
|
|
57
98
|
mutateState: (mutator) => { mutator(state); },
|
|
58
99
|
};
|
|
@@ -107,6 +148,57 @@ test('submit still enqueues when autoClearBeforeSubmit rejects', async () => {
|
|
|
107
148
|
assert.deepEqual(getExecuted(), ['survives rejection'], 'rejected auto-clear must not lose the prompt');
|
|
108
149
|
});
|
|
109
150
|
|
|
151
|
+
test('runtime clear false skips auto-clear and sends the first post-failure prompt to the provider', async () => {
|
|
152
|
+
const {
|
|
153
|
+
api, bag, getExecuted, getProviderRequests, getState,
|
|
154
|
+
} = makeEngine({ autoClearEnabled: true, runtimeClear: async () => false });
|
|
155
|
+
|
|
156
|
+
assert.equal(api.submit('first after failed auto-clear'), true);
|
|
157
|
+
await tick(); await tick();
|
|
158
|
+
|
|
159
|
+
assert.deepEqual(getExecuted(), ['first after failed auto-clear'], 'user card remains visible');
|
|
160
|
+
assert.deepEqual(getProviderRequests(), ['first after failed auto-clear'], 'first post-failure input reaches provider');
|
|
161
|
+
assert.equal(getState().items.some((item) => item.label === 'Auto-clear skipped'), true);
|
|
162
|
+
assert.equal(getState().items.some((item) => item.label === 'Auto-clear complete'), false);
|
|
163
|
+
assert.equal(getState().items.some((item) => item.id === 'existing'), true, 'failed clear preserves the existing session UI');
|
|
164
|
+
assert.equal(bag.runtime.session.messages[0].content, 'existing prompt', 'failed clear preserves the runtime session');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('late runtime clear false keeps the UI skipped and sends the first post-timeout prompt', async () => {
|
|
168
|
+
let resolveClear;
|
|
169
|
+
let harness;
|
|
170
|
+
const autoClearBeforeSubmit = () => harness.bag.performSessionClear({
|
|
171
|
+
verb: 'Auto-clearing idle conversation',
|
|
172
|
+
doneLabel: 'Auto-clear complete',
|
|
173
|
+
skipLabel: 'Auto-clear skipped',
|
|
174
|
+
surface: 'auto-clear',
|
|
175
|
+
useCompaction: true,
|
|
176
|
+
compactTimeoutMs: 5,
|
|
177
|
+
});
|
|
178
|
+
harness = makeEngine({
|
|
179
|
+
autoClearBeforeSubmit,
|
|
180
|
+
runtimeClear: () => new Promise((resolve) => { resolveClear = resolve; }),
|
|
181
|
+
});
|
|
182
|
+
harness.bag.runtime.getCompactionSettings = () => ({ compactType: 'summary' });
|
|
183
|
+
|
|
184
|
+
assert.equal(harness.api.submit('first after timed-out auto-clear'), true);
|
|
185
|
+
await harness.waitForProviderDispatch();
|
|
186
|
+
assert.equal(typeof resolveClear, 'function', 'compacting clear started');
|
|
187
|
+
assert.deepEqual(
|
|
188
|
+
harness.getProviderRequests(),
|
|
189
|
+
['first after timed-out auto-clear'],
|
|
190
|
+
'first post-timeout input reaches provider before late clear settles',
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
resolveClear(false);
|
|
194
|
+
await tick(); await tick();
|
|
195
|
+
|
|
196
|
+
assert.equal(harness.getState().items.some((item) => item.label === 'Auto-clear skipped'), true);
|
|
197
|
+
assert.equal(harness.getState().items.some((item) => item.label === 'Auto-clear complete'), false);
|
|
198
|
+
assert.equal(harness.getState().items.some((item) => item.id === 'existing'), true, 'late false preserves the existing session UI');
|
|
199
|
+
assert.equal(harness.bag.runtime.session.messages[0].content, 'existing prompt', 'late false preserves the runtime session');
|
|
200
|
+
});
|
|
201
|
+
|
|
110
202
|
test('empty and whitespace submits are still rejected', () => {
|
|
111
203
|
const { api, bag } = makeEngine();
|
|
112
204
|
assert.equal(api.submit(' '), false);
|
|
@@ -8,7 +8,7 @@ import { buildTranscriptRowIndexIncremental } from '../src/tui/app/transcript-wi
|
|
|
8
8
|
|
|
9
9
|
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
10
|
|
|
11
|
-
function makeTurnHarness(ask) {
|
|
11
|
+
function makeTurnHarness(ask, stateOverrides = {}) {
|
|
12
12
|
let seq = 0;
|
|
13
13
|
let state = {
|
|
14
14
|
items: [],
|
|
@@ -18,6 +18,7 @@ function makeTurnHarness(ask) {
|
|
|
18
18
|
busy: false,
|
|
19
19
|
spinner: null,
|
|
20
20
|
thinking: null,
|
|
21
|
+
...stateOverrides,
|
|
21
22
|
};
|
|
22
23
|
const itemIndexById = new Map();
|
|
23
24
|
const set = (patch) => { state = { ...state, ...patch }; return true; };
|
|
@@ -43,6 +44,15 @@ function makeTurnHarness(ask) {
|
|
|
43
44
|
const clearStreamingTail = (id = null) => {
|
|
44
45
|
if (id == null || state.streamingTail?.id === id) set({ streamingTail: null });
|
|
45
46
|
};
|
|
47
|
+
const replaceItems = (items, options = {}) => {
|
|
48
|
+
state = replaceEngineItemsState({
|
|
49
|
+
state,
|
|
50
|
+
items,
|
|
51
|
+
itemIndexById,
|
|
52
|
+
preserveStreamingTail: options.preserveStreamingTail === true,
|
|
53
|
+
});
|
|
54
|
+
return items;
|
|
55
|
+
};
|
|
46
56
|
const bag = {
|
|
47
57
|
runtime: { id: null, toolMode: 'auto', ask, abort: () => true },
|
|
48
58
|
nextId: () => `id_${++seq}`,
|
|
@@ -55,6 +65,7 @@ function makeTurnHarness(ask) {
|
|
|
55
65
|
set,
|
|
56
66
|
pushItem,
|
|
57
67
|
patchItem,
|
|
68
|
+
replaceItems,
|
|
58
69
|
updateStreamingTail,
|
|
59
70
|
settleStreamingTail,
|
|
60
71
|
clearStreamingTail,
|
|
@@ -77,6 +88,50 @@ function makeTurnHarness(ask) {
|
|
|
77
88
|
return { runTurn: createRunTurn(bag), getState: () => state };
|
|
78
89
|
}
|
|
79
90
|
|
|
91
|
+
test('successful mid-turn compact trims history but preserves live turn references', async () => {
|
|
92
|
+
let preservedTail = false;
|
|
93
|
+
const harness = makeTurnHarness(async (_text, options) => {
|
|
94
|
+
options.onTextDelta('before compact\n');
|
|
95
|
+
await wait(30);
|
|
96
|
+
options.onCompactEvent({ status: 'compacted', trigger: 'reactive' });
|
|
97
|
+
preservedTail = harness.getState().streamingTail?.text === 'before compact\n';
|
|
98
|
+
options.onCompactEvent({ status: 'compacted', trigger: 'reactive' });
|
|
99
|
+
options.onTextDelta('after compact\n');
|
|
100
|
+
return { result: { content: 'before compact\nafter compact\n' }, session: { messages: [] } };
|
|
101
|
+
}, {
|
|
102
|
+
items: [
|
|
103
|
+
{ id: 'old', kind: 'assistant', text: 'old history' },
|
|
104
|
+
{ id: 'current-user', kind: 'user', text: 'go' },
|
|
105
|
+
],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
assert.equal(await harness.runTurn('go', { submittedIds: ['current-user'] }), 'done');
|
|
109
|
+
assert.equal(preservedTail, true);
|
|
110
|
+
assert.equal(harness.getState().items.some((item) => item.id === 'old'), false);
|
|
111
|
+
assert.equal(harness.getState().items.some((item) => item.id === 'current-user'), true);
|
|
112
|
+
assert.equal(
|
|
113
|
+
harness.getState().items.filter((item) => item.label === 'Compact complete (overflow recovery)').length,
|
|
114
|
+
2,
|
|
115
|
+
);
|
|
116
|
+
assert.equal(
|
|
117
|
+
harness.getState().items.find((item) => item.kind === 'assistant')?.text,
|
|
118
|
+
'before compact\nafter compact\n',
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('failed mid-turn compact leaves prior transcript items untouched', async () => {
|
|
123
|
+
const harness = makeTurnHarness(async (_text, options) => {
|
|
124
|
+
options.onCompactEvent({ status: 'failed' });
|
|
125
|
+
return { result: { content: '' }, session: { messages: [] } };
|
|
126
|
+
}, {
|
|
127
|
+
items: [{ id: 'old', kind: 'assistant', text: 'old history' }],
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
assert.equal(await harness.runTurn('go'), 'done');
|
|
131
|
+
assert.equal(harness.getState().items.some((item) => item.id === 'old'), true);
|
|
132
|
+
assert.equal(harness.getState().items.some((item) => item.label === 'Compact failed'), true);
|
|
133
|
+
});
|
|
134
|
+
|
|
80
135
|
test('stream flush keeps settled items identity and finalize appends one assistant', async () => {
|
|
81
136
|
let identityStable = false;
|
|
82
137
|
const harness = makeTurnHarness(async (_text, options) => {
|
|
@@ -9,5 +9,13 @@ Independent regression/risk review agent.
|
|
|
9
9
|
Review the approved intent, diff, and tests with independent judgment. Prioritize
|
|
10
10
|
actionable correctness, regression, security, and verification risks; inspect
|
|
11
11
|
affected boundaries. Do not reimplement the change or report non-risky nits.
|
|
12
|
+
Treat implementer and Lead reports as claims to test rather than evidence;
|
|
13
|
+
independently evaluate the final deliverable against the original request and
|
|
14
|
+
available evidence with a critical lens, actively seeking errors, unsupported
|
|
15
|
+
assumptions, and counterexamples before confirming.
|
|
12
16
|
Report findings first, severity-ordered, with one line per `file:line`. If clean,
|
|
13
17
|
say so in one line and include only material residual risk.
|
|
18
|
+
|
|
19
|
+
When the work comes with stated criteria or reference material for judging
|
|
20
|
+
it, verify against those as given — substituting your own interpretation or
|
|
21
|
+
a self-built check is a verification risk to report.
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
# Lead Brief
|
|
2
2
|
|
|
3
|
-
- One-line fragments
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
- One-line fragments. `Task:` is mandatory and lossless: include the intent,
|
|
4
|
+
required outcomes, negative outcomes, and completion/stop boundary. Minimum
|
|
5
|
+
chars, maximum info: omit role-known rules and repeated context/facts; split
|
|
6
|
+
scope, don't pad or discard task requirements.
|
|
7
|
+
- Preserve user-supplied exact targets and exact replacements/outputs in
|
|
8
|
+
`Task:`. Never infer exactness from a task name, file count, or perceived
|
|
9
|
+
difficulty.
|
|
10
|
+
- All other fields are optional task-specific deltas: `Anchors:`
|
|
11
|
+
`Allow/Forbid:` `Deliver:` `Verify:`. Omit any that add no information.
|
|
6
12
|
- Anchors: `file:line` + one-line conclusion; never log/code bodies. Specify
|
|
7
13
|
outcome, not method unless required. `Deliver:` gives shape/size, never a long
|
|
8
14
|
handoff. Referenced spec/test beats its summary.
|
|
15
|
+
- Preserve the exact same `Task:` across Worker -> Debugger -> Reviewer. Never
|
|
16
|
+
summarize, rewrite, or replace it when handing the scope to the next role.
|
|
9
17
|
- Full brief only for fresh spawn/`respawned: true`; live follow-ups are delta.
|
|
10
18
|
Dead-tag send is cold: re-supply anchors.
|
|
11
19
|
- Never `send` mid-run; batch one follow-up after completion; interrupt only to
|
|
@@ -3,5 +3,3 @@
|
|
|
3
3
|
- Write-role agents self-verify via `shell`; Lead runs cross-scope
|
|
4
4
|
verification, benches, and all git via it.
|
|
5
5
|
- Use current project/workspace unless user request/tool need.
|
|
6
|
-
- If workflow permits delegation, use `agent` for scoped
|
|
7
|
-
implementation/research/review/debugging; a no-delegation workflow controls.
|
|
@@ -44,3 +44,7 @@
|
|
|
44
44
|
reread returned spans. A third window means the first was too narrow.
|
|
45
45
|
- One carve-out to parallel-first: apply the edit, then verify it in a
|
|
46
46
|
separate shell turn and consume results in order. Otherwise stay parallel.
|
|
47
|
+
- A long-running command promoted to background is a decision point, not a
|
|
48
|
+
cue to wait: estimate from its observed progress whether it can finish
|
|
49
|
+
within the remaining budget; if not, diagnose the bottleneck and switch to
|
|
50
|
+
an alternative route. Waiting is chosen, never assumed.
|
|
@@ -194,6 +194,10 @@ export const DEFAULT_FIRST_RESPONSE_TIMEOUT_MS = envTimeoutMs(
|
|
|
194
194
|
'MIXDOG_AGENT_FIRST_RESPONSE_TIMEOUT_MS',
|
|
195
195
|
120_000,
|
|
196
196
|
);
|
|
197
|
+
export const DEFAULT_FIRST_VISIBLE_CEILING_MS = envTimeoutMs(
|
|
198
|
+
'MIXDOG_AGENT_FIRST_VISIBLE_TIMEOUT_MS',
|
|
199
|
+
600_000,
|
|
200
|
+
);
|
|
197
201
|
export const DEFAULT_STALE_TIMEOUT_MS = envTimeoutMs(
|
|
198
202
|
'MIXDOG_AGENT_STALE_TIMEOUT_MS',
|
|
199
203
|
30 * 60_000,
|
|
@@ -209,6 +213,10 @@ export function resolveAgentWatchdogPolicy(agent, overrides = {}) {
|
|
|
209
213
|
overrides.firstResponseTimeoutMs,
|
|
210
214
|
DEFAULT_FIRST_RESPONSE_TIMEOUT_MS,
|
|
211
215
|
);
|
|
216
|
+
const firstVisibleCeilingMs = resolveExplicitMs(
|
|
217
|
+
overrides.firstVisibleTimeoutMs,
|
|
218
|
+
DEFAULT_FIRST_VISIBLE_CEILING_MS,
|
|
219
|
+
);
|
|
212
220
|
|
|
213
221
|
let idleStaleMs;
|
|
214
222
|
if (Number.isFinite(overrides.idleTimeoutMs) && overrides.idleTimeoutMs >= 0) {
|
|
@@ -238,6 +246,7 @@ export function resolveAgentWatchdogPolicy(agent, overrides = {}) {
|
|
|
238
246
|
|
|
239
247
|
return {
|
|
240
248
|
firstResponseMs,
|
|
249
|
+
firstVisibleCeilingMs,
|
|
241
250
|
idleStaleMs,
|
|
242
251
|
toolRunningMs,
|
|
243
252
|
};
|
|
@@ -248,8 +257,16 @@ export function evaluateAgentWatchdogAbort(snapshot, now, policy) {
|
|
|
248
257
|
|
|
249
258
|
if (snapshot.waitingForFirstActivity) {
|
|
250
259
|
const startedAt = snapshot.modelRequestStartedAt || snapshot.askStartedAt;
|
|
251
|
-
|
|
252
|
-
|
|
260
|
+
const hasTransport = Boolean(
|
|
261
|
+
startedAt
|
|
262
|
+
&& snapshot.lastTransportAt
|
|
263
|
+
&& snapshot.lastTransportAt > startedAt
|
|
264
|
+
);
|
|
265
|
+
const ceilingMs = hasTransport
|
|
266
|
+
? policy.firstVisibleCeilingMs
|
|
267
|
+
: policy.firstResponseMs;
|
|
268
|
+
if (ceilingMs > 0 && startedAt && now - startedAt > ceilingMs) {
|
|
269
|
+
return new AgentStallAbortError(`agent first response stale (${ceilingMs}ms)`);
|
|
253
270
|
}
|
|
254
271
|
return null;
|
|
255
272
|
}
|
|
@@ -293,6 +310,7 @@ export function evaluateAgentWatchdogAbort(snapshot, now, policy) {
|
|
|
293
310
|
export function agentWatchdogPolicyActive(policy) {
|
|
294
311
|
if (!policy) return false;
|
|
295
312
|
return (policy.firstResponseMs > 0)
|
|
313
|
+
|| (policy.firstVisibleCeilingMs > 0)
|
|
296
314
|
|| (policy.idleStaleMs > 0)
|
|
297
315
|
|| (policy.toolRunningMs > 0);
|
|
298
316
|
}
|
|
@@ -784,6 +784,13 @@ export function composeSystemPrompt(opts) {
|
|
|
784
784
|
? ''
|
|
785
785
|
: loadScopedRoleInstructions(opts.agent || null, opts.provider || null);
|
|
786
786
|
const stableSystemParts = [];
|
|
787
|
+
// Active workflow contract leads the role layer: it must outrank the
|
|
788
|
+
// generic role/tool guidance below it, not trail the profile/meta block
|
|
789
|
+
// in BP3 (observed: leads deprioritized delegation rules that sat behind
|
|
790
|
+
// ~3KB of profile/output-style text).
|
|
791
|
+
if (opts.workflowContext && typeof opts.workflowContext === 'string' && opts.workflowContext.trim()) {
|
|
792
|
+
stableSystemParts.push(opts.workflowContext.trim());
|
|
793
|
+
}
|
|
787
794
|
if (opts.roleRules) stableSystemParts.push(opts.roleRules);
|
|
788
795
|
if (opts.userPrompt) stableSystemParts.push(opts.userPrompt);
|
|
789
796
|
if (roleInstructionContext) stableSystemParts.push(roleInstructionContext);
|
|
@@ -798,9 +805,6 @@ export function composeSystemPrompt(opts) {
|
|
|
798
805
|
if (opts.metaContext && typeof opts.metaContext === 'string' && opts.metaContext.trim()) {
|
|
799
806
|
sessionMarkerParts.push(opts.metaContext.trim());
|
|
800
807
|
}
|
|
801
|
-
if (opts.workflowContext && typeof opts.workflowContext === 'string' && opts.workflowContext.trim()) {
|
|
802
|
-
sessionMarkerParts.push(opts.workflowContext.trim());
|
|
803
|
-
}
|
|
804
808
|
if (!_skip.memory && opts.coreMemoryContext && typeof opts.coreMemoryContext === 'string' && opts.coreMemoryContext.trim()) {
|
|
805
809
|
sessionMarkerParts.push('# Core Memory\n' + opts.coreMemoryContext.trim());
|
|
806
810
|
}
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* no-tool role guard) — not a permission check. No gating is needed here.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { isToolEnvelope, makeToolEnvelope } from './session/tool-envelope.mjs';
|
|
18
|
+
import { isToolEnvelope, makeToolEnvelope, normalizeToolEnvelope } from './session/tool-envelope.mjs';
|
|
19
|
+
import { classifyResultKind } from './session/result-classification.mjs';
|
|
19
20
|
|
|
20
21
|
let _executor = null;
|
|
21
22
|
let _tools = [];
|
|
@@ -62,12 +63,24 @@ function _normalize(result) {
|
|
|
62
63
|
// JSON.stringify below and the loop would see the stringified envelope as
|
|
63
64
|
// the tool_result (body inlined + duplicated, no newMessages).
|
|
64
65
|
if (isToolEnvelope(result)) {
|
|
65
|
-
|
|
66
|
+
const normalized = normalizeToolEnvelope(_normalize(result.result));
|
|
67
|
+
return makeToolEnvelope(normalized.result, result.newMessages, {
|
|
68
|
+
explicitSuccess: normalized.explicitSuccess || result.explicitSuccess === true,
|
|
69
|
+
});
|
|
66
70
|
}
|
|
67
71
|
if (result && typeof result === 'object' && Array.isArray(result.content)) {
|
|
68
|
-
|
|
72
|
+
const text = result.content
|
|
69
73
|
.map((c) => (c?.type === 'text' ? c.text || '' : JSON.stringify(c)))
|
|
70
74
|
.join('\n');
|
|
75
|
+
// Preserve MCP-style handler outcome metadata across the object→string
|
|
76
|
+
// boundary. Explicit failures retain the canonical Error: convention;
|
|
77
|
+
// explicit successes use a transient envelope so legitimate output
|
|
78
|
+
// beginning with Error: is not mistaken for a failed execution.
|
|
79
|
+
if (result.isError === true) return !text.startsWith('Error:') ? `Error: ${text}` : text;
|
|
80
|
+
if (result.isError === false && classifyResultKind(text) === 'error') {
|
|
81
|
+
return makeToolEnvelope(text, [], { explicitSuccess: true });
|
|
82
|
+
}
|
|
83
|
+
return text;
|
|
71
84
|
}
|
|
72
85
|
if (typeof result === 'string') return result;
|
|
73
86
|
return JSON.stringify(result);
|
|
@@ -6,6 +6,8 @@ import { randomUUID } from 'crypto';
|
|
|
6
6
|
import { smartReadTruncate } from '../tools/builtin/read-formatting.mjs';
|
|
7
7
|
import { shutdownStdioChild, killStdioChildTreeFast } from './child-tree.mjs';
|
|
8
8
|
import { readServicePort, markServiceUnreachable, isConnRefuseError } from '../../../shared/service-discovery.mjs';
|
|
9
|
+
import { makeToolEnvelope, normalizeToolEnvelope } from '../session/tool-envelope.mjs';
|
|
10
|
+
import { classifyResultKind } from '../session/result-classification.mjs';
|
|
9
11
|
// --- Types ---
|
|
10
12
|
/** Known auto-detect targets: port file path relative to tmpdir.
|
|
11
13
|
* Note: `mixdog` used to self-loopback via active-instance.json's
|
|
@@ -215,6 +217,16 @@ export async function executeMcpTool(name, args) {
|
|
|
215
217
|
throw new Error(`Tool call failed: ${firstMsg}; retry after reconnect also failed: ${retryMsg}`);
|
|
216
218
|
}
|
|
217
219
|
}
|
|
220
|
+
const normalized = normalizeToolEnvelope(normalizeMcpToolResult(result));
|
|
221
|
+
const text = capMcpOutput(normalized.result);
|
|
222
|
+
return normalized.explicitSuccess
|
|
223
|
+
? makeToolEnvelope(text, [], { explicitSuccess: true })
|
|
224
|
+
: text;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Preserve MCP failure metadata across the object→string boundary. The
|
|
228
|
+
// session loop classifies the canonical Error: prefix as toolKind:error.
|
|
229
|
+
export function normalizeMcpToolResult(result) {
|
|
218
230
|
const content = result.content;
|
|
219
231
|
let text;
|
|
220
232
|
if (Array.isArray(content)) {
|
|
@@ -224,7 +236,11 @@ export async function executeMcpTool(name, args) {
|
|
|
224
236
|
} else {
|
|
225
237
|
text = typeof content === 'string' ? content : JSON.stringify(content);
|
|
226
238
|
}
|
|
227
|
-
return
|
|
239
|
+
if (result.isError === true) return !text.startsWith('Error:') ? `Error: ${text}` : text;
|
|
240
|
+
if (result.isError === false && classifyResultKind(text) === 'error') {
|
|
241
|
+
return makeToolEnvelope(text, [], { explicitSuccess: true });
|
|
242
|
+
}
|
|
243
|
+
return text;
|
|
228
244
|
}
|
|
229
245
|
|
|
230
246
|
// MCP per-tool-call timeout. Default 2min: a hung/unresponsive MCP server
|