@dotsetlabs/dotclaw 1.5.2 → 1.6.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/README.md +6 -3
- package/config-examples/runtime.json +9 -8
- package/config-examples/tool-policy.json +6 -0
- package/container/agent-runner/package-lock.json +2 -2
- package/container/agent-runner/package.json +1 -1
- package/container/agent-runner/src/agent-config.ts +6 -13
- package/container/agent-runner/src/container-protocol.ts +0 -6
- package/container/agent-runner/src/id.ts +4 -0
- package/container/agent-runner/src/index.ts +175 -178
- package/container/agent-runner/src/ipc.ts +3 -15
- package/container/agent-runner/src/prompt-packs.ts +5 -209
- package/container/agent-runner/src/tools.ts +6 -5
- package/dist/agent-execution.d.ts +0 -6
- package/dist/agent-execution.d.ts.map +1 -1
- package/dist/agent-execution.js +2 -2
- package/dist/agent-execution.js.map +1 -1
- package/dist/background-jobs.d.ts +1 -0
- package/dist/background-jobs.d.ts.map +1 -1
- package/dist/background-jobs.js +18 -3
- package/dist/background-jobs.js.map +1 -1
- package/dist/behavior-config.d.ts +0 -1
- package/dist/behavior-config.d.ts.map +1 -1
- package/dist/behavior-config.js +0 -3
- package/dist/behavior-config.js.map +1 -1
- package/dist/cli.js +294 -41
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/container-protocol.d.ts +0 -6
- package/dist/container-protocol.d.ts.map +1 -1
- package/dist/container-runner.d.ts +5 -0
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +44 -2
- package/dist/container-runner.js.map +1 -1
- package/dist/dashboard.js +1 -1
- package/dist/dashboard.js.map +1 -1
- package/dist/db.d.ts +19 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +130 -28
- package/dist/db.js.map +1 -1
- package/dist/id.d.ts +2 -0
- package/dist/id.d.ts.map +1 -0
- package/dist/id.js +4 -0
- package/dist/id.js.map +1 -0
- package/dist/index.js +152 -272
- package/dist/index.js.map +1 -1
- package/dist/json-helpers.d.ts +1 -0
- package/dist/json-helpers.d.ts.map +1 -1
- package/dist/json-helpers.js +33 -1
- package/dist/json-helpers.js.map +1 -1
- package/dist/maintenance.d.ts +1 -0
- package/dist/maintenance.d.ts.map +1 -1
- package/dist/maintenance.js +13 -3
- package/dist/maintenance.js.map +1 -1
- package/dist/memory-embeddings.d.ts +1 -0
- package/dist/memory-embeddings.d.ts.map +1 -1
- package/dist/memory-embeddings.js +10 -1
- package/dist/memory-embeddings.js.map +1 -1
- package/dist/memory-store.d.ts.map +1 -1
- package/dist/memory-store.js +2 -1
- package/dist/memory-store.js.map +1 -1
- package/dist/metrics.d.ts +1 -0
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +16 -2
- package/dist/metrics.js.map +1 -1
- package/dist/paths.d.ts +4 -2
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +4 -2
- package/dist/paths.js.map +1 -1
- package/dist/runtime-config.d.ts +4 -7
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +13 -16
- package/dist/runtime-config.js.map +1 -1
- package/dist/task-scheduler.d.ts +1 -0
- package/dist/task-scheduler.d.ts.map +1 -1
- package/dist/task-scheduler.js +10 -1
- package/dist/task-scheduler.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -1
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
MemoryConfig,
|
|
27
27
|
Message
|
|
28
28
|
} from './memory.js';
|
|
29
|
-
import { loadPromptPackWithCanary,
|
|
29
|
+
import { loadPromptPackWithCanary, formatPromptPack, PromptPack } from './prompt-packs.js';
|
|
30
30
|
|
|
31
31
|
type OpenRouterResult = ReturnType<OpenRouter['callModel']>;
|
|
32
32
|
|
|
@@ -71,72 +71,64 @@ function log(message: string): void {
|
|
|
71
71
|
console.error(`[agent-runner] ${message}`);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const record = content as { text?: unknown; content?: unknown; value?: unknown };
|
|
92
|
-
if (typeof record.text === 'string') return record.text;
|
|
93
|
-
if (typeof record.content === 'string') return record.content;
|
|
94
|
-
if (typeof record.value === 'string') return record.value;
|
|
95
|
-
}
|
|
96
|
-
return '';
|
|
74
|
+
// ── Response extraction pipeline ─────────────────────────────────────
|
|
75
|
+
// OpenRouter SDK v0.3.x returns raw response IDs (gen-*, resp-*, etc.) instead
|
|
76
|
+
// of text for fast reasoning models (GPT-5-mini/nano). Reasoning tokens consume
|
|
77
|
+
// the output budget, leaving nothing for actual text. This multi-layer pipeline
|
|
78
|
+
// works around that:
|
|
79
|
+
// 1. isLikelyResponseId — detect leaked IDs so we never surface them
|
|
80
|
+
// 2. extractTextFromRawResponse — walk raw response fields ourselves
|
|
81
|
+
// 3. getTextWithFallback — try SDK getText(), fall back to raw extraction
|
|
82
|
+
// 4. chatCompletionsFallback — retry via /chat/completions when all else fails
|
|
83
|
+
// Remove this pipeline once the SDK reliably returns text for reasoning models.
|
|
84
|
+
|
|
85
|
+
const RESPONSE_ID_PREFIXES = ['gen-', 'resp-', 'resp_', 'chatcmpl-', 'msg_'];
|
|
86
|
+
|
|
87
|
+
function isLikelyResponseId(value: string): boolean {
|
|
88
|
+
const trimmed = value.trim();
|
|
89
|
+
if (!trimmed || trimmed.includes(' ') || trimmed.includes('\n')) return false;
|
|
90
|
+
return RESPONSE_ID_PREFIXES.some(prefix => trimmed.startsWith(prefix));
|
|
97
91
|
}
|
|
98
92
|
|
|
99
|
-
function
|
|
93
|
+
function isValidText(value: unknown): value is string {
|
|
94
|
+
return typeof value === 'string' && value.trim().length > 0 && !isLikelyResponseId(value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function extractTextFromRawResponse(response: unknown): string {
|
|
100
98
|
if (!response || typeof response !== 'object') return '';
|
|
101
|
-
const record = response as
|
|
102
|
-
outputText?: unknown;
|
|
103
|
-
output_text?: unknown;
|
|
104
|
-
output?: unknown;
|
|
105
|
-
choices?: unknown;
|
|
106
|
-
};
|
|
99
|
+
const record = response as Record<string, unknown>;
|
|
107
100
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
if (typeof record.output_text === 'string' && record.output_text.trim()) {
|
|
112
|
-
return record.output_text;
|
|
113
|
-
}
|
|
101
|
+
// 1. SDK-parsed camelCase field
|
|
102
|
+
if (isValidText(record.outputText)) return record.outputText;
|
|
114
103
|
|
|
104
|
+
// 2. Raw API snake_case field
|
|
105
|
+
if (isValidText(record.output_text)) return record.output_text;
|
|
106
|
+
|
|
107
|
+
// 3. Walk response.output[] for message/output_text items
|
|
115
108
|
if (Array.isArray(record.output)) {
|
|
116
|
-
const
|
|
109
|
+
const parts: string[] = [];
|
|
117
110
|
for (const item of record.output) {
|
|
118
111
|
if (!item || typeof item !== 'object') continue;
|
|
119
|
-
const typed = item as { type?:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
112
|
+
const typed = item as { type?: string; content?: unknown; text?: string };
|
|
113
|
+
if (typed.type === 'message' && Array.isArray(typed.content)) {
|
|
114
|
+
for (const part of typed.content as Array<{ type?: string; text?: string }>) {
|
|
115
|
+
if (part?.type === 'output_text' && isValidText(part.text)) {
|
|
116
|
+
parts.push(part.text);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} else if (typed.type === 'output_text' && isValidText(typed.text)) {
|
|
120
|
+
parts.push(typed.text);
|
|
126
121
|
}
|
|
127
122
|
}
|
|
128
|
-
const joined =
|
|
123
|
+
const joined = parts.join('');
|
|
129
124
|
if (joined.trim()) return joined;
|
|
130
125
|
}
|
|
131
126
|
|
|
127
|
+
// 4. OpenAI chat completions compat
|
|
132
128
|
if (Array.isArray(record.choices) && record.choices.length > 0) {
|
|
133
|
-
const choice = record.choices[0] as { message?: { content?: unknown }
|
|
134
|
-
if (choice?.message) {
|
|
135
|
-
|
|
136
|
-
if (text.trim()) return text;
|
|
137
|
-
}
|
|
138
|
-
if (typeof choice?.text === 'string' && choice.text.trim()) {
|
|
139
|
-
return choice.text;
|
|
129
|
+
const choice = record.choices[0] as { message?: { content?: unknown } } | null | undefined;
|
|
130
|
+
if (choice?.message && isValidText(choice.message.content)) {
|
|
131
|
+
return choice.message.content;
|
|
140
132
|
}
|
|
141
133
|
}
|
|
142
134
|
|
|
@@ -144,22 +136,103 @@ function extractTextFallbackFromResponse(response: unknown): string {
|
|
|
144
136
|
}
|
|
145
137
|
|
|
146
138
|
async function getTextWithFallback(result: OpenRouterResult, context: string): Promise<string> {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
139
|
+
// 1. Try the SDK's proper getText() first — this handles tool execution and
|
|
140
|
+
// extracts text from the final response via the SDK's own logic.
|
|
141
|
+
try {
|
|
142
|
+
const text = await result.getText();
|
|
143
|
+
if (isValidText(text)) {
|
|
144
|
+
return text;
|
|
145
|
+
}
|
|
146
|
+
if (text && isLikelyResponseId(text)) {
|
|
147
|
+
log(`Ignored response id from getText (${context}): ${String(text).slice(0, 60)}`);
|
|
148
|
+
}
|
|
149
|
+
} catch (err) {
|
|
150
|
+
log(`getText failed (${context}): ${err instanceof Error ? err.message : String(err)}`);
|
|
150
151
|
}
|
|
152
|
+
|
|
153
|
+
// 2. Fall back to raw response extraction — walk known fields ourselves
|
|
151
154
|
try {
|
|
152
155
|
const response = await result.getResponse();
|
|
153
|
-
const fallbackText =
|
|
154
|
-
if (fallbackText
|
|
155
|
-
log(`Recovered
|
|
156
|
+
const fallbackText = extractTextFromRawResponse(response);
|
|
157
|
+
if (fallbackText) {
|
|
158
|
+
log(`Recovered text from raw response (${context})`);
|
|
156
159
|
return fallbackText;
|
|
157
160
|
}
|
|
158
|
-
|
|
161
|
+
const r = response as Record<string, unknown>;
|
|
162
|
+
const outputLen = Array.isArray(r.output) ? (r.output as unknown[]).length : 0;
|
|
163
|
+
log(`No text in raw response (${context}): id=${String(r.id ?? 'none').slice(0, 40)} status=${String(r.status ?? '?')} outputs=${outputLen}`);
|
|
159
164
|
} catch (err) {
|
|
160
|
-
log(`
|
|
165
|
+
log(`Raw response extraction failed (${context}): ${err instanceof Error ? err.message : String(err)}`);
|
|
161
166
|
}
|
|
162
|
-
|
|
167
|
+
|
|
168
|
+
// 3. Never return a response ID
|
|
169
|
+
return '';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Direct Chat Completions API fallback.
|
|
174
|
+
* When the Responses API returns a gen-ID instead of text (common with fast
|
|
175
|
+
* models like gpt-5-nano/mini via OpenRouter), retry using the standard
|
|
176
|
+
* /chat/completions endpoint which reliably returns text content.
|
|
177
|
+
*/
|
|
178
|
+
async function chatCompletionsFallback(params: {
|
|
179
|
+
model: string;
|
|
180
|
+
instructions: string;
|
|
181
|
+
messages: Array<{ role: string; content: string }>;
|
|
182
|
+
maxOutputTokens: number;
|
|
183
|
+
temperature: number;
|
|
184
|
+
}): Promise<string> {
|
|
185
|
+
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
186
|
+
if (!apiKey) return '';
|
|
187
|
+
|
|
188
|
+
const headers: Record<string, string> = {
|
|
189
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
190
|
+
'Content-Type': 'application/json'
|
|
191
|
+
};
|
|
192
|
+
if (agent.openrouter.siteUrl) {
|
|
193
|
+
headers['HTTP-Referer'] = agent.openrouter.siteUrl;
|
|
194
|
+
}
|
|
195
|
+
if (agent.openrouter.siteName) {
|
|
196
|
+
headers['X-Title'] = agent.openrouter.siteName;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const chatMessages = [
|
|
200
|
+
{ role: 'system', content: params.instructions },
|
|
201
|
+
...params.messages
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
log(`Chat Completions fallback: model=${params.model}, messages=${chatMessages.length}`);
|
|
205
|
+
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
206
|
+
method: 'POST',
|
|
207
|
+
headers,
|
|
208
|
+
body: JSON.stringify({
|
|
209
|
+
model: params.model,
|
|
210
|
+
messages: chatMessages,
|
|
211
|
+
max_completion_tokens: params.maxOutputTokens,
|
|
212
|
+
temperature: params.temperature,
|
|
213
|
+
reasoning_effort: 'low'
|
|
214
|
+
}),
|
|
215
|
+
signal: AbortSignal.timeout(agent.openrouter.timeoutMs)
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const bodyText = await response.text();
|
|
219
|
+
if (!response.ok) {
|
|
220
|
+
log(`Chat Completions fallback HTTP ${response.status}: ${bodyText.slice(0, 300)}`);
|
|
221
|
+
return '';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const data = JSON.parse(bodyText);
|
|
226
|
+
const content = data?.choices?.[0]?.message?.content;
|
|
227
|
+
if (isValidText(content)) {
|
|
228
|
+
log(`Chat Completions fallback recovered text (${String(content).length} chars)`);
|
|
229
|
+
return content;
|
|
230
|
+
}
|
|
231
|
+
log(`Chat Completions fallback returned no text: ${JSON.stringify(data).slice(0, 300)}`);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
log(`Chat Completions fallback parse error: ${err instanceof Error ? err.message : String(err)}`);
|
|
234
|
+
}
|
|
235
|
+
return '';
|
|
163
236
|
}
|
|
164
237
|
|
|
165
238
|
function writeOutput(output: ContainerOutput): void {
|
|
@@ -536,53 +609,15 @@ function buildSystemInstructions(params: {
|
|
|
536
609
|
? `Job artifacts directory: /workspace/group/jobs/${params.jobId}`
|
|
537
610
|
: '';
|
|
538
611
|
|
|
539
|
-
const
|
|
540
|
-
?
|
|
541
|
-
pack: params.taskExtractionPack,
|
|
542
|
-
maxDemos: PROMPT_PACKS_MAX_DEMOS,
|
|
543
|
-
maxChars: PROMPT_PACKS_MAX_CHARS
|
|
544
|
-
})
|
|
545
|
-
: '';
|
|
546
|
-
|
|
547
|
-
const responseQualityBlock = params.responseQualityPack
|
|
548
|
-
? formatResponseQualityPack({
|
|
549
|
-
pack: params.responseQualityPack,
|
|
550
|
-
maxDemos: PROMPT_PACKS_MAX_DEMOS,
|
|
551
|
-
maxChars: PROMPT_PACKS_MAX_CHARS
|
|
552
|
-
})
|
|
553
|
-
: '';
|
|
554
|
-
|
|
555
|
-
const toolCallingBlock = params.toolCallingPack
|
|
556
|
-
? formatToolCallingPack({
|
|
557
|
-
pack: params.toolCallingPack,
|
|
558
|
-
maxDemos: PROMPT_PACKS_MAX_DEMOS,
|
|
559
|
-
maxChars: PROMPT_PACKS_MAX_CHARS
|
|
560
|
-
})
|
|
561
|
-
: '';
|
|
562
|
-
|
|
563
|
-
const toolOutcomeBlock = params.toolOutcomePack
|
|
564
|
-
? formatToolOutcomePack({
|
|
565
|
-
pack: params.toolOutcomePack,
|
|
566
|
-
maxDemos: PROMPT_PACKS_MAX_DEMOS,
|
|
567
|
-
maxChars: PROMPT_PACKS_MAX_CHARS
|
|
568
|
-
})
|
|
569
|
-
: '';
|
|
570
|
-
|
|
571
|
-
const memoryPolicyBlock = params.memoryPolicyPack
|
|
572
|
-
? formatMemoryPolicyPack({
|
|
573
|
-
pack: params.memoryPolicyPack,
|
|
574
|
-
maxDemos: PROMPT_PACKS_MAX_DEMOS,
|
|
575
|
-
maxChars: PROMPT_PACKS_MAX_CHARS
|
|
576
|
-
})
|
|
577
|
-
: '';
|
|
612
|
+
const fmtPack = (label: string, pack: PromptPack | null | undefined) =>
|
|
613
|
+
pack ? formatPromptPack({ label, pack, maxDemos: PROMPT_PACKS_MAX_DEMOS, maxChars: PROMPT_PACKS_MAX_CHARS }) : '';
|
|
578
614
|
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
: '';
|
|
615
|
+
const taskExtractionBlock = fmtPack('Task Extraction Guidelines', params.taskExtractionPack);
|
|
616
|
+
const responseQualityBlock = fmtPack('Response Quality Guidelines', params.responseQualityPack);
|
|
617
|
+
const toolCallingBlock = fmtPack('Tool Calling Guidelines', params.toolCallingPack);
|
|
618
|
+
const toolOutcomeBlock = fmtPack('Tool Outcome Guidelines', params.toolOutcomePack);
|
|
619
|
+
const memoryPolicyBlock = fmtPack('Memory Policy Guidelines', params.memoryPolicyPack);
|
|
620
|
+
const memoryRecallBlock = fmtPack('Memory Recall Guidelines', params.memoryRecallPack);
|
|
586
621
|
|
|
587
622
|
return [
|
|
588
623
|
`You are ${params.assistantName}, a personal assistant running inside DotClaw.`,
|
|
@@ -712,7 +747,8 @@ async function updateMemorySummary(params: {
|
|
|
712
747
|
instructions: prompt.instructions,
|
|
713
748
|
input: prompt.input,
|
|
714
749
|
maxOutputTokens: params.maxOutputTokens,
|
|
715
|
-
temperature: 0.1
|
|
750
|
+
temperature: 0.1,
|
|
751
|
+
reasoning: { effort: 'low' as const }
|
|
716
752
|
});
|
|
717
753
|
const text = await getTextWithFallback(result, 'summary');
|
|
718
754
|
return parseSummaryResponse(text);
|
|
@@ -726,7 +762,8 @@ function buildMemoryExtractionPrompt(params: {
|
|
|
726
762
|
memoryPolicyPack?: PromptPack | null;
|
|
727
763
|
}): { instructions: string; input: string } {
|
|
728
764
|
const policyBlock = params.memoryPolicyPack
|
|
729
|
-
?
|
|
765
|
+
? formatPromptPack({
|
|
766
|
+
label: 'Memory Policy Guidelines',
|
|
730
767
|
pack: params.memoryPolicyPack,
|
|
731
768
|
maxDemos: PROMPT_PACKS_MAX_DEMOS,
|
|
732
769
|
maxChars: PROMPT_PACKS_MAX_CHARS
|
|
@@ -853,7 +890,8 @@ async function validateResponseQuality(params: {
|
|
|
853
890
|
instructions: prompt.instructions,
|
|
854
891
|
input: prompt.input,
|
|
855
892
|
maxOutputTokens: params.maxOutputTokens,
|
|
856
|
-
temperature: params.temperature
|
|
893
|
+
temperature: params.temperature,
|
|
894
|
+
reasoning: { effort: 'low' as const }
|
|
857
895
|
});
|
|
858
896
|
const text = await getTextWithFallback(result, 'response_validation');
|
|
859
897
|
return parseResponseValidation(text);
|
|
@@ -959,24 +997,6 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
959
997
|
const responseValidateMinPromptTokens = agent.responseValidation.minPromptTokens || 0;
|
|
960
998
|
const responseValidateMinResponseTokens = agent.responseValidation.minResponseTokens || 0;
|
|
961
999
|
const maxContextMessageTokens = agent.context.maxContextMessageTokens;
|
|
962
|
-
const streamingEnabled = Boolean(input.streaming?.enabled && typeof input.streaming?.draftId === 'number');
|
|
963
|
-
const streamingDraftId = streamingEnabled ? input.streaming?.draftId : undefined;
|
|
964
|
-
const streamingMinIntervalMs = Math.max(
|
|
965
|
-
0,
|
|
966
|
-
Math.floor(
|
|
967
|
-
typeof input.streaming?.minIntervalMs === 'number'
|
|
968
|
-
? input.streaming.minIntervalMs
|
|
969
|
-
: agent.streaming.minIntervalMs
|
|
970
|
-
)
|
|
971
|
-
);
|
|
972
|
-
const streamingMinChars = Math.max(
|
|
973
|
-
1,
|
|
974
|
-
Math.floor(
|
|
975
|
-
typeof input.streaming?.minChars === 'number'
|
|
976
|
-
? input.streaming.minChars
|
|
977
|
-
: agent.streaming.minChars
|
|
978
|
-
)
|
|
979
|
-
);
|
|
980
1000
|
|
|
981
1001
|
const openrouter = getCachedOpenRouter(apiKey, openrouterOptions);
|
|
982
1002
|
const tokenEstimate = resolveTokenEstimate(input, agentConfig);
|
|
@@ -1008,21 +1028,6 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1008
1028
|
}
|
|
1009
1029
|
});
|
|
1010
1030
|
|
|
1011
|
-
let streamLastSentAt = 0;
|
|
1012
|
-
let streamLastSentLength = 0;
|
|
1013
|
-
const sendStreamUpdate = (text: string, force = false) => {
|
|
1014
|
-
if (!streamingEnabled || !streamingDraftId) return;
|
|
1015
|
-
if (!text || !text.trim()) return;
|
|
1016
|
-
const now = Date.now();
|
|
1017
|
-
if (!force) {
|
|
1018
|
-
if (now - streamLastSentAt < streamingMinIntervalMs) return;
|
|
1019
|
-
if (text.length - streamLastSentLength < streamingMinChars) return;
|
|
1020
|
-
}
|
|
1021
|
-
streamLastSentAt = now;
|
|
1022
|
-
streamLastSentLength = text.length;
|
|
1023
|
-
void ipc.sendDraft(text, streamingDraftId).catch(() => undefined);
|
|
1024
|
-
};
|
|
1025
|
-
|
|
1026
1031
|
if (process.env.DOTCLAW_SELF_CHECK === '1') {
|
|
1027
1032
|
try {
|
|
1028
1033
|
const details = await runSelfCheck({ model });
|
|
@@ -1225,7 +1230,8 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1225
1230
|
instructions: plannerPrompt.instructions,
|
|
1226
1231
|
input: plannerPrompt.input,
|
|
1227
1232
|
maxOutputTokens: plannerMaxOutputTokens,
|
|
1228
|
-
temperature: plannerTemperature
|
|
1233
|
+
temperature: plannerTemperature,
|
|
1234
|
+
reasoning: { effort: 'low' as const }
|
|
1229
1235
|
});
|
|
1230
1236
|
const plannerText = await getTextWithFallback(plannerResult, 'planner');
|
|
1231
1237
|
const plan = parsePlannerResponse(plannerText);
|
|
@@ -1292,47 +1298,39 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1292
1298
|
stopWhen: stepCountIs(maxToolSteps),
|
|
1293
1299
|
maxOutputTokens: config.maxOutputTokens,
|
|
1294
1300
|
temperature: config.temperature,
|
|
1295
|
-
|
|
1301
|
+
reasoning: { effort: 'low' as const }
|
|
1296
1302
|
};
|
|
1297
|
-
const result = await openrouter.callModel(callParams
|
|
1303
|
+
const result = await openrouter.callModel(callParams);
|
|
1298
1304
|
const localLatencyMs = Date.now() - startedAt;
|
|
1305
|
+
|
|
1306
|
+
// Get the complete response text via the SDK's proper getText() path
|
|
1307
|
+
let localResponseText = await getTextWithFallback(result, 'completion');
|
|
1308
|
+
|
|
1299
1309
|
const toolCallsFromModel = await result.getToolCalls();
|
|
1300
1310
|
if (toolCallsFromModel.length > 0) {
|
|
1301
1311
|
log(`Model made ${toolCallsFromModel.length} tool call(s): ${toolCallsFromModel.map(t => t.name).join(', ')}`);
|
|
1302
1312
|
}
|
|
1303
1313
|
|
|
1304
|
-
let localResponseText = '';
|
|
1305
|
-
let streamed = false;
|
|
1306
|
-
if (streamingEnabled && typeof (result as { getTextStream?: () => AsyncIterable<unknown> }).getTextStream === 'function') {
|
|
1307
|
-
try {
|
|
1308
|
-
const stream = (result as { getTextStream: () => AsyncIterable<unknown> }).getTextStream();
|
|
1309
|
-
for await (const chunk of stream) {
|
|
1310
|
-
const delta = typeof chunk === 'string'
|
|
1311
|
-
? chunk
|
|
1312
|
-
: (typeof (chunk as { text?: unknown })?.text === 'string' ? (chunk as { text?: string }).text || '' : '');
|
|
1313
|
-
if (!delta) continue;
|
|
1314
|
-
localResponseText += delta;
|
|
1315
|
-
sendStreamUpdate(localResponseText);
|
|
1316
|
-
}
|
|
1317
|
-
if (localResponseText) {
|
|
1318
|
-
sendStreamUpdate(localResponseText, true);
|
|
1319
|
-
}
|
|
1320
|
-
streamed = true;
|
|
1321
|
-
} catch (err) {
|
|
1322
|
-
log(`Streaming failed, falling back to full response: ${err instanceof Error ? err.message : String(err)}`);
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
if (!streamed || !localResponseText || !localResponseText.trim()) {
|
|
1326
|
-
localResponseText = await getTextWithFallback(result, 'completion');
|
|
1327
|
-
if (localResponseText && localResponseText.trim()) {
|
|
1328
|
-
sendStreamUpdate(localResponseText, true);
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
1314
|
if (!localResponseText || !localResponseText.trim()) {
|
|
1332
1315
|
if (toolCallsFromModel.length > 0) {
|
|
1333
1316
|
localResponseText = 'I started running tool calls but did not get a final response. If you want me to continue, please ask a narrower subtask or say "continue".';
|
|
1317
|
+
} else {
|
|
1318
|
+
// Responses API likely returned a gen-ID; retry with Chat Completions API
|
|
1319
|
+
try {
|
|
1320
|
+
localResponseText = await chatCompletionsFallback({
|
|
1321
|
+
model,
|
|
1322
|
+
instructions: resolvedInstructions,
|
|
1323
|
+
messages: messagesToOpenRouter(contextMessages),
|
|
1324
|
+
maxOutputTokens: config.maxOutputTokens,
|
|
1325
|
+
temperature: config.temperature
|
|
1326
|
+
});
|
|
1327
|
+
} catch (err) {
|
|
1328
|
+
log(`Chat Completions fallback error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
if (!localResponseText || !localResponseText.trim()) {
|
|
1332
|
+
log(`Warning: Model returned empty/whitespace response after all fallbacks. tool calls: ${toolCallsFromModel.length}`);
|
|
1334
1333
|
}
|
|
1335
|
-
log(`Warning: Model returned empty/whitespace response. Raw length: ${localResponseText?.length ?? 0}, tool calls: ${toolCallsFromModel.length}`);
|
|
1336
1334
|
} else {
|
|
1337
1335
|
log(`Model returned text response (${localResponseText.length} chars)`);
|
|
1338
1336
|
}
|
|
@@ -1392,8 +1390,6 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1392
1390
|
}
|
|
1393
1391
|
retriesLeft -= 1;
|
|
1394
1392
|
log(`Response validation failed; retrying (${retriesLeft} retries left)`);
|
|
1395
|
-
streamLastSentAt = 0;
|
|
1396
|
-
streamLastSentLength = 0;
|
|
1397
1393
|
const retryGuidance = buildRetryGuidance(validationResult);
|
|
1398
1394
|
const retryAttempt = await runCompletion(retryGuidance);
|
|
1399
1395
|
responseText = retryAttempt.responseText;
|
|
@@ -1464,7 +1460,8 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1464
1460
|
instructions: extractionPrompt.instructions,
|
|
1465
1461
|
input: extractionPrompt.input,
|
|
1466
1462
|
maxOutputTokens: memoryExtractionMaxOutputTokens,
|
|
1467
|
-
temperature: 0.1
|
|
1463
|
+
temperature: 0.1,
|
|
1464
|
+
reasoning: { effort: 'low' as const }
|
|
1468
1465
|
});
|
|
1469
1466
|
const extractionText = await getTextWithFallback(extractionResult, 'memory_extraction');
|
|
1470
1467
|
const extractedItems = parseMemoryExtraction(extractionText);
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import fs from 'fs';
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import { CronExpressionParser } from 'cron-parser';
|
|
9
|
+
import { generateId } from './id.js';
|
|
9
10
|
|
|
10
11
|
const IPC_DIR = '/workspace/ipc';
|
|
11
12
|
const MESSAGES_DIR = path.join(IPC_DIR, 'messages');
|
|
@@ -27,7 +28,7 @@ export interface IpcConfig {
|
|
|
27
28
|
function writeIpcFile(dir: string, data: object): string {
|
|
28
29
|
fs.mkdirSync(dir, { recursive: true });
|
|
29
30
|
|
|
30
|
-
const filename = `${
|
|
31
|
+
const filename = `${generateId('')}.json`;
|
|
31
32
|
const filepath = path.join(dir, filename);
|
|
32
33
|
|
|
33
34
|
const tempPath = `${filepath}.tmp`;
|
|
@@ -50,7 +51,7 @@ async function requestResponse(
|
|
|
50
51
|
fs.mkdirSync(REQUESTS_DIR, { recursive: true });
|
|
51
52
|
fs.mkdirSync(RESPONSES_DIR, { recursive: true });
|
|
52
53
|
|
|
53
|
-
const id =
|
|
54
|
+
const id = generateId('req');
|
|
54
55
|
writeIpcFile(REQUESTS_DIR, {
|
|
55
56
|
id,
|
|
56
57
|
type,
|
|
@@ -92,19 +93,6 @@ export function createIpcHandlers(ctx: IpcContext, config: IpcConfig) {
|
|
|
92
93
|
const filename = writeIpcFile(MESSAGES_DIR, data);
|
|
93
94
|
return { ok: true, id: filename };
|
|
94
95
|
},
|
|
95
|
-
async sendDraft(text: string, draftId: number) {
|
|
96
|
-
const data = {
|
|
97
|
-
type: 'message_draft',
|
|
98
|
-
chatJid,
|
|
99
|
-
text,
|
|
100
|
-
draftId,
|
|
101
|
-
groupFolder,
|
|
102
|
-
timestamp: new Date().toISOString()
|
|
103
|
-
};
|
|
104
|
-
const filename = writeIpcFile(MESSAGES_DIR, data);
|
|
105
|
-
return { ok: true, id: filename };
|
|
106
|
-
},
|
|
107
|
-
|
|
108
96
|
async scheduleTask(args: {
|
|
109
97
|
prompt: string;
|
|
110
98
|
schedule_type: 'cron' | 'interval' | 'once';
|