discoclaw 1.2.3 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.context/voice.md +30 -2
- package/.env.example +6 -0
- package/dist/cli/dashboard.js +7 -1
- package/dist/config.js +7 -0
- package/dist/cron/executor.js +72 -1
- package/dist/dashboard/api/metrics.js +7 -0
- package/dist/dashboard/api/metrics.test.js +16 -0
- package/dist/dashboard/api/traces.js +14 -0
- package/dist/dashboard/api/traces.test.js +40 -0
- package/dist/dashboard/page.js +187 -8
- package/dist/dashboard/server.js +81 -14
- package/dist/dashboard/server.test.js +120 -4
- package/dist/discord/deferred-runner.js +306 -219
- package/dist/discord/message-coordinator.js +1 -28
- package/dist/discord/reaction-handler.js +81 -3
- package/dist/index.js +15 -1
- package/dist/observability/trace-store.js +56 -0
- package/dist/observability/trace-utils.js +31 -0
- package/dist/runtime/codex-cli.js +3 -2
- package/dist/runtime/codex-cli.test.js +33 -0
- package/dist/runtime/model-tiers.js +1 -1
- package/dist/runtime/model-tiers.test.js +9 -0
- package/dist/runtime/openai-tool-schemas.js +17 -0
- package/dist/voice/audio-pipeline.js +246 -6
- package/dist/voice/audio-pipeline.test.js +481 -0
- package/dist/voice/audio-receiver.js +8 -0
- package/dist/voice/audio-receiver.test.js +16 -0
- package/dist/voice/conversation-buffer.js +16 -6
- package/dist/voice/providers/gemini-live-provider.js +481 -0
- package/dist/voice/providers/gemini-live-provider.test.js +834 -0
- package/dist/voice/providers/gemini-live-responder.js +267 -0
- package/dist/voice/providers/gemini-live-responder.test.js +615 -0
- package/dist/voice/providers/gemini-live-token-estimator.js +100 -0
- package/dist/voice/providers/gemini-live-token-estimator.test.js +160 -0
- package/dist/voice/providers/gemini-live-types.js +32 -0
- package/dist/voice/providers/gemini-tool-mapper.js +91 -0
- package/dist/voice/providers/gemini-tool-mapper.test.js +253 -0
- package/dist/voice/providers/index.js +3 -0
- package/dist/voice/types.test.js +6 -0
- package/dist/voice/voice-prompt-builder.js +26 -17
- package/dist/voice/voice-prompt-builder.test.js +16 -1
- package/package.json +1 -1
- package/templates/instructions/SYSTEM_DEFAULTS.md +8 -0
|
@@ -21,6 +21,8 @@ import { downloadMessageImages, resolveMediaType } from './image-download.js';
|
|
|
21
21
|
import { downloadTextAttachments, classifyAttachments, downloadDocumentAttachments } from './file-download.js';
|
|
22
22
|
import { mapRuntimeErrorToUserMessage } from './user-errors.js';
|
|
23
23
|
import { globalMetrics } from '../observability/metrics.js';
|
|
24
|
+
import { globalTraceStore } from '../observability/trace-store.js';
|
|
25
|
+
import { summarizeTraceValue } from '../observability/trace-utils.js';
|
|
24
26
|
import { resolveModel } from '../runtime/model-tiers.js';
|
|
25
27
|
import { resolveGroundedToolCapabilities } from '../runtime/tool-capabilities.js';
|
|
26
28
|
import { adaptRuntimeEventText } from './runtime-event-text-adapter.js';
|
|
@@ -522,6 +524,9 @@ function createReactionHandler(mode, params, queue, statusRef) {
|
|
|
522
524
|
let currentPrompt = prompt;
|
|
523
525
|
let pendingFollowUp = null;
|
|
524
526
|
const actionHistory = [];
|
|
527
|
+
const traceId = `reaction_${randomUUID()}`;
|
|
528
|
+
let traceOutcome = 'success';
|
|
529
|
+
globalTraceStore.startTrace(traceId, sessionKey, 'reaction', reaction.message.channelId);
|
|
525
530
|
try {
|
|
526
531
|
// -- auto-follow-up loop --
|
|
527
532
|
while (true) {
|
|
@@ -606,8 +611,15 @@ function createReactionHandler(mode, params, queue, statusRef) {
|
|
|
606
611
|
const t0 = Date.now();
|
|
607
612
|
const previewMode = params.streamPreviewMode ?? 'compact';
|
|
608
613
|
const debugStreamPreviewLines = Boolean(params.debugStreamPreviewLines);
|
|
614
|
+
const toolStartTimesByName = new Map();
|
|
609
615
|
metrics.recordInvokeStart('reaction');
|
|
610
616
|
params.log?.info({ flow: 'reaction', sessionKey }, 'obs.invoke.start');
|
|
617
|
+
globalTraceStore.addEvent(traceId, {
|
|
618
|
+
type: 'invoke_start',
|
|
619
|
+
at: t0,
|
|
620
|
+
summary: followUpDepth === 0 ? 'initial invoke' : `follow-up ${followUpDepth}`,
|
|
621
|
+
promptPreview: summarizeTraceValue(currentPrompt, 220),
|
|
622
|
+
});
|
|
611
623
|
let invokeError = null;
|
|
612
624
|
let lastEditAt = 0;
|
|
613
625
|
let streamEditTimeoutStreak = 0;
|
|
@@ -819,10 +831,39 @@ function createReactionHandler(mode, params, queue, statusRef) {
|
|
|
819
831
|
lastEventAt = Date.now();
|
|
820
832
|
stallWarned = false;
|
|
821
833
|
lastStallProgressAt = 0;
|
|
822
|
-
if (evt.type === 'tool_start')
|
|
834
|
+
if (evt.type === 'tool_start') {
|
|
823
835
|
activeToolCount++;
|
|
824
|
-
|
|
836
|
+
const startedAt = Date.now();
|
|
837
|
+
const existingStarts = toolStartTimesByName.get(evt.name) ?? [];
|
|
838
|
+
existingStarts.push(startedAt);
|
|
839
|
+
toolStartTimesByName.set(evt.name, existingStarts);
|
|
840
|
+
globalTraceStore.addEvent(traceId, {
|
|
841
|
+
type: 'tool_start',
|
|
842
|
+
at: startedAt,
|
|
843
|
+
toolName: evt.name,
|
|
844
|
+
inputSummary: summarizeTraceValue(evt.input),
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
else if (evt.type === 'tool_end') {
|
|
825
848
|
activeToolCount = Math.max(0, activeToolCount - 1);
|
|
849
|
+
const endedAt = Date.now();
|
|
850
|
+
const existingStarts = toolStartTimesByName.get(evt.name) ?? [];
|
|
851
|
+
const startedAt = existingStarts.shift();
|
|
852
|
+
if (existingStarts.length > 0) {
|
|
853
|
+
toolStartTimesByName.set(evt.name, existingStarts);
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
toolStartTimesByName.delete(evt.name);
|
|
857
|
+
}
|
|
858
|
+
globalTraceStore.addEvent(traceId, {
|
|
859
|
+
type: 'tool_end',
|
|
860
|
+
at: endedAt,
|
|
861
|
+
toolName: evt.name,
|
|
862
|
+
ok: evt.ok,
|
|
863
|
+
durationMs: startedAt == null ? undefined : Math.max(0, endedAt - startedAt),
|
|
864
|
+
outputSummary: summarizeTraceValue(evt.output),
|
|
865
|
+
});
|
|
866
|
+
}
|
|
826
867
|
if (evt.type === 'text_final') {
|
|
827
868
|
hadTextFinal = true;
|
|
828
869
|
finalText = evt.text;
|
|
@@ -845,6 +886,14 @@ function createReactionHandler(mode, params, queue, statusRef) {
|
|
|
845
886
|
}
|
|
846
887
|
else if (evt.type === 'error') {
|
|
847
888
|
invokeError = evt.message;
|
|
889
|
+
traceOutcome = 'error';
|
|
890
|
+
globalTraceStore.addEvent(traceId, {
|
|
891
|
+
type: 'error',
|
|
892
|
+
at: Date.now(),
|
|
893
|
+
message: evt.message,
|
|
894
|
+
stage: 'runtime',
|
|
895
|
+
summary: followUpDepth === 0 ? 'initial invoke' : `follow-up ${followUpDepth}`,
|
|
896
|
+
});
|
|
848
897
|
metrics.recordInvokeResult('reaction', Date.now() - t0, false, evt.message);
|
|
849
898
|
params.log?.error({ sessionKey, error: evt.message }, `${logPrefix}:runtime error`);
|
|
850
899
|
params.log?.warn({ flow: 'reaction', sessionKey, error: evt.message }, 'obs.invoke.error');
|
|
@@ -864,6 +913,15 @@ function createReactionHandler(mode, params, queue, statusRef) {
|
|
|
864
913
|
streamEditQueue = Promise.resolve();
|
|
865
914
|
}
|
|
866
915
|
metrics.recordInvokeResult('reaction', Date.now() - t0, !invokeError, invokeError ?? undefined);
|
|
916
|
+
if (invokeError) {
|
|
917
|
+
traceOutcome = 'error';
|
|
918
|
+
}
|
|
919
|
+
globalTraceStore.addEvent(traceId, {
|
|
920
|
+
type: 'invoke_end',
|
|
921
|
+
at: Date.now(),
|
|
922
|
+
ok: !invokeError,
|
|
923
|
+
summary: followUpDepth === 0 ? 'initial invoke' : `follow-up ${followUpDepth}`,
|
|
924
|
+
});
|
|
867
925
|
params.log?.info({ flow: 'reaction', sessionKey, ms: Date.now() - t0, ok: !invokeError }, 'obs.invoke.end');
|
|
868
926
|
let processedText = finalText || deltaText || (collectedImages.length > 0 ? '' : '(no output)');
|
|
869
927
|
params.log?.info({ sessionKey, sessionId, ms: Date.now() - t0, hadError: Boolean(invokeError) }, `${logPrefix}:invoke:end`);
|
|
@@ -939,8 +997,18 @@ function createReactionHandler(mode, params, queue, statusRef) {
|
|
|
939
997
|
spawnCtx: params.spawnCtx,
|
|
940
998
|
});
|
|
941
999
|
actionResults = results;
|
|
942
|
-
for (
|
|
1000
|
+
for (let i = 0; i < results.length; i++) {
|
|
1001
|
+
const result = results[i];
|
|
943
1002
|
metrics.recordActionResult(result.ok);
|
|
1003
|
+
globalTraceStore.addEvent(traceId, {
|
|
1004
|
+
type: 'action_result',
|
|
1005
|
+
at: Date.now(),
|
|
1006
|
+
action: parsed.actions[i]?.type ?? 'unknown',
|
|
1007
|
+
ok: result.ok,
|
|
1008
|
+
detail: result.ok
|
|
1009
|
+
? summarizeTraceValue(result.summary, 220)
|
|
1010
|
+
: summarizeTraceValue(result.error, 220),
|
|
1011
|
+
});
|
|
944
1012
|
params.log?.info({ flow: 'reaction', sessionKey, ok: result.ok }, 'obs.action.result');
|
|
945
1013
|
}
|
|
946
1014
|
// Record action history for follow-up dedup.
|
|
@@ -1089,6 +1157,15 @@ function createReactionHandler(mode, params, queue, statusRef) {
|
|
|
1089
1157
|
} // end while (true)
|
|
1090
1158
|
}
|
|
1091
1159
|
catch (innerErr) {
|
|
1160
|
+
traceOutcome = 'error';
|
|
1161
|
+
globalTraceStore.addEvent(traceId, {
|
|
1162
|
+
type: 'error',
|
|
1163
|
+
at: Date.now(),
|
|
1164
|
+
message: innerErr instanceof Error ? innerErr.message : String(innerErr),
|
|
1165
|
+
name: innerErr instanceof Error ? innerErr.name : undefined,
|
|
1166
|
+
stack: innerErr instanceof Error ? summarizeTraceValue(innerErr.stack, 400) : undefined,
|
|
1167
|
+
stage: 'reaction_flow',
|
|
1168
|
+
});
|
|
1092
1169
|
// Inner catch: attempt to show the error in the reply before the finally
|
|
1093
1170
|
// block runs dispose(). Setting replyFinalized = true on success prevents
|
|
1094
1171
|
// the finally's safety-net delete from removing the error message.
|
|
@@ -1107,6 +1184,7 @@ function createReactionHandler(mode, params, queue, statusRef) {
|
|
|
1107
1184
|
throw innerErr;
|
|
1108
1185
|
}
|
|
1109
1186
|
finally {
|
|
1187
|
+
globalTraceStore.endTrace(traceId, traceOutcome);
|
|
1110
1188
|
// Safety net runs before dispose() so cold-start recovery can still see
|
|
1111
1189
|
// the in-flight entry if the delete fails.
|
|
1112
1190
|
if (!replyFinalized && reply && !isShuttingDown()) {
|
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ import { buildDurableMemorySection } from './discord/prompt-common.js';
|
|
|
19
19
|
import { parseDiscordActions, executeDiscordActions, buildTieredDiscordActionsPromptSection, buildAllResultLines, appendActionResults } from './discord/actions.js';
|
|
20
20
|
import { DiscordTransportClient } from './discord/transport-client.js';
|
|
21
21
|
import { buildVoiceActionFlags } from './voice/voice-action-flags.js';
|
|
22
|
-
import { loadVoiceIdentity, buildVoicePrompt, buildVoiceFollowUpPrompt, buildVoicePromptSectionEstimates } from './voice/voice-prompt-builder.js';
|
|
22
|
+
import { loadVoiceIdentity, buildVoicePrompt, buildVoiceFollowUpPrompt, buildVoicePromptSectionEstimates, buildVoiceSystemInstruction, } from './voice/voice-prompt-builder.js';
|
|
23
23
|
import { sanitizeForVoice, sanitizeVoiceReplyForSpeech } from './voice/voice-sanitize.js';
|
|
24
24
|
import { shouldTriggerFollowUp } from './discord/action-categories.js';
|
|
25
25
|
import { configureDeferredScheduler } from './discord/deferred-runner.js';
|
|
@@ -2077,6 +2077,15 @@ if (taskCtx) {
|
|
|
2077
2077
|
}
|
|
2078
2078
|
};
|
|
2079
2079
|
}
|
|
2080
|
+
const buildGeminiSystemInstruction = async () => {
|
|
2081
|
+
const identity = await loadVoiceIdentity(workspaceCwd);
|
|
2082
|
+
return buildVoiceSystemInstruction({
|
|
2083
|
+
identity,
|
|
2084
|
+
durableMemory: '',
|
|
2085
|
+
voiceSystemPrompt: cfg.voiceSystemPrompt,
|
|
2086
|
+
actionsSection: '',
|
|
2087
|
+
});
|
|
2088
|
+
};
|
|
2080
2089
|
audioPipeline = new AudioPipelineManager({
|
|
2081
2090
|
log,
|
|
2082
2091
|
voiceConfig: {
|
|
@@ -2093,14 +2102,19 @@ if (taskCtx) {
|
|
|
2093
2102
|
},
|
|
2094
2103
|
allowedUserIds: allowUserIds,
|
|
2095
2104
|
createDecoder: opusDecoderFactory,
|
|
2105
|
+
voiceProvider: cfg.voicePipelineProvider,
|
|
2106
|
+
geminiApiKey: cfg.geminiApiKey,
|
|
2107
|
+
enabledTools: runtimeTools,
|
|
2096
2108
|
invokeAi: voiceInvokeAi,
|
|
2097
2109
|
runtime: voiceRuntimeRef.runtime.id,
|
|
2098
2110
|
runtimeModel: voiceModelRef.model,
|
|
2099
2111
|
runtimeCwd: workspaceCwd,
|
|
2100
2112
|
runtimeTimeoutMs,
|
|
2113
|
+
sessionRotationMs: cfg.geminiSessionRotationMs,
|
|
2101
2114
|
transcriptMirror,
|
|
2102
2115
|
botDisplayName,
|
|
2103
2116
|
backfill,
|
|
2117
|
+
buildGeminiSystemInstruction,
|
|
2104
2118
|
onTranscription: (guildId, result) => {
|
|
2105
2119
|
if (result.isFinal && result.text.trim()) {
|
|
2106
2120
|
log.info({ guildId, text: result.text, confidence: result.confidence }, 'voice:transcription');
|
|
@@ -88,6 +88,62 @@ export class TraceStore {
|
|
|
88
88
|
.slice(0, limit)
|
|
89
89
|
.map(cloneTrace);
|
|
90
90
|
}
|
|
91
|
+
get size() {
|
|
92
|
+
return this.traces.size;
|
|
93
|
+
}
|
|
94
|
+
summary() {
|
|
95
|
+
const allTraces = [...this.traces.values()];
|
|
96
|
+
const flows = ['message', 'reaction', 'cron', 'defer'];
|
|
97
|
+
const byFlow = {};
|
|
98
|
+
for (const flow of flows) {
|
|
99
|
+
const matching = allTraces.filter((t) => t.flow === flow);
|
|
100
|
+
const completed = matching.filter((t) => t.outcome !== 'in_progress');
|
|
101
|
+
const succeeded = completed.filter((t) => t.outcome === 'success').length;
|
|
102
|
+
const totalDuration = completed.reduce((sum, t) => sum + t.durationMs, 0);
|
|
103
|
+
byFlow[flow] = {
|
|
104
|
+
total: matching.length,
|
|
105
|
+
succeeded,
|
|
106
|
+
failed: completed.length - succeeded,
|
|
107
|
+
inProgress: matching.length - completed.length,
|
|
108
|
+
avgDurationMs: completed.length > 0 ? Math.round(totalDuration / completed.length) : 0,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const byOutcome = {};
|
|
112
|
+
for (const trace of allTraces) {
|
|
113
|
+
byOutcome[trace.outcome] = (byOutcome[trace.outcome] ?? 0) + 1;
|
|
114
|
+
}
|
|
115
|
+
const MAX_RECENT_ERRORS = 10;
|
|
116
|
+
const recentErrors = [];
|
|
117
|
+
const sorted = [...allTraces].sort((a, b) => b.startedAt - a.startedAt);
|
|
118
|
+
for (const trace of sorted) {
|
|
119
|
+
if (recentErrors.length >= MAX_RECENT_ERRORS)
|
|
120
|
+
break;
|
|
121
|
+
if (trace.outcome === 'success' || trace.outcome === 'in_progress')
|
|
122
|
+
continue;
|
|
123
|
+
const lastError = [...trace.events].reverse().find((e) => e.type === 'error');
|
|
124
|
+
recentErrors.push({
|
|
125
|
+
traceId: trace.traceId,
|
|
126
|
+
flow: trace.flow,
|
|
127
|
+
message: lastError && 'message' in lastError ? lastError.message : trace.outcome,
|
|
128
|
+
at: trace.startedAt,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
let oldestAt = null;
|
|
132
|
+
let newestAt = null;
|
|
133
|
+
if (allTraces.length > 0) {
|
|
134
|
+
oldestAt = Math.min(...allTraces.map((t) => t.startedAt));
|
|
135
|
+
newestAt = Math.max(...allTraces.map((t) => t.startedAt));
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
total: allTraces.length,
|
|
139
|
+
inProgress: allTraces.filter((t) => t.outcome === 'in_progress').length,
|
|
140
|
+
oldestAt,
|
|
141
|
+
newestAt,
|
|
142
|
+
byFlow,
|
|
143
|
+
byOutcome,
|
|
144
|
+
recentErrors,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
91
147
|
makeRoomForNewTrace() {
|
|
92
148
|
this.pruneToLimit(this.maxEntries - 1);
|
|
93
149
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared trace-event helpers for summarizing values before they enter trace events.
|
|
3
|
+
*/
|
|
4
|
+
export function summarizeTraceText(value, maxChars = 160) {
|
|
5
|
+
const normalized = value.replace(/\s+/g, ' ').trim();
|
|
6
|
+
if (!normalized) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
if (normalized.length <= maxChars) {
|
|
10
|
+
return normalized;
|
|
11
|
+
}
|
|
12
|
+
return `${normalized.slice(0, Math.max(1, maxChars - 1))}…`;
|
|
13
|
+
}
|
|
14
|
+
export function summarizeTraceValue(value, maxChars = 160) {
|
|
15
|
+
if (value == null) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
if (typeof value === 'string') {
|
|
19
|
+
return summarizeTraceText(value, maxChars);
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const serialized = JSON.stringify(value);
|
|
23
|
+
if (serialized) {
|
|
24
|
+
return summarizeTraceText(serialized, maxChars);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Fall through to String(value).
|
|
29
|
+
}
|
|
30
|
+
return summarizeTraceText(String(value), maxChars);
|
|
31
|
+
}
|
|
@@ -21,7 +21,8 @@ function mergeSystemPrompt(systemPrompt, appendSystemPrompt) {
|
|
|
21
21
|
function normalizeInvokeParams(params, opts) {
|
|
22
22
|
const requestedModel = params.model || opts.defaultModel;
|
|
23
23
|
const remappedModel = remapCrossRuntimeTierModel(requestedModel, 'codex');
|
|
24
|
-
const
|
|
24
|
+
const normalizedModel = remappedModel?.model ?? requestedModel;
|
|
25
|
+
const effectiveModel = normalizedModel || opts.defaultModel;
|
|
25
26
|
const effectiveReasoningEffort = params.reasoningEffort
|
|
26
27
|
?? (remappedModel ? resolveReasoningEffort(remappedModel.sourceTier, 'codex') : undefined);
|
|
27
28
|
if (remappedModel) {
|
|
@@ -34,7 +35,7 @@ function normalizeInvokeParams(params, opts) {
|
|
|
34
35
|
}
|
|
35
36
|
return {
|
|
36
37
|
...params,
|
|
37
|
-
model:
|
|
38
|
+
model: normalizedModel,
|
|
38
39
|
...(effectiveReasoningEffort ? { reasoningEffort: effectiveReasoningEffort } : {}),
|
|
39
40
|
systemPrompt: mergeSystemPrompt(params.systemPrompt, opts.appendSystemPrompt),
|
|
40
41
|
...(opts.disableSessions ? { sessionKey: undefined } : {}),
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import { CODEX_RUNTIME_CAPABILITIES } from './tool-capabilities.js';
|
|
5
|
+
import { initTierOverrides } from './model-tiers.js';
|
|
5
6
|
const { mockExeca } = vi.hoisted(() => {
|
|
6
7
|
return {
|
|
7
8
|
mockExeca: vi.fn(),
|
|
@@ -129,10 +130,12 @@ describe('Codex CLI runtime adapter', () => {
|
|
|
129
130
|
const originalStableHome = process.env.DISCOCLAW_CODEX_STABLE_HOME;
|
|
130
131
|
beforeEach(() => {
|
|
131
132
|
mockExeca.mockReset();
|
|
133
|
+
initTierOverrides({});
|
|
132
134
|
delete process.env.DISCOCLAW_CLI_LAUNCHER_STATE_HARDENING;
|
|
133
135
|
delete process.env.DISCOCLAW_CODEX_STABLE_HOME;
|
|
134
136
|
});
|
|
135
137
|
afterEach(() => {
|
|
138
|
+
initTierOverrides({});
|
|
136
139
|
if (originalHardening === undefined) {
|
|
137
140
|
delete process.env.DISCOCLAW_CLI_LAUNCHER_STATE_HARDENING;
|
|
138
141
|
}
|
|
@@ -294,6 +297,36 @@ describe('Codex CLI runtime adapter', () => {
|
|
|
294
297
|
sourceTier: 'fast',
|
|
295
298
|
}), 'codex:model remapped to codex-compatible tier default');
|
|
296
299
|
});
|
|
300
|
+
it('falls back to the codex default model when the codex fast tier is configured as adapter-default sentinel', async () => {
|
|
301
|
+
initTierOverrides({ DISCOCLAW_TIER_CODEX_FAST: '' });
|
|
302
|
+
const log = {
|
|
303
|
+
debug: vi.fn(),
|
|
304
|
+
warn: vi.fn(),
|
|
305
|
+
};
|
|
306
|
+
mockExeca.mockReturnValue(createMockSubprocess({
|
|
307
|
+
stdout: 'cli remap ok',
|
|
308
|
+
exitCode: 0,
|
|
309
|
+
}));
|
|
310
|
+
const rt = createCodexCliRuntime({
|
|
311
|
+
codexBin: 'codex',
|
|
312
|
+
defaultModel: 'gpt-5.4',
|
|
313
|
+
log,
|
|
314
|
+
});
|
|
315
|
+
await collectEvents(rt.invoke({
|
|
316
|
+
prompt: 'Summarize',
|
|
317
|
+
model: 'gpt-5-mini',
|
|
318
|
+
cwd: '/tmp/non-default-cwd',
|
|
319
|
+
}));
|
|
320
|
+
const callArgs = mockExeca.mock.calls[0][1];
|
|
321
|
+
const modelIdx = callArgs.indexOf('-m');
|
|
322
|
+
expect(callArgs[modelIdx + 1]).toBe('gpt-5.4');
|
|
323
|
+
expect(log.warn).toHaveBeenCalledWith(expect.objectContaining({
|
|
324
|
+
requestedModel: 'gpt-5-mini',
|
|
325
|
+
effectiveModel: 'gpt-5.4',
|
|
326
|
+
sourceRuntimeId: 'openai',
|
|
327
|
+
sourceTier: 'fast',
|
|
328
|
+
}), 'codex:model remapped to codex-compatible tier default');
|
|
329
|
+
});
|
|
297
330
|
it('large prompt uses stdin instead of positional arg', async () => {
|
|
298
331
|
const largePrompt = 'x'.repeat(200_000);
|
|
299
332
|
mockExeca.mockReturnValue(createMockSubprocess({
|
|
@@ -168,7 +168,7 @@ export function remapCrossRuntimeTierModel(model, targetRuntimeId) {
|
|
|
168
168
|
if (!sourceTier)
|
|
169
169
|
return null;
|
|
170
170
|
const targetModel = tierMap[targetRuntimeId]?.[sourceTier];
|
|
171
|
-
if (
|
|
171
|
+
if (targetModel === undefined || normalizeModelLookup(targetModel) === normalized)
|
|
172
172
|
return null;
|
|
173
173
|
return {
|
|
174
174
|
sourceRuntimeId,
|
|
@@ -238,6 +238,15 @@ describe('remapCrossRuntimeTierModel', () => {
|
|
|
238
238
|
model: 'gpt-5.4',
|
|
239
239
|
});
|
|
240
240
|
});
|
|
241
|
+
it('preserves adapter-default sentinels when the target runtime tier is intentionally blank', () => {
|
|
242
|
+
initTierOverrides({ DISCOCLAW_TIER_CODEX_FAST: '' });
|
|
243
|
+
expect(remapCrossRuntimeTierModel('gpt-5-mini', 'codex')).toEqual({
|
|
244
|
+
sourceRuntimeId: 'openai',
|
|
245
|
+
sourceTier: 'fast',
|
|
246
|
+
targetRuntimeId: 'codex',
|
|
247
|
+
model: '',
|
|
248
|
+
});
|
|
249
|
+
});
|
|
241
250
|
it('does not remap ambiguous cross-runtime models', () => {
|
|
242
251
|
expect(remapCrossRuntimeTierModel('gpt-5.4', 'codex')).toBeNull();
|
|
243
252
|
});
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Maps internal tool names (Read, Write, …) to OpenAI function names
|
|
5
5
|
* (read_file, write_file, …) and provides JSON Schema parameter definitions.
|
|
6
|
+
*
|
|
7
|
+
* Also provides `buildGeminiToolDeclarations()` to produce Gemini-compatible
|
|
8
|
+
* function declarations for the Multimodal Live API (Phase 2.1).
|
|
6
9
|
*/
|
|
10
|
+
import { toGeminiTools } from '../voice/providers/gemini-tool-mapper.js';
|
|
7
11
|
/** Discoclaw tool name → OpenAI function names */
|
|
8
12
|
const DISCO_TO_OPENAI_NAMES = {
|
|
9
13
|
Read: ['read_file'],
|
|
@@ -369,3 +373,16 @@ export function buildToolSchemas(enabledTools) {
|
|
|
369
373
|
}
|
|
370
374
|
return schemas;
|
|
371
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Build Gemini-compatible function declarations for the given enabled tools.
|
|
378
|
+
*
|
|
379
|
+
* Composes `buildToolSchemas()` → `toGeminiTools()` so callers (e.g.
|
|
380
|
+
* AudioPipelineManager) can get Gemini Live–ready declarations in one call.
|
|
381
|
+
* Returns `undefined` when no tools match (same semantics as `toGeminiTools`).
|
|
382
|
+
*/
|
|
383
|
+
export function buildGeminiToolDeclarations(enabledTools, opts = {}) {
|
|
384
|
+
const schemas = buildToolSchemas(enabledTools);
|
|
385
|
+
return toGeminiTools(schemas, opts.nonBlocking
|
|
386
|
+
? { nonBlockingFunctionNames: schemas.map((schema) => schema.function.name) }
|
|
387
|
+
: undefined);
|
|
388
|
+
}
|