mixdog 0.9.36 → 0.9.38
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 +3 -2
- package/scripts/abort-recovery-test.mjs +118 -0
- package/scripts/compact-smoke.mjs +7 -7
- package/scripts/dispatch-persist-recovery-test.mjs +141 -0
- package/scripts/explore-bench-tmp.mjs +19 -0
- package/scripts/explore-bench.mjs +101 -14
- package/scripts/explore-prompt-policy-test.mjs +31 -0
- package/scripts/ingest-pure-conversation-smoke.mjs +11 -11
- package/scripts/notify-completion-mirror-test.mjs +73 -0
- package/scripts/openai-ws-early-settle-test.mjs +176 -0
- package/scripts/output-style-smoke.mjs +17 -17
- package/scripts/provider-toolcall-test.mjs +333 -14
- package/scripts/recall-bench-cases.json +3 -3
- package/scripts/recall-bench.mjs +1 -1
- package/scripts/recall-quality-cases.json +4 -4
- package/scripts/recall-usecase-cases.json +2 -2
- package/scripts/session-bench.mjs +13 -13
- package/scripts/session-sweep.mjs +266 -0
- package/scripts/steering-drain-buckets-test.mjs +72 -11
- package/scripts/submit-commandbusy-race-test.mjs +114 -0
- package/scripts/tool-smoke.mjs +83 -10
- package/src/app.mjs +2 -1
- package/src/defaults/cycle3-review-prompt.md +9 -0
- package/src/defaults/skills/setup/SKILL.md +93 -293
- package/src/lib/rules-builder.cjs +3 -2
- package/src/output-styles/default.md +2 -2
- package/src/output-styles/extreme-minimal.md +1 -1
- package/src/output-styles/minimal.md +1 -1
- package/src/output-styles/simple.md +2 -3
- package/src/rules/agent/30-explorer.md +40 -14
- package/src/rules/lead/01-general.md +4 -0
- package/src/rules/shared/01-tool.md +7 -3
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +15 -3
- package/src/runtime/agent/orchestrator/config.mjs +1 -1
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +31 -8
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +13 -7
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +35 -5
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +27 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +36 -37
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +72 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +26 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +12 -28
- package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +18 -15
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +119 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +113 -2
- package/src/runtime/agent/orchestrator/providers/registry.mjs +44 -5
- package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +78 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +57 -31
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +8 -6
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +28 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +30 -55
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +24 -16
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +116 -21
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +55 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +35 -20
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -9
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +77 -31
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +25 -3
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +73 -25
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +17 -7
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -12
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +56 -6
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -0
- package/src/runtime/channels/lib/worker-main.mjs +5 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +104 -0
- package/src/runtime/memory/lib/ko-morph.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle3.mjs +64 -4
- package/src/runtime/memory/lib/memory-text-utils.mjs +4 -4
- package/src/runtime/memory/lib/recall-format.mjs +3 -3
- package/src/runtime/shared/child-spawn-gate.mjs +15 -0
- package/src/runtime/shared/config.mjs +98 -12
- package/src/runtime/shared/process-listener-headroom.mjs +12 -0
- package/src/session-runtime/cwd-plugins.mjs +6 -3
- package/src/session-runtime/model-route-api.mjs +50 -2
- package/src/session-runtime/provider-auth-api.mjs +8 -1
- package/src/session-runtime/provider-models.mjs +3 -3
- package/src/session-runtime/resource-api.mjs +22 -0
- package/src/session-runtime/runtime-core.mjs +56 -10
- package/src/session-runtime/settings-api.mjs +11 -2
- package/src/session-runtime/warmup-schedulers.mjs +7 -1
- package/src/standalone/agent-tool.mjs +15 -9
- package/src/standalone/explore-tool.mjs +4 -1
- package/src/tui/App.jsx +6 -5
- package/src/tui/app/transcript-window.mjs +60 -10
- package/src/tui/app/use-transcript-window.mjs +2 -1
- package/src/tui/components/ContextPanel.jsx +4 -1
- package/src/tui/components/ToolExecution.jsx +15 -12
- package/src/tui/components/TranscriptItem.jsx +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +8 -8
- package/src/tui/dist/index.mjs +482 -151
- package/src/tui/engine/agent-job-feed.mjs +36 -6
- package/src/tui/engine/notification-plan.mjs +3 -3
- package/src/tui/engine/queue-helpers.mjs +8 -0
- package/src/tui/engine/render-timing.mjs +104 -8
- package/src/tui/engine/session-api.mjs +58 -3
- package/src/tui/engine/session-flow.mjs +93 -29
- package/src/tui/engine/tool-card-results.mjs +28 -13
- package/src/tui/engine/turn.mjs +238 -157
- package/src/tui/engine.mjs +17 -8
- package/src/tui/index.jsx +10 -1
- package/src/workflows/default/WORKFLOW.md +8 -4
- package/src/workflows/solo/WORKFLOW.md +8 -4
|
@@ -16,9 +16,13 @@
|
|
|
16
16
|
error origin, ...) and send them as ONE `query[]` call — facets run in
|
|
17
17
|
parallel. Never fan out rephrasings of the same target; on
|
|
18
18
|
EXPLORATION_FAILED, retry once with changed tokens.
|
|
19
|
-
- Verified
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
- Verified = user-provided, tool-returned, or the session project cwd itself.
|
|
20
|
+
Unscoped grep/glob/list from the project root needs NO find hop; find only
|
|
21
|
+
resolves a genuinely guessed path-name fragment, and it rides the SAME turn
|
|
22
|
+
as independent probes (unscoped grep, code_graph) — never a solo
|
|
23
|
+
path-verification turn. ENOENT → `find` basename, never retry a guess; don't
|
|
24
|
+
mask a miss with a guessed glob scope (path "." + `src/**`) or an invented
|
|
25
|
+
absolute path, but plain project-root searches are fine.
|
|
22
26
|
- Retrieval stops when evidence covers the deliverable: single-answer tasks
|
|
23
27
|
end at the first sufficient anchor; enumeration tasks (review, audit) end
|
|
24
28
|
when the stated scope is covered. Never re-verify a hit already on screen;
|
|
@@ -21,15 +21,22 @@ const MIXDOG_SLOW_TOOL_TRACE_NAMES = new Set(
|
|
|
21
21
|
.filter(Boolean)
|
|
22
22
|
);
|
|
23
23
|
|
|
24
|
-
function traceAgentLoop({ sessionId, iteration, sendMs, messageCount, bodyBytesEst }) {
|
|
25
|
-
|
|
24
|
+
function traceAgentLoop({ sessionId, iteration, sendMs, messageCount, bodyBytesEst, agent = null }) {
|
|
25
|
+
// Two emit modes, no behavior change either way:
|
|
26
|
+
// VERBOSE=1 → full loop row incl. body_bytes_est (payload serialized).
|
|
27
|
+
// TIMING=1 → lightweight send-latency attribution for high-fanout
|
|
28
|
+
// benches; bodyBytesEst is skipped upstream so measuring
|
|
29
|
+
// the send does not perturb it (body_bytes_est → null).
|
|
30
|
+
if (process.env.MIXDOG_AGENT_TRACE_VERBOSE !== '1'
|
|
31
|
+
&& process.env.MIXDOG_AGENT_TRACE_TIMING !== '1') return;
|
|
26
32
|
appendAgentTrace({
|
|
27
33
|
sessionId,
|
|
28
34
|
iteration,
|
|
29
35
|
kind: 'loop',
|
|
36
|
+
agent: agent || null,
|
|
30
37
|
send_ms: sendMs,
|
|
31
38
|
message_count: messageCount,
|
|
32
|
-
body_bytes_est: bodyBytesEst,
|
|
39
|
+
body_bytes_est: bodyBytesEst ?? null,
|
|
33
40
|
});
|
|
34
41
|
}
|
|
35
42
|
|
|
@@ -206,6 +213,11 @@ function _redactLogText(text) {
|
|
|
206
213
|
|
|
207
214
|
function classifyToolFailure(resultText, toolName) {
|
|
208
215
|
const text = String(resultText ?? '').toLowerCase();
|
|
216
|
+
if (/\[shell-tool-failed\]/i.test(String(resultText ?? ''))) return 'tool-call/failure';
|
|
217
|
+
if (/\[shell-run-failed\]/i.test(String(resultText ?? ''))) {
|
|
218
|
+
if (/timeout|timed out|aborted|interrupted/.test(text)) return 'timeout/abort';
|
|
219
|
+
return 'command-exit';
|
|
220
|
+
}
|
|
209
221
|
if (/requires either|invalid arguments|unknown parameter|must be|schema|expected|required|old_string is .*>=/.test(text)) return 'schema/args';
|
|
210
222
|
if (/not in allow-list|not allowed/.test(text)) return 'permission';
|
|
211
223
|
if (String(toolName || '') === 'shell' || /^\s*\[exit code:\s*\d+\]/i.test(String(resultText ?? ''))) return 'command-exit';
|
|
@@ -46,7 +46,7 @@ export const MAINTENANCE_SLOTS = Object.freeze(['explore', 'memory']);
|
|
|
46
46
|
export const PROFILE_LANGUAGES = Object.freeze([
|
|
47
47
|
{ id: 'system', label: 'System (locale)', prompt: null },
|
|
48
48
|
{ id: 'en', label: 'English', prompt: 'English' },
|
|
49
|
-
{ id: 'ko', label: '
|
|
49
|
+
{ id: 'ko', label: 'Korean', prompt: 'Korean' },
|
|
50
50
|
{ id: 'ja', label: '日本語', prompt: 'Japanese (日本語)' },
|
|
51
51
|
{ id: 'zh-Hans', label: '中文(简体)', prompt: 'Simplified Chinese (简体中文)' },
|
|
52
52
|
{ id: 'zh-Hant', label: '中文(繁體)', prompt: 'Traditional Chinese (繁體中文)' },
|
|
@@ -372,6 +372,27 @@ export function recoverPending(dataDir, notifyFn, { sessionId, priorSessionId, c
|
|
|
372
372
|
const entry = map[handle] || {};
|
|
373
373
|
const tool = entry.tool || 'dispatch';
|
|
374
374
|
const queries = Array.isArray(entry.queries) ? entry.queries : [];
|
|
375
|
+
// Determine the true owner session for this entry. A scoped recovery
|
|
376
|
+
// may have matched purely on clientHostPid (not on the owner session
|
|
377
|
+
// id); in that case we must NOT stamp the reconnecting filter session's
|
|
378
|
+
// id onto another session's abort — that injects an old-session abort
|
|
379
|
+
// into the wrong resumed session. Deliver to the true owner session, or
|
|
380
|
+
// leave the entry persisted when it carries no owner session to target.
|
|
381
|
+
const cid = entry.callerSessionId != null && String(entry.callerSessionId)
|
|
382
|
+
? String(entry.callerSessionId)
|
|
383
|
+
: null;
|
|
384
|
+
const ownerMatch = cid != null && (cid === filterSid || (priorSid != null && cid === priorSid));
|
|
385
|
+
if (scoped && !ownerMatch && cid == null) {
|
|
386
|
+
// hostPid-only match with no owner session id — cannot target a
|
|
387
|
+
// session safely. Leave persisted for a correctly-scoped recovery.
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
// Owner match → prefer the reconnecting filter session id (the owner's
|
|
391
|
+
// new session). When only priorSessionId matched and no current
|
|
392
|
+
// sessionId was supplied, filterSid is null — keep the entry's known
|
|
393
|
+
// owner `cid` for stamping/ack scoping rather than dropping it.
|
|
394
|
+
// Non-owner matches (hostPid-only) always stamp the entry's true owner.
|
|
395
|
+
const stampSid = (ownerMatch && filterSid) ? filterSid : cid;
|
|
375
396
|
// Single recovery mode: the worker was in flight at restart. Emit the
|
|
376
397
|
// Aborted boilerplate so the Lead can retry. Completed result bodies are
|
|
377
398
|
// never persisted, so there is nothing to replay here.
|
|
@@ -383,21 +404,23 @@ export function recoverPending(dataDir, notifyFn, { sessionId, priorSessionId, c
|
|
|
383
404
|
dispatch_id: handle,
|
|
384
405
|
tool,
|
|
385
406
|
error: String(isError),
|
|
386
|
-
...(
|
|
387
|
-
? { caller_session_id: filterSid }
|
|
388
|
-
: (entry.callerSessionId ? { caller_session_id: entry.callerSessionId } : {})),
|
|
407
|
+
...(stampSid ? { caller_session_id: stampSid } : {}),
|
|
389
408
|
...(filterHostPid > 0
|
|
390
409
|
? { client_host_pid: String(filterHostPid) }
|
|
391
410
|
: (entry.clientHostPid > 0 ? { client_host_pid: String(entry.clientHostPid) } : {})),
|
|
392
411
|
instruction: `Earlier ${tool} dispatch (${handle}) was aborted by a plugin restart. Retry if the answer is still needed.`,
|
|
393
412
|
};
|
|
394
413
|
try { process.stderr.write(`[dispatch-persist] recover handle=${handle} tool=${tool} kind=abort\n`); } catch { /* best-effort */ }
|
|
395
|
-
// Entry remains on disk until notifyFn
|
|
396
|
-
//
|
|
397
|
-
//
|
|
414
|
+
// Entry remains on disk until notifyFn settles as DELIVERED. Matching
|
|
415
|
+
// notifyToolCompletion settlement semantics (tool-execution-contract),
|
|
416
|
+
// only an explicit `false`/`0` resolve counts as undelivered and keeps
|
|
417
|
+
// the entry for retry; any other resolve (including `undefined`/void
|
|
418
|
+
// from a delivered notifyFn) removes it — otherwise it re-fires until
|
|
419
|
+
// TTL. A crash between fire and ack is likewise safe: the entry survives
|
|
420
|
+
// and recoverPending re-fires it on the next restart.
|
|
398
421
|
try {
|
|
399
|
-
Promise.resolve(notifyFn(content, meta)).then(() => {
|
|
400
|
-
removePending(dataDir, handle);
|
|
422
|
+
Promise.resolve(notifyFn(content, meta)).then((ok) => {
|
|
423
|
+
if (ok !== false && ok !== 0) removePending(dataDir, handle);
|
|
401
424
|
}).catch(() => { /* best-effort — entry stays for next recoverPending */ });
|
|
402
425
|
} catch { /* best-effort */ }
|
|
403
426
|
}
|
|
@@ -411,6 +411,14 @@ export async function beginOAuthLogin() {
|
|
|
411
411
|
};
|
|
412
412
|
const url = buildUrl(OAUTH_REDIRECT_URI);
|
|
413
413
|
const manualUrl = buildUrl(OAUTH_MANUAL_REDIRECT_URI);
|
|
414
|
+
const openLoginUrl = async (targetUrl, label = 'login') => {
|
|
415
|
+
try {
|
|
416
|
+
const { openInBrowser } = await import('../../../shared/open-url.mjs');
|
|
417
|
+
openInBrowser(targetUrl.toString());
|
|
418
|
+
} catch (err) {
|
|
419
|
+
process.stderr.write(`[anthropic-oauth] browser open failed for ${label} URL: ${String(err?.message || err).slice(0, 200)}\n`);
|
|
420
|
+
}
|
|
421
|
+
};
|
|
414
422
|
|
|
415
423
|
let server = null;
|
|
416
424
|
let timeout = null;
|
|
@@ -454,14 +462,12 @@ export async function beginOAuthLogin() {
|
|
|
454
462
|
timeout = setTimeout(() => finish(null), OAUTH_LOGIN_TIMEOUT_MS);
|
|
455
463
|
server.listen(OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_HOST, async () => {
|
|
456
464
|
process.stderr.write(`\n[anthropic-oauth] Open this URL to log in with Claude:\n${url.toString()}\n\nIf the localhost callback cannot complete, open this manual URL and paste the shown code#state:\n${manualUrl.toString()}\n\n`);
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
}
|
|
465
|
+
await openLoginUrl(url, 'callback');
|
|
466
|
+
});
|
|
467
|
+
server.on('error', async (err) => {
|
|
468
|
+
process.stderr.write(`\n[anthropic-oauth] localhost callback unavailable on ${OAUTH_CALLBACK_HOST}:${OAUTH_CALLBACK_PORT}: ${err?.message || err}\n[anthropic-oauth] Opening manual login URL instead. Paste the shown code#state:\n${manualUrl.toString()}\n\n`);
|
|
469
|
+
await openLoginUrl(manualUrl, 'manual');
|
|
463
470
|
});
|
|
464
|
-
server.on('error', (err) => finish(null, new Error(`[anthropic-oauth] callback server failed on ${OAUTH_CALLBACK_HOST}:${OAUTH_CALLBACK_PORT}: ${err?.message || err}`)));
|
|
465
471
|
});
|
|
466
472
|
|
|
467
473
|
return {
|
|
@@ -373,15 +373,15 @@ function toAnthropicMessages(messages) {
|
|
|
373
373
|
|
|
374
374
|
if (m.role === 'tool') {
|
|
375
375
|
const last = result[result.length - 1];
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
376
|
+
// Do not replay native deferred-loader `tool_reference` blocks from
|
|
377
|
+
// prior turns. They are tied to the exact Anthropic deferred-tool
|
|
378
|
+
// request that produced them; after /model or provider switches the
|
|
379
|
+
// new request may not carry the matching deferred tool surface and
|
|
380
|
+
// Anthropic rejects the history as a request validation error.
|
|
379
381
|
const block = {
|
|
380
382
|
type: 'tool_result',
|
|
381
383
|
tool_use_id: m.toolCallId || '',
|
|
382
|
-
content:
|
|
383
|
-
? refs.map((name) => ({ type: 'tool_reference', tool_name: name }))
|
|
384
|
-
: normalizeContentForAnthropic(m.content),
|
|
384
|
+
content: normalizeContentForAnthropic(m.content),
|
|
385
385
|
};
|
|
386
386
|
if (last?.role === 'user' && Array.isArray(last.content)) {
|
|
387
387
|
last.content.push(block);
|
|
@@ -376,15 +376,15 @@ function toAnthropicMessages(messages) {
|
|
|
376
376
|
}
|
|
377
377
|
if (m.role === 'tool') {
|
|
378
378
|
const last = result[result.length - 1];
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
379
|
+
// Do not replay native deferred-loader `tool_reference` blocks from
|
|
380
|
+
// prior turns. They are tied to the exact Anthropic deferred-tool
|
|
381
|
+
// request that produced them; after /model or provider switches the
|
|
382
|
+
// new request may not carry the matching deferred tool surface and
|
|
383
|
+
// Anthropic rejects the history as a request validation error.
|
|
382
384
|
const block = {
|
|
383
385
|
type: 'tool_result',
|
|
384
386
|
tool_use_id: m.toolCallId || '',
|
|
385
|
-
content:
|
|
386
|
-
? refs.map((name) => ({ type: 'tool_reference', tool_name: name }))
|
|
387
|
-
: normalizeContentForAnthropic(m.content),
|
|
387
|
+
content: normalizeContentForAnthropic(m.content),
|
|
388
388
|
};
|
|
389
389
|
if (last?.role === 'user' && Array.isArray(last.content)) {
|
|
390
390
|
last.content.push(block);
|
|
@@ -15,31 +15,61 @@ function pushUnique(list, value) {
|
|
|
15
15
|
if (!list.includes(value)) list.push(value);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// Short-TTL memo. Under simultaneous multi-agent launch, buildDefaultConfig
|
|
19
|
+
// and registry lazy-init call these probes in bursts; each probe does
|
|
20
|
+
// synchronous existsSync + readFileSync disk work. Cache the boolean per key
|
|
21
|
+
// for a brief window so one launch burst shares a single read instead of N.
|
|
22
|
+
// Kept short so the registry's "re-probe on each getProvider miss" self-heal
|
|
23
|
+
// (a credential file appearing/changing) is delayed by at most PROBE_TTL_MS.
|
|
24
|
+
const PROBE_TTL_MS = 3000;
|
|
25
|
+
const _probeCache = new Map();
|
|
26
|
+
function memoProbe(key, compute) {
|
|
27
|
+
const hit = _probeCache.get(key);
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
if (hit && now - hit.ts < PROBE_TTL_MS) return hit.value;
|
|
30
|
+
const value = compute();
|
|
31
|
+
_probeCache.set(key, { ts: now, value });
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
18
35
|
export function hasAnthropicOAuthCredentials() {
|
|
36
|
+
return memoProbe('anthropic-oauth', () => {
|
|
19
37
|
const paths = [];
|
|
38
|
+
const candidates = [];
|
|
20
39
|
pushUnique(paths, process.env.ANTHROPIC_OAUTH_CREDENTIALS_PATH);
|
|
21
40
|
pushUnique(paths, ANTHROPIC_DEFAULT_CREDENTIALS_PATH);
|
|
22
41
|
for (const path of paths) {
|
|
23
42
|
const raw = readJsonIfExists(path);
|
|
24
43
|
const oauth = raw?.claudeAiOauth;
|
|
25
|
-
if (oauth?.accessToken
|
|
26
|
-
|
|
44
|
+
if (oauth?.accessToken) {
|
|
45
|
+
candidates.push({
|
|
46
|
+
accessToken: oauth.accessToken,
|
|
47
|
+
expiresAt: Number(oauth.expiresAt ?? oauth.expires_at) || 0,
|
|
48
|
+
scopes: Array.isArray(oauth.scopes) ? oauth.scopes : [],
|
|
49
|
+
});
|
|
27
50
|
}
|
|
28
51
|
}
|
|
29
|
-
return false;
|
|
52
|
+
if (!candidates.length) return false;
|
|
53
|
+
candidates.sort((a, b) => (Number(b.expiresAt) || 0) - (Number(a.expiresAt) || 0));
|
|
54
|
+
const chosen = candidates[0];
|
|
55
|
+
return !!(chosen.accessToken && Array.isArray(chosen.scopes) && chosen.scopes.includes('user:inference'));
|
|
56
|
+
});
|
|
30
57
|
}
|
|
31
58
|
|
|
32
59
|
export function hasOpenAIOAuthCredentials() {
|
|
60
|
+
return memoProbe('openai-oauth', () => {
|
|
33
61
|
const paths = [join(resolvePluginData(), 'openai-oauth.json')];
|
|
34
62
|
for (const path of paths) {
|
|
35
63
|
const raw = readJsonIfExists(path);
|
|
36
|
-
|
|
37
|
-
if (tokens?.access_token && tokens?.refresh_token) return true;
|
|
64
|
+
if (raw?.access_token && raw?.refresh_token) return true;
|
|
38
65
|
}
|
|
39
66
|
return false;
|
|
67
|
+
});
|
|
40
68
|
}
|
|
41
69
|
|
|
42
70
|
export function hasGrokOAuthCredentials() {
|
|
71
|
+
return memoProbe('grok-oauth', () => {
|
|
43
72
|
const own = readJsonIfExists(join(resolvePluginData(), 'grok-oauth.json'));
|
|
44
73
|
return !!(own?.access_token && own?.refresh_token);
|
|
74
|
+
});
|
|
45
75
|
}
|
|
@@ -10,6 +10,7 @@ import { populateHttpStatusFromMessage } from './retry-classifier.mjs';
|
|
|
10
10
|
import { customToolCallFromResponseItem } from './custom-tool-wire.mjs';
|
|
11
11
|
import { createLeakGuard, createToolCallDedupe, dedupeToolCallList } from './anthropic-leaked-toolcall.mjs';
|
|
12
12
|
import { randomBytes } from 'crypto';
|
|
13
|
+
import { createActiveToolItemTracker } from './tool-stream-state.mjs';
|
|
13
14
|
|
|
14
15
|
// Synthesize a native-shaped OpenAI tool call from a recovered leaked call.
|
|
15
16
|
// Matches the `call_...` id scheme the native Responses/Chat paths use so the
|
|
@@ -554,14 +555,17 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
|
|
|
554
555
|
name: event.item.name || '',
|
|
555
556
|
callId: event.item.call_id || '',
|
|
556
557
|
});
|
|
558
|
+
state.toolTracker?.mark(event.item);
|
|
557
559
|
state.toolInFlight = true;
|
|
558
560
|
} else if (event.item?.type === 'custom_tool_call') {
|
|
561
|
+
state.toolTracker?.mark(event.item);
|
|
559
562
|
state.toolInFlight = true;
|
|
560
563
|
} else if (event.item?.type === 'tool_search_call') {
|
|
561
564
|
// Mark tool_search in-flight at item-added time, same as
|
|
562
565
|
// function_call/custom_tool_call above, so the stall-recovery
|
|
563
566
|
// pendingToolUse gate never drops a mid-flight tool_search
|
|
564
567
|
// before response.output_item.done pushes it.
|
|
568
|
+
state.toolTracker?.mark(event.item);
|
|
565
569
|
state.toolInFlight = true;
|
|
566
570
|
}
|
|
567
571
|
try { onStreamDelta?.(); } catch {}
|
|
@@ -569,6 +573,7 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
|
|
|
569
573
|
case 'response.function_call_arguments.delta':
|
|
570
574
|
// A tool call's args are streaming — mark tool work in-flight so a
|
|
571
575
|
// mid-args stall is NEVER accepted as a text-only partial-final.
|
|
576
|
+
state.toolTracker?.mark(null, event.item_id);
|
|
572
577
|
state.toolInFlight = true;
|
|
573
578
|
try { onStreamDelta?.(); } catch {}
|
|
574
579
|
break;
|
|
@@ -576,6 +581,7 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
|
|
|
576
581
|
// Custom-tool input streams before output_item.done records the call
|
|
577
582
|
// in pendingCalls; flag it so a mid-input stall gates out partial-
|
|
578
583
|
// final success (otherwise a tool-bearing turn looks text-only).
|
|
584
|
+
state.toolTracker?.mark(null, event.item_id);
|
|
579
585
|
state.toolInFlight = true;
|
|
580
586
|
try { onStreamDelta?.(); } catch {}
|
|
581
587
|
break;
|
|
@@ -616,10 +622,21 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
|
|
|
616
622
|
state.toolCalls.push(call);
|
|
617
623
|
emitCompatToolCallOnce(state, call, onToolCall);
|
|
618
624
|
}
|
|
625
|
+
// Drop the resolved function item from pendingCalls before
|
|
626
|
+
// recomputing toolInFlight — otherwise a completed call keeps
|
|
627
|
+
// pendingCalls.size > 0 and the latch never clears, so a later
|
|
628
|
+
// text-only stall stays wrongly gated as tool-bearing.
|
|
629
|
+
if (itemId) state.pendingCalls.delete(itemId);
|
|
630
|
+
state.toolTracker?.clear(item, itemId);
|
|
631
|
+
state.toolInFlight = state.pendingCalls.size > 0 || (state.toolTracker ? state.toolTracker.items.size > 0 : false);
|
|
619
632
|
} else if (item.type === 'tool_search_call') {
|
|
620
633
|
pushToolSearchCall(item);
|
|
634
|
+
state.toolTracker?.clear(item, item.id || '');
|
|
635
|
+
state.toolInFlight = state.pendingCalls.size > 0 || (state.toolTracker ? state.toolTracker.items.size > 0 : false);
|
|
621
636
|
} else if (item.type === 'custom_tool_call') {
|
|
622
637
|
pushCustomToolCall(item);
|
|
638
|
+
state.toolTracker?.clear(item, item.id || '');
|
|
639
|
+
state.toolInFlight = state.pendingCalls.size > 0 || (state.toolTracker ? state.toolTracker.items.size > 0 : false);
|
|
623
640
|
}
|
|
624
641
|
try { onStreamDelta?.(); } catch {}
|
|
625
642
|
break;
|
|
@@ -746,6 +763,14 @@ export async function consumeCompatResponsesStream(stream, {
|
|
|
746
763
|
pendingCalls: new Map(),
|
|
747
764
|
emittedToolCallKeys: new Set(),
|
|
748
765
|
emittedToolCall: false,
|
|
766
|
+
// Active tool-item / alias tracking shared with the WS + HTTP-SSE
|
|
767
|
+
// Responses streams (tool-stream-state.mjs): mark on output_item.added /
|
|
768
|
+
// arg-input deltas, clear on output_item.done. Unions id/call_id/item_id
|
|
769
|
+
// aliases so a mark under one key and a clear under another resolve to
|
|
770
|
+
// the same item — closes the custom-tool-input in-flight gap that a bare
|
|
771
|
+
// boolean toolInFlight latch could not (a mid-input stall now gates out
|
|
772
|
+
// text-only partial-final).
|
|
773
|
+
toolTracker: createActiveToolItemTracker(),
|
|
749
774
|
completed: false,
|
|
750
775
|
completedResponse: null,
|
|
751
776
|
sawOutput: false,
|
|
@@ -823,6 +848,7 @@ export async function consumeCompatResponsesStream(stream, {
|
|
|
823
848
|
|| leakedCalls.length > 0
|
|
824
849
|
|| (state.pendingCalls && state.pendingCalls.size > 0)
|
|
825
850
|
|| (Array.isArray(state.toolCalls) && state.toolCalls.length > 0)
|
|
851
|
+
|| (state.toolTracker && state.toolTracker.items.size > 0)
|
|
826
852
|
|| state.toolInFlight === true;
|
|
827
853
|
err.partialModel = state.model || undefined;
|
|
828
854
|
} catch { /* best-effort */ }
|
|
@@ -849,6 +875,7 @@ export async function consumeCompatResponsesStream(stream, {
|
|
|
849
875
|
|| leakedCalls.length > 0
|
|
850
876
|
|| (state.pendingCalls && state.pendingCalls.size > 0)
|
|
851
877
|
|| (Array.isArray(state.toolCalls) && state.toolCalls.length > 0)
|
|
878
|
+
|| (state.toolTracker && state.toolTracker.items.size > 0)
|
|
852
879
|
|| state.toolInFlight === true;
|
|
853
880
|
err.partialModel = state.model || undefined;
|
|
854
881
|
} catch { /* best-effort */ }
|
|
@@ -105,11 +105,26 @@ export function toOpenAITools(tools) {
|
|
|
105
105
|
}));
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
function functionToolFromSessionTool(t, name = t?.name) {
|
|
109
|
+
return {
|
|
110
|
+
type: 'function',
|
|
111
|
+
name,
|
|
112
|
+
description: t.description,
|
|
113
|
+
parameters: t.inputSchema || { type: 'object', additionalProperties: true },
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function toResponsesTools(tools, options = {}) {
|
|
118
|
+
const provider = String(options?.provider || '').toLowerCase();
|
|
119
|
+
const allowNativeToolSearch = options?.nativeToolSearch === true
|
|
120
|
+
|| (options?.nativeToolSearch !== false && provider !== 'xai');
|
|
109
121
|
return tools.map((t) => {
|
|
110
122
|
// load_tool advertises as the OpenAI-native `tool_search` wire type
|
|
111
|
-
// (legacy 'tool_search' name still accepted for back-compat).
|
|
123
|
+
// (legacy 'tool_search' name still accepted for back-compat). xAI/Grok
|
|
124
|
+
// Responses rejects that OpenAI-only variant ("unknown variant
|
|
125
|
+
// 'tool_search'"), so serialize it as an ordinary function there.
|
|
112
126
|
if (t?.name === 'load_tool' || t?.name === 'tool_search') {
|
|
127
|
+
if (!allowNativeToolSearch) return functionToolFromSessionTool(t, 'load_tool');
|
|
113
128
|
return {
|
|
114
129
|
type: 'tool_search',
|
|
115
130
|
execution: 'client',
|
|
@@ -122,12 +137,7 @@ export function toResponsesTools(tools) {
|
|
|
122
137
|
// tools (e.g. apply_patch) as ordinary function tools instead. Grammar
|
|
123
138
|
// tools may carry no usable inputSchema, so fall back to a permissive
|
|
124
139
|
// object schema so grok still registers a valid function tool.
|
|
125
|
-
return
|
|
126
|
-
type: 'function',
|
|
127
|
-
name: t.name,
|
|
128
|
-
description: t.description,
|
|
129
|
-
parameters: t.inputSchema || { type: 'object', additionalProperties: true },
|
|
130
|
-
};
|
|
140
|
+
return functionToolFromSessionTool(t);
|
|
131
141
|
});
|
|
132
142
|
}
|
|
133
143
|
|
|
@@ -271,18 +281,10 @@ export function collectCompatResponseSearchSources(response) {
|
|
|
271
281
|
|
|
272
282
|
export function toResponsesInputMessage(m, pendingToolMedia = null, customToolCallNameById = null) {
|
|
273
283
|
if (m.role === 'tool') {
|
|
274
|
-
if (Array.isArray(m.nativeToolSearch?.openaiTools)) {
|
|
275
|
-
return {
|
|
276
|
-
type: 'tool_search_output',
|
|
277
|
-
call_id: m.toolCallId || '',
|
|
278
|
-
status: 'completed',
|
|
279
|
-
execution: 'client',
|
|
280
|
-
tools: m.nativeToolSearch.openaiTools,
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
284
|
const { output, mediaContent } = splitToolContentForOpenAIResponses(m.content);
|
|
284
285
|
// xai path: never emit `custom_tool_call_output` (the `custom` variant
|
|
285
|
-
// is rejected by grok). Replay prior tool outputs
|
|
286
|
+
// is rejected by grok). Replay prior tool outputs — including old
|
|
287
|
+
// native tool_search outputs after /model switches — as the standard
|
|
286
288
|
// `function_call_output` item regardless of original native type.
|
|
287
289
|
const item = {
|
|
288
290
|
type: 'function_call_output',
|
|
@@ -296,25 +298,16 @@ export function toResponsesInputMessage(m, pendingToolMedia = null, customToolCa
|
|
|
296
298
|
const items = [];
|
|
297
299
|
if (m.content) items.push({ role: 'assistant', content: normalizeContentForOpenAIResponses(m.content, { role: 'assistant' }) });
|
|
298
300
|
for (const tc of m.toolCalls) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
// variant). tc.arguments already holds the recovered object
|
|
310
|
-
// form, so the same stringify path as regular calls applies.
|
|
311
|
-
items.push({
|
|
312
|
-
type: 'function_call',
|
|
313
|
-
call_id: tc.id,
|
|
314
|
-
name: tc.name,
|
|
315
|
-
arguments: JSON.stringify(tc.arguments || {}),
|
|
316
|
-
});
|
|
317
|
-
}
|
|
301
|
+
// xAI/Grok rejects OpenAI-only Responses variants such as
|
|
302
|
+
// `custom_tool_call` and `tool_search_call`, including when they
|
|
303
|
+
// appear in replayed history after a model switch. Replay all prior
|
|
304
|
+
// client tools as plain function calls.
|
|
305
|
+
items.push({
|
|
306
|
+
type: 'function_call',
|
|
307
|
+
call_id: tc.id,
|
|
308
|
+
name: tc.name === 'tool_search' ? 'load_tool' : tc.name,
|
|
309
|
+
arguments: JSON.stringify(tc.arguments || {}),
|
|
310
|
+
});
|
|
318
311
|
}
|
|
319
312
|
return items;
|
|
320
313
|
}
|
|
@@ -351,6 +344,7 @@ export function toXaiResponsesInput(messages, providerState, options = {}) {
|
|
|
351
344
|
startIndex = Math.max(0, Math.min(seen, messages.length));
|
|
352
345
|
if (messages[startIndex]?.role === 'assistant') startIndex += 1;
|
|
353
346
|
}
|
|
347
|
+
const dropToolHistory = !previousResponseId && (resetReason !== null || !state?.previousResponseId);
|
|
354
348
|
const input = [];
|
|
355
349
|
const pendingToolMedia = [];
|
|
356
350
|
const customToolCallNameById = new Map();
|
|
@@ -361,6 +355,11 @@ export function toXaiResponsesInput(messages, providerState, options = {}) {
|
|
|
361
355
|
for (const m of messages.slice(startIndex)) {
|
|
362
356
|
if (!includeSystem && m.role === 'system') continue;
|
|
363
357
|
if (m.role !== 'tool') flushToolMedia();
|
|
358
|
+
if (dropToolHistory && m.role === 'tool') continue;
|
|
359
|
+
if (dropToolHistory && m.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length > 0) {
|
|
360
|
+
if (m.content) input.push({ role: 'assistant', content: normalizeContentForOpenAIResponses(m.content, { role: 'assistant' }) });
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
364
363
|
const converted = toResponsesInputMessage(m, pendingToolMedia, customToolCallNameById);
|
|
365
364
|
if (Array.isArray(converted)) input.push(...converted);
|
|
366
365
|
else input.push(converted);
|
|
@@ -467,7 +467,7 @@ export class OpenAICompatProvider {
|
|
|
467
467
|
};
|
|
468
468
|
if (previousResponseId) params.previous_response_id = previousResponseId;
|
|
469
469
|
const nativeTools = nativeResponsesTools(opts);
|
|
470
|
-
if (tools?.length || nativeTools.length) params.tools = [...nativeTools, ...toResponsesTools(tools || [])];
|
|
470
|
+
if (tools?.length || nativeTools.length) params.tools = [...nativeTools, ...toResponsesTools(tools || [], { provider: 'xai' })];
|
|
471
471
|
// SSE transport: report 'requesting' until the stream opens, then
|
|
472
472
|
// per-chunk onStreamDelta feeds the agent stall watchdog.
|
|
473
473
|
try { opts.onStageChange?.('requesting'); } catch { /* heartbeat best-effort */ }
|
|
@@ -664,7 +664,7 @@ export class OpenAICompatProvider {
|
|
|
664
664
|
// first response already anchors instructions for the continuation.
|
|
665
665
|
else if (instructions) params.instructions = instructions;
|
|
666
666
|
const nativeTools = nativeResponsesTools(opts);
|
|
667
|
-
if (tools?.length || nativeTools.length) params.tools = [...nativeTools, ...toResponsesTools(tools || [])];
|
|
667
|
+
if (tools?.length || nativeTools.length) params.tools = [...nativeTools, ...toResponsesTools(tools || [], { provider: 'xai' })];
|
|
668
668
|
const reasoningEffort = normalizeXaiReasoningEffort(opts.xaiReasoningEffort
|
|
669
669
|
?? opts.effort
|
|
670
670
|
?? this.config?.reasoningEffort
|