@dotsetlabs/dotclaw 1.5.2 → 1.7.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 +8 -3
- package/config-examples/runtime.json +10 -8
- package/config-examples/tool-policy.json +17 -0
- package/container/Dockerfile +4 -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 +17 -15
- package/container/agent-runner/src/container-protocol.ts +10 -6
- package/container/agent-runner/src/daemon.ts +237 -32
- package/container/agent-runner/src/heartbeat-worker.ts +94 -0
- package/container/agent-runner/src/id.ts +4 -0
- package/container/agent-runner/src/index.ts +356 -188
- package/container/agent-runner/src/ipc.ts +161 -12
- package/container/agent-runner/src/memory.ts +8 -2
- package/container/agent-runner/src/prompt-packs.ts +5 -209
- package/container/agent-runner/src/request-worker.ts +25 -0
- package/container/agent-runner/src/tools.ts +423 -32
- package/dist/agent-context.d.ts +1 -0
- package/dist/agent-context.d.ts.map +1 -1
- package/dist/agent-context.js +15 -7
- package/dist/agent-context.js.map +1 -1
- package/dist/agent-execution.d.ts +11 -6
- package/dist/agent-execution.d.ts.map +1 -1
- package/dist/agent-execution.js +6 -4
- package/dist/agent-execution.js.map +1 -1
- package/dist/background-jobs.d.ts +9 -0
- package/dist/background-jobs.d.ts.map +1 -1
- package/dist/background-jobs.js +70 -15
- 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 +313 -45
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +6 -0
- package/dist/config.js.map +1 -1
- package/dist/container-protocol.d.ts +10 -6
- package/dist/container-protocol.d.ts.map +1 -1
- package/dist/container-runner.d.ts +28 -8
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +254 -54
- 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 +31 -3
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +277 -39
- 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 +1194 -381
- 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 +2 -0
- package/dist/maintenance.d.ts.map +1 -1
- package/dist/maintenance.js +196 -17
- 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 +38 -4
- package/dist/memory-embeddings.js.map +1 -1
- package/dist/memory-recall.d.ts.map +1 -1
- package/dist/memory-recall.js +6 -1
- package/dist/memory-recall.js.map +1 -1
- package/dist/memory-store.d.ts +1 -0
- package/dist/memory-store.d.ts.map +1 -1
- package/dist/memory-store.js +72 -10
- 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/path-mapping.d.ts +4 -0
- package/dist/path-mapping.d.ts.map +1 -0
- package/dist/path-mapping.js +50 -0
- package/dist/path-mapping.js.map +1 -0
- 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/personalization.d.ts +1 -0
- package/dist/personalization.d.ts.map +1 -1
- package/dist/personalization.js +14 -0
- package/dist/personalization.js.map +1 -1
- package/dist/progress.d.ts +2 -2
- package/dist/progress.d.ts.map +1 -1
- package/dist/progress.js +18 -14
- package/dist/progress.js.map +1 -1
- package/dist/runtime-config.d.ts +18 -7
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +29 -18
- package/dist/runtime-config.js.map +1 -1
- package/dist/task-scheduler.d.ts +7 -1
- package/dist/task-scheduler.d.ts.map +1 -1
- package/dist/task-scheduler.js +182 -48
- package/dist/task-scheduler.js.map +1 -1
- package/dist/timezone.d.ts +4 -0
- package/dist/timezone.d.ts.map +1 -0
- package/dist/timezone.js +111 -0
- package/dist/timezone.js.map +1 -0
- package/dist/types.d.ts +31 -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
|
|
|
@@ -40,6 +40,9 @@ const AVAILABLE_GROUPS_PATH = '/workspace/ipc/available_groups.json';
|
|
|
40
40
|
const GROUP_CLAUDE_PATH = path.join(GROUP_DIR, 'CLAUDE.md');
|
|
41
41
|
const GLOBAL_CLAUDE_PATH = path.join(GLOBAL_DIR, 'CLAUDE.md');
|
|
42
42
|
const CLAUDE_NOTES_MAX_CHARS = 4000;
|
|
43
|
+
const SKILL_NOTES_MAX_FILES = 16;
|
|
44
|
+
const SKILL_NOTES_MAX_CHARS = 3000;
|
|
45
|
+
const SKILL_NOTES_TOTAL_MAX_CHARS = 18_000;
|
|
43
46
|
|
|
44
47
|
const agentConfig = loadAgentConfig();
|
|
45
48
|
const agent = agentConfig.agent;
|
|
@@ -71,72 +74,64 @@ function log(message: string): void {
|
|
|
71
74
|
console.error(`[agent-runner] ${message}`);
|
|
72
75
|
}
|
|
73
76
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
return '';
|
|
77
|
+
// ── Response extraction pipeline ─────────────────────────────────────
|
|
78
|
+
// OpenRouter SDK v0.3.x returns raw response IDs (gen-*, resp-*, etc.) instead
|
|
79
|
+
// of text for fast reasoning models (GPT-5-mini/nano). Reasoning tokens consume
|
|
80
|
+
// the output budget, leaving nothing for actual text. This multi-layer pipeline
|
|
81
|
+
// works around that:
|
|
82
|
+
// 1. isLikelyResponseId — detect leaked IDs so we never surface them
|
|
83
|
+
// 2. extractTextFromRawResponse — walk raw response fields ourselves
|
|
84
|
+
// 3. getTextWithFallback — try SDK getText(), fall back to raw extraction
|
|
85
|
+
// 4. chatCompletionsFallback — retry via /chat/completions when all else fails
|
|
86
|
+
// Remove this pipeline once the SDK reliably returns text for reasoning models.
|
|
87
|
+
|
|
88
|
+
const RESPONSE_ID_PREFIXES = ['gen-', 'resp-', 'resp_', 'chatcmpl-', 'msg_'];
|
|
89
|
+
|
|
90
|
+
function isLikelyResponseId(value: string): boolean {
|
|
91
|
+
const trimmed = value.trim();
|
|
92
|
+
if (!trimmed || trimmed.includes(' ') || trimmed.includes('\n')) return false;
|
|
93
|
+
return RESPONSE_ID_PREFIXES.some(prefix => trimmed.startsWith(prefix));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isValidText(value: unknown): value is string {
|
|
97
|
+
return typeof value === 'string' && value.trim().length > 0 && !isLikelyResponseId(value);
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
function
|
|
100
|
+
function extractTextFromRawResponse(response: unknown): string {
|
|
100
101
|
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
|
-
};
|
|
102
|
+
const record = response as Record<string, unknown>;
|
|
107
103
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
104
|
+
// 1. SDK-parsed camelCase field
|
|
105
|
+
if (isValidText(record.outputText)) return record.outputText;
|
|
106
|
+
|
|
107
|
+
// 2. Raw API snake_case field
|
|
108
|
+
if (isValidText(record.output_text)) return record.output_text;
|
|
114
109
|
|
|
110
|
+
// 3. Walk response.output[] for message/output_text items
|
|
115
111
|
if (Array.isArray(record.output)) {
|
|
116
|
-
const
|
|
112
|
+
const parts: string[] = [];
|
|
117
113
|
for (const item of record.output) {
|
|
118
114
|
if (!item || typeof item !== 'object') continue;
|
|
119
|
-
const typed = item as { type?:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
115
|
+
const typed = item as { type?: string; content?: unknown; text?: string };
|
|
116
|
+
if (typed.type === 'message' && Array.isArray(typed.content)) {
|
|
117
|
+
for (const part of typed.content as Array<{ type?: string; text?: string }>) {
|
|
118
|
+
if (part?.type === 'output_text' && isValidText(part.text)) {
|
|
119
|
+
parts.push(part.text);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} else if (typed.type === 'output_text' && isValidText(typed.text)) {
|
|
123
|
+
parts.push(typed.text);
|
|
126
124
|
}
|
|
127
125
|
}
|
|
128
|
-
const joined =
|
|
126
|
+
const joined = parts.join('');
|
|
129
127
|
if (joined.trim()) return joined;
|
|
130
128
|
}
|
|
131
129
|
|
|
130
|
+
// 4. OpenAI chat completions compat
|
|
132
131
|
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;
|
|
132
|
+
const choice = record.choices[0] as { message?: { content?: unknown } } | null | undefined;
|
|
133
|
+
if (choice?.message && isValidText(choice.message.content)) {
|
|
134
|
+
return choice.message.content;
|
|
140
135
|
}
|
|
141
136
|
}
|
|
142
137
|
|
|
@@ -144,22 +139,103 @@ function extractTextFallbackFromResponse(response: unknown): string {
|
|
|
144
139
|
}
|
|
145
140
|
|
|
146
141
|
async function getTextWithFallback(result: OpenRouterResult, context: string): Promise<string> {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
142
|
+
// 1. Try the SDK's proper getText() first — this handles tool execution and
|
|
143
|
+
// extracts text from the final response via the SDK's own logic.
|
|
144
|
+
try {
|
|
145
|
+
const text = await result.getText();
|
|
146
|
+
if (isValidText(text)) {
|
|
147
|
+
return text;
|
|
148
|
+
}
|
|
149
|
+
if (text && isLikelyResponseId(text)) {
|
|
150
|
+
log(`Ignored response id from getText (${context}): ${String(text).slice(0, 60)}`);
|
|
151
|
+
}
|
|
152
|
+
} catch (err) {
|
|
153
|
+
log(`getText failed (${context}): ${err instanceof Error ? err.message : String(err)}`);
|
|
150
154
|
}
|
|
155
|
+
|
|
156
|
+
// 2. Fall back to raw response extraction — walk known fields ourselves
|
|
151
157
|
try {
|
|
152
158
|
const response = await result.getResponse();
|
|
153
|
-
const fallbackText =
|
|
154
|
-
if (fallbackText
|
|
155
|
-
log(`Recovered
|
|
159
|
+
const fallbackText = extractTextFromRawResponse(response);
|
|
160
|
+
if (fallbackText) {
|
|
161
|
+
log(`Recovered text from raw response (${context})`);
|
|
156
162
|
return fallbackText;
|
|
157
163
|
}
|
|
158
|
-
|
|
164
|
+
const r = response as Record<string, unknown>;
|
|
165
|
+
const outputLen = Array.isArray(r.output) ? (r.output as unknown[]).length : 0;
|
|
166
|
+
log(`No text in raw response (${context}): id=${String(r.id ?? 'none').slice(0, 40)} status=${String(r.status ?? '?')} outputs=${outputLen}`);
|
|
159
167
|
} catch (err) {
|
|
160
|
-
log(`
|
|
168
|
+
log(`Raw response extraction failed (${context}): ${err instanceof Error ? err.message : String(err)}`);
|
|
161
169
|
}
|
|
162
|
-
|
|
170
|
+
|
|
171
|
+
// 3. Never return a response ID
|
|
172
|
+
return '';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Direct Chat Completions API fallback.
|
|
177
|
+
* When the Responses API returns a gen-ID instead of text (common with fast
|
|
178
|
+
* models like gpt-5-nano/mini via OpenRouter), retry using the standard
|
|
179
|
+
* /chat/completions endpoint which reliably returns text content.
|
|
180
|
+
*/
|
|
181
|
+
async function chatCompletionsFallback(params: {
|
|
182
|
+
model: string;
|
|
183
|
+
instructions: string;
|
|
184
|
+
messages: Array<{ role: string; content: string }>;
|
|
185
|
+
maxOutputTokens: number;
|
|
186
|
+
temperature: number;
|
|
187
|
+
}): Promise<string> {
|
|
188
|
+
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
189
|
+
if (!apiKey) return '';
|
|
190
|
+
|
|
191
|
+
const headers: Record<string, string> = {
|
|
192
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
193
|
+
'Content-Type': 'application/json'
|
|
194
|
+
};
|
|
195
|
+
if (agent.openrouter.siteUrl) {
|
|
196
|
+
headers['HTTP-Referer'] = agent.openrouter.siteUrl;
|
|
197
|
+
}
|
|
198
|
+
if (agent.openrouter.siteName) {
|
|
199
|
+
headers['X-Title'] = agent.openrouter.siteName;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const chatMessages = [
|
|
203
|
+
{ role: 'system', content: params.instructions },
|
|
204
|
+
...params.messages
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
log(`Chat Completions fallback: model=${params.model}, messages=${chatMessages.length}`);
|
|
208
|
+
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
209
|
+
method: 'POST',
|
|
210
|
+
headers,
|
|
211
|
+
body: JSON.stringify({
|
|
212
|
+
model: params.model,
|
|
213
|
+
messages: chatMessages,
|
|
214
|
+
max_completion_tokens: params.maxOutputTokens,
|
|
215
|
+
temperature: params.temperature,
|
|
216
|
+
reasoning_effort: 'low'
|
|
217
|
+
}),
|
|
218
|
+
signal: AbortSignal.timeout(agent.openrouter.timeoutMs)
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const bodyText = await response.text();
|
|
222
|
+
if (!response.ok) {
|
|
223
|
+
log(`Chat Completions fallback HTTP ${response.status}: ${bodyText.slice(0, 300)}`);
|
|
224
|
+
return '';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const data = JSON.parse(bodyText);
|
|
229
|
+
const content = data?.choices?.[0]?.message?.content;
|
|
230
|
+
if (isValidText(content)) {
|
|
231
|
+
log(`Chat Completions fallback recovered text (${String(content).length} chars)`);
|
|
232
|
+
return content;
|
|
233
|
+
}
|
|
234
|
+
log(`Chat Completions fallback returned no text: ${JSON.stringify(data).slice(0, 300)}`);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
log(`Chat Completions fallback parse error: ${err instanceof Error ? err.message : String(err)}`);
|
|
237
|
+
}
|
|
238
|
+
return '';
|
|
163
239
|
}
|
|
164
240
|
|
|
165
241
|
function writeOutput(output: ContainerOutput): void {
|
|
@@ -397,6 +473,7 @@ function buildSystemInstructions(params: {
|
|
|
397
473
|
assistantName: string;
|
|
398
474
|
groupNotes?: string | null;
|
|
399
475
|
globalNotes?: string | null;
|
|
476
|
+
skillNotes?: SkillNote[];
|
|
400
477
|
memorySummary: string;
|
|
401
478
|
memoryFacts: string[];
|
|
402
479
|
sessionRecall: string[];
|
|
@@ -429,7 +506,19 @@ function buildSystemInstructions(params: {
|
|
|
429
506
|
'- `GitClone`: clone git repositories into the workspace.',
|
|
430
507
|
'- `NpmInstall`: install npm dependencies in the workspace.',
|
|
431
508
|
'- `mcp__dotclaw__send_message`: send Telegram messages.',
|
|
432
|
-
'- `
|
|
509
|
+
'- `mcp__dotclaw__send_file`: send a file/document.',
|
|
510
|
+
'- `mcp__dotclaw__send_photo`: send a photo with compression.',
|
|
511
|
+
'- `mcp__dotclaw__send_voice`: send a voice message (.ogg format).',
|
|
512
|
+
'- `mcp__dotclaw__send_audio`: send an audio file (mp3, m4a, etc.).',
|
|
513
|
+
'- `mcp__dotclaw__send_location`: send a map pin (latitude/longitude).',
|
|
514
|
+
'- `mcp__dotclaw__send_contact`: send a contact card (phone + name).',
|
|
515
|
+
'- `mcp__dotclaw__send_poll`: create a Telegram poll.',
|
|
516
|
+
'- `mcp__dotclaw__send_buttons`: send a message with inline keyboard buttons.',
|
|
517
|
+
'- `mcp__dotclaw__edit_message`: edit a previously sent message.',
|
|
518
|
+
'- `mcp__dotclaw__delete_message`: delete a message.',
|
|
519
|
+
'- `mcp__dotclaw__download_url`: download a URL to the workspace as a file.',
|
|
520
|
+
'- Users may send photos, documents, voice messages, and videos. These are downloaded to `/workspace/group/inbox/` and referenced as `<attachment>` tags in messages. Process them with Read/Bash/Python tools.',
|
|
521
|
+
'- `mcp__dotclaw__schedule_task`: schedule tasks (set `timezone` for locale-specific schedules).',
|
|
433
522
|
'- `mcp__dotclaw__run_task`: run a scheduled task immediately.',
|
|
434
523
|
'- `mcp__dotclaw__list_tasks`, `mcp__dotclaw__pause_task`, `mcp__dotclaw__resume_task`, `mcp__dotclaw__cancel_task`.',
|
|
435
524
|
'- `mcp__dotclaw__update_task`: update a task (state, prompt, schedule, status).',
|
|
@@ -479,6 +568,7 @@ function buildSystemInstructions(params: {
|
|
|
479
568
|
|
|
480
569
|
const groupNotes = params.groupNotes ? `Group notes:\n${params.groupNotes}` : '';
|
|
481
570
|
const globalNotes = params.globalNotes ? `Global notes:\n${params.globalNotes}` : '';
|
|
571
|
+
const skillNotes = formatSkillNotes(params.skillNotes || []);
|
|
482
572
|
|
|
483
573
|
const toolReliability = params.toolReliability && params.toolReliability.length > 0
|
|
484
574
|
? params.toolReliability
|
|
@@ -536,53 +626,15 @@ function buildSystemInstructions(params: {
|
|
|
536
626
|
? `Job artifacts directory: /workspace/group/jobs/${params.jobId}`
|
|
537
627
|
: '';
|
|
538
628
|
|
|
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
|
-
: '';
|
|
629
|
+
const fmtPack = (label: string, pack: PromptPack | null | undefined) =>
|
|
630
|
+
pack ? formatPromptPack({ label, pack, maxDemos: PROMPT_PACKS_MAX_DEMOS, maxChars: PROMPT_PACKS_MAX_CHARS }) : '';
|
|
554
631
|
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
-
: '';
|
|
578
|
-
|
|
579
|
-
const memoryRecallBlock = params.memoryRecallPack
|
|
580
|
-
? formatMemoryRecallPack({
|
|
581
|
-
pack: params.memoryRecallPack,
|
|
582
|
-
maxDemos: PROMPT_PACKS_MAX_DEMOS,
|
|
583
|
-
maxChars: PROMPT_PACKS_MAX_CHARS
|
|
584
|
-
})
|
|
585
|
-
: '';
|
|
632
|
+
const taskExtractionBlock = fmtPack('Task Extraction Guidelines', params.taskExtractionPack);
|
|
633
|
+
const responseQualityBlock = fmtPack('Response Quality Guidelines', params.responseQualityPack);
|
|
634
|
+
const toolCallingBlock = fmtPack('Tool Calling Guidelines', params.toolCallingPack);
|
|
635
|
+
const toolOutcomeBlock = fmtPack('Tool Outcome Guidelines', params.toolOutcomePack);
|
|
636
|
+
const memoryPolicyBlock = fmtPack('Memory Policy Guidelines', params.memoryPolicyPack);
|
|
637
|
+
const memoryRecallBlock = fmtPack('Memory Recall Guidelines', params.memoryRecallPack);
|
|
586
638
|
|
|
587
639
|
return [
|
|
588
640
|
`You are ${params.assistantName}, a personal assistant running inside DotClaw.`,
|
|
@@ -594,6 +646,7 @@ function buildSystemInstructions(params: {
|
|
|
594
646
|
browserAutomation,
|
|
595
647
|
groupNotes,
|
|
596
648
|
globalNotes,
|
|
649
|
+
skillNotes,
|
|
597
650
|
timezoneNote,
|
|
598
651
|
params.planBlock || '',
|
|
599
652
|
toolCallingBlock,
|
|
@@ -655,6 +708,123 @@ function loadClaudeNotes(): { group: string | null; global: string | null } {
|
|
|
655
708
|
};
|
|
656
709
|
}
|
|
657
710
|
|
|
711
|
+
export type SkillNote = {
|
|
712
|
+
scope: 'group' | 'global';
|
|
713
|
+
path: string;
|
|
714
|
+
content: string;
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
function collectSkillFiles(rootDir: string, maxFiles: number): string[] {
|
|
718
|
+
const files: string[] = [];
|
|
719
|
+
const seen = new Set<string>();
|
|
720
|
+
const addFile = (filePath: string) => {
|
|
721
|
+
const normalized = path.resolve(filePath);
|
|
722
|
+
if (seen.has(normalized)) return;
|
|
723
|
+
if (!fs.existsSync(normalized)) return;
|
|
724
|
+
let stat: fs.Stats;
|
|
725
|
+
try {
|
|
726
|
+
stat = fs.statSync(normalized);
|
|
727
|
+
} catch {
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
if (!stat.isFile()) return;
|
|
731
|
+
if (!normalized.toLowerCase().endsWith('.md')) return;
|
|
732
|
+
seen.add(normalized);
|
|
733
|
+
files.push(normalized);
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
addFile(path.join(rootDir, 'SKILL.md'));
|
|
737
|
+
|
|
738
|
+
const skillsDir = path.join(rootDir, 'skills');
|
|
739
|
+
if (fs.existsSync(skillsDir)) {
|
|
740
|
+
const stack = [skillsDir];
|
|
741
|
+
while (stack.length > 0 && files.length < maxFiles) {
|
|
742
|
+
const current = stack.pop();
|
|
743
|
+
if (!current) continue;
|
|
744
|
+
let entries: fs.Dirent[];
|
|
745
|
+
try {
|
|
746
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
747
|
+
} catch {
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
751
|
+
for (const entry of entries) {
|
|
752
|
+
const nextPath = path.join(current, entry.name);
|
|
753
|
+
if (entry.isSymbolicLink()) continue;
|
|
754
|
+
if (entry.isDirectory()) {
|
|
755
|
+
stack.push(nextPath);
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
if (entry.isFile()) {
|
|
759
|
+
addFile(nextPath);
|
|
760
|
+
}
|
|
761
|
+
if (files.length >= maxFiles) break;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
files.sort((a, b) => a.localeCompare(b));
|
|
767
|
+
return files.slice(0, maxFiles);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
export function loadSkillNotesFromRoots(params: {
|
|
771
|
+
groupDir: string;
|
|
772
|
+
globalDir: string;
|
|
773
|
+
maxFiles?: number;
|
|
774
|
+
maxCharsPerFile?: number;
|
|
775
|
+
maxTotalChars?: number;
|
|
776
|
+
}): SkillNote[] {
|
|
777
|
+
const maxFiles = Number.isFinite(params.maxFiles) ? Math.max(1, Math.floor(params.maxFiles!)) : SKILL_NOTES_MAX_FILES;
|
|
778
|
+
const maxCharsPerFile = Number.isFinite(params.maxCharsPerFile)
|
|
779
|
+
? Math.max(200, Math.floor(params.maxCharsPerFile!))
|
|
780
|
+
: SKILL_NOTES_MAX_CHARS;
|
|
781
|
+
const maxTotalChars = Number.isFinite(params.maxTotalChars)
|
|
782
|
+
? Math.max(maxCharsPerFile, Math.floor(params.maxTotalChars!))
|
|
783
|
+
: SKILL_NOTES_TOTAL_MAX_CHARS;
|
|
784
|
+
|
|
785
|
+
const notes: SkillNote[] = [];
|
|
786
|
+
let consumedChars = 0;
|
|
787
|
+
|
|
788
|
+
const appendScopeNotes = (scope: 'group' | 'global', rootDir: string) => {
|
|
789
|
+
const skillFiles = collectSkillFiles(rootDir, maxFiles);
|
|
790
|
+
for (const filePath of skillFiles) {
|
|
791
|
+
if (notes.length >= maxFiles) break;
|
|
792
|
+
if (consumedChars >= maxTotalChars) break;
|
|
793
|
+
const content = readTextFileLimited(filePath, maxCharsPerFile);
|
|
794
|
+
if (!content) continue;
|
|
795
|
+
const remaining = maxTotalChars - consumedChars;
|
|
796
|
+
const truncated = content.length > remaining
|
|
797
|
+
? `${content.slice(0, remaining)}\n\n[Truncated for total skill budget]`
|
|
798
|
+
: content;
|
|
799
|
+
const relativePath = path.relative(rootDir, filePath).split(path.sep).join('/');
|
|
800
|
+
notes.push({
|
|
801
|
+
scope,
|
|
802
|
+
path: relativePath || path.basename(filePath),
|
|
803
|
+
content: truncated
|
|
804
|
+
});
|
|
805
|
+
consumedChars += truncated.length;
|
|
806
|
+
if (consumedChars >= maxTotalChars) break;
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
appendScopeNotes('group', params.groupDir);
|
|
811
|
+
appendScopeNotes('global', params.globalDir);
|
|
812
|
+
return notes;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function formatSkillNotes(notes: SkillNote[]): string {
|
|
816
|
+
if (!notes || notes.length === 0) return '';
|
|
817
|
+
const lines: string[] = [
|
|
818
|
+
'Skill instructions (loaded from SKILL.md / skills/*.md):',
|
|
819
|
+
'When a task matches a skill, follow that skill workflow first and keep output concise.'
|
|
820
|
+
];
|
|
821
|
+
for (const note of notes) {
|
|
822
|
+
lines.push(`[${note.scope}] ${note.path}`);
|
|
823
|
+
lines.push(note.content);
|
|
824
|
+
}
|
|
825
|
+
return lines.join('\n\n');
|
|
826
|
+
}
|
|
827
|
+
|
|
658
828
|
function extractQueryFromPrompt(prompt: string): string {
|
|
659
829
|
if (!prompt) return '';
|
|
660
830
|
const messageMatches = [...prompt.matchAll(/<message[^>]*>([\s\S]*?)<\/message>/g)];
|
|
@@ -670,6 +840,7 @@ function decodeXml(value: string): string {
|
|
|
670
840
|
.replace(/</g, '<')
|
|
671
841
|
.replace(/>/g, '>')
|
|
672
842
|
.replace(/"/g, '"')
|
|
843
|
+
.replace(/'/g, "'")
|
|
673
844
|
.replace(/&/g, '&');
|
|
674
845
|
}
|
|
675
846
|
|
|
@@ -712,7 +883,8 @@ async function updateMemorySummary(params: {
|
|
|
712
883
|
instructions: prompt.instructions,
|
|
713
884
|
input: prompt.input,
|
|
714
885
|
maxOutputTokens: params.maxOutputTokens,
|
|
715
|
-
temperature: 0.1
|
|
886
|
+
temperature: 0.1,
|
|
887
|
+
reasoning: { effort: 'low' as const }
|
|
716
888
|
});
|
|
717
889
|
const text = await getTextWithFallback(result, 'summary');
|
|
718
890
|
return parseSummaryResponse(text);
|
|
@@ -726,7 +898,8 @@ function buildMemoryExtractionPrompt(params: {
|
|
|
726
898
|
memoryPolicyPack?: PromptPack | null;
|
|
727
899
|
}): { instructions: string; input: string } {
|
|
728
900
|
const policyBlock = params.memoryPolicyPack
|
|
729
|
-
?
|
|
901
|
+
? formatPromptPack({
|
|
902
|
+
label: 'Memory Policy Guidelines',
|
|
730
903
|
pack: params.memoryPolicyPack,
|
|
731
904
|
maxDemos: PROMPT_PACKS_MAX_DEMOS,
|
|
732
905
|
maxChars: PROMPT_PACKS_MAX_CHARS
|
|
@@ -853,7 +1026,8 @@ async function validateResponseQuality(params: {
|
|
|
853
1026
|
instructions: prompt.instructions,
|
|
854
1027
|
input: prompt.input,
|
|
855
1028
|
maxOutputTokens: params.maxOutputTokens,
|
|
856
|
-
temperature: params.temperature
|
|
1029
|
+
temperature: params.temperature,
|
|
1030
|
+
reasoning: { effort: 'low' as const }
|
|
857
1031
|
});
|
|
858
1032
|
const text = await getTextWithFallback(result, 'response_validation');
|
|
859
1033
|
return parseResponseValidation(text);
|
|
@@ -959,29 +1133,15 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
959
1133
|
const responseValidateMinPromptTokens = agent.responseValidation.minPromptTokens || 0;
|
|
960
1134
|
const responseValidateMinResponseTokens = agent.responseValidation.minResponseTokens || 0;
|
|
961
1135
|
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
1136
|
|
|
981
1137
|
const openrouter = getCachedOpenRouter(apiKey, openrouterOptions);
|
|
982
1138
|
const tokenEstimate = resolveTokenEstimate(input, agentConfig);
|
|
983
1139
|
const availableGroups = loadAvailableGroups();
|
|
984
1140
|
const claudeNotes = loadClaudeNotes();
|
|
1141
|
+
const skillNotes = loadSkillNotesFromRoots({
|
|
1142
|
+
groupDir: GROUP_DIR,
|
|
1143
|
+
globalDir: GLOBAL_DIR
|
|
1144
|
+
});
|
|
985
1145
|
|
|
986
1146
|
const { ctx: sessionCtx, isNew } = createSessionContext(SESSION_ROOT, input.sessionId);
|
|
987
1147
|
const toolCalls: ToolCallRecord[] = [];
|
|
@@ -1008,21 +1168,6 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1008
1168
|
}
|
|
1009
1169
|
});
|
|
1010
1170
|
|
|
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
1171
|
if (process.env.DOTCLAW_SELF_CHECK === '1') {
|
|
1027
1172
|
try {
|
|
1028
1173
|
const details = await runSelfCheck({ model });
|
|
@@ -1047,6 +1192,16 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1047
1192
|
if (input.isScheduledTask) {
|
|
1048
1193
|
prompt = `[SCHEDULED TASK - You are running automatically, not in response to a user message. Use mcp__dotclaw__send_message if needed to communicate with the user.]\n\n${input.prompt}`;
|
|
1049
1194
|
}
|
|
1195
|
+
if (Array.isArray(input.attachments) && input.attachments.length > 0) {
|
|
1196
|
+
const attachmentSummary = input.attachments.map(attachment => {
|
|
1197
|
+
const parts = [`type=${attachment.type}`, `path=${attachment.path}`];
|
|
1198
|
+
if (attachment.file_name) parts.push(`filename=${attachment.file_name}`);
|
|
1199
|
+
if (attachment.mime_type) parts.push(`mime=${attachment.mime_type}`);
|
|
1200
|
+
if (Number.isFinite(attachment.file_size)) parts.push(`size=${attachment.file_size}`);
|
|
1201
|
+
return `- ${parts.join(' ')}`;
|
|
1202
|
+
}).join('\n');
|
|
1203
|
+
prompt = `${prompt}\n\n<latest_attachments>\n${attachmentSummary}\n</latest_attachments>`;
|
|
1204
|
+
}
|
|
1050
1205
|
|
|
1051
1206
|
appendHistory(sessionCtx, 'user', prompt);
|
|
1052
1207
|
let history = loadHistory(sessionCtx);
|
|
@@ -1177,6 +1332,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1177
1332
|
assistantName,
|
|
1178
1333
|
groupNotes: claudeNotes.group,
|
|
1179
1334
|
globalNotes: claudeNotes.global,
|
|
1335
|
+
skillNotes,
|
|
1180
1336
|
memorySummary: sessionCtx.state.summary,
|
|
1181
1337
|
memoryFacts: sessionCtx.state.facts,
|
|
1182
1338
|
sessionRecall,
|
|
@@ -1225,7 +1381,8 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1225
1381
|
instructions: plannerPrompt.instructions,
|
|
1226
1382
|
input: plannerPrompt.input,
|
|
1227
1383
|
maxOutputTokens: plannerMaxOutputTokens,
|
|
1228
|
-
temperature: plannerTemperature
|
|
1384
|
+
temperature: plannerTemperature,
|
|
1385
|
+
reasoning: { effort: 'low' as const }
|
|
1229
1386
|
});
|
|
1230
1387
|
const plannerText = await getTextWithFallback(plannerResult, 'planner');
|
|
1231
1388
|
const plan = parsePlannerResponse(plannerText);
|
|
@@ -1282,6 +1439,16 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1282
1439
|
+ estimateMessagesTokens(contextMessages, tokenEstimate.tokensPerChar, tokenEstimate.tokensPerMessage)
|
|
1283
1440
|
+ tokenEstimate.tokensPerRequest;
|
|
1284
1441
|
|
|
1442
|
+
const safeLimit = Math.floor(config.maxContextTokens * 0.9);
|
|
1443
|
+
if (resolvedPromptTokens > safeLimit && contextMessages.length > 2) {
|
|
1444
|
+
log(`Estimated ${resolvedPromptTokens} tokens exceeds safe limit ${safeLimit}, truncating`);
|
|
1445
|
+
while (contextMessages.length > 2) {
|
|
1446
|
+
const currentTokens = resolvedInstructionTokens + estimateMessagesTokens(contextMessages, tokenEstimate.tokensPerChar, tokenEstimate.tokensPerMessage) + tokenEstimate.tokensPerRequest;
|
|
1447
|
+
if (currentTokens <= safeLimit) break;
|
|
1448
|
+
contextMessages.splice(0, 1);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1285
1452
|
log('Starting OpenRouter call...');
|
|
1286
1453
|
const startedAt = Date.now();
|
|
1287
1454
|
const callParams = {
|
|
@@ -1292,47 +1459,39 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1292
1459
|
stopWhen: stepCountIs(maxToolSteps),
|
|
1293
1460
|
maxOutputTokens: config.maxOutputTokens,
|
|
1294
1461
|
temperature: config.temperature,
|
|
1295
|
-
|
|
1462
|
+
reasoning: { effort: 'low' as const }
|
|
1296
1463
|
};
|
|
1297
|
-
const result = await openrouter.callModel(callParams
|
|
1464
|
+
const result = await openrouter.callModel(callParams);
|
|
1298
1465
|
const localLatencyMs = Date.now() - startedAt;
|
|
1466
|
+
|
|
1467
|
+
// Get the complete response text via the SDK's proper getText() path
|
|
1468
|
+
let localResponseText = await getTextWithFallback(result, 'completion');
|
|
1469
|
+
|
|
1299
1470
|
const toolCallsFromModel = await result.getToolCalls();
|
|
1300
1471
|
if (toolCallsFromModel.length > 0) {
|
|
1301
1472
|
log(`Model made ${toolCallsFromModel.length} tool call(s): ${toolCallsFromModel.map(t => t.name).join(', ')}`);
|
|
1302
1473
|
}
|
|
1303
1474
|
|
|
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
1475
|
if (!localResponseText || !localResponseText.trim()) {
|
|
1332
1476
|
if (toolCallsFromModel.length > 0) {
|
|
1333
1477
|
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".';
|
|
1478
|
+
} else {
|
|
1479
|
+
// Responses API likely returned a gen-ID; retry with Chat Completions API
|
|
1480
|
+
try {
|
|
1481
|
+
localResponseText = await chatCompletionsFallback({
|
|
1482
|
+
model,
|
|
1483
|
+
instructions: resolvedInstructions,
|
|
1484
|
+
messages: messagesToOpenRouter(contextMessages),
|
|
1485
|
+
maxOutputTokens: config.maxOutputTokens,
|
|
1486
|
+
temperature: config.temperature
|
|
1487
|
+
});
|
|
1488
|
+
} catch (err) {
|
|
1489
|
+
log(`Chat Completions fallback error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
if (!localResponseText || !localResponseText.trim()) {
|
|
1493
|
+
log(`Warning: Model returned empty/whitespace response after all fallbacks. tool calls: ${toolCallsFromModel.length}`);
|
|
1334
1494
|
}
|
|
1335
|
-
log(`Warning: Model returned empty/whitespace response. Raw length: ${localResponseText?.length ?? 0}, tool calls: ${toolCallsFromModel.length}`);
|
|
1336
1495
|
} else {
|
|
1337
1496
|
log(`Model returned text response (${localResponseText.length} chars)`);
|
|
1338
1497
|
}
|
|
@@ -1360,8 +1519,9 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1360
1519
|
&& completionTokens >= responseValidateMinResponseTokens
|
|
1361
1520
|
&& (responseValidateAllowToolCalls || modelToolCalls.length === 0);
|
|
1362
1521
|
if (shouldValidate) {
|
|
1522
|
+
const MAX_VALIDATION_ITERATIONS = 5;
|
|
1363
1523
|
let retriesLeft = responseValidateMaxRetries;
|
|
1364
|
-
|
|
1524
|
+
for (let _validationIter = 0; _validationIter < MAX_VALIDATION_ITERATIONS; _validationIter++) {
|
|
1365
1525
|
if (!responseValidateAllowToolCalls && modelToolCalls.length > 0) {
|
|
1366
1526
|
break;
|
|
1367
1527
|
}
|
|
@@ -1392,8 +1552,6 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1392
1552
|
}
|
|
1393
1553
|
retriesLeft -= 1;
|
|
1394
1554
|
log(`Response validation failed; retrying (${retriesLeft} retries left)`);
|
|
1395
|
-
streamLastSentAt = 0;
|
|
1396
|
-
streamLastSentLength = 0;
|
|
1397
1555
|
const retryGuidance = buildRetryGuidance(validationResult);
|
|
1398
1556
|
const retryAttempt = await runCompletion(retryGuidance);
|
|
1399
1557
|
responseText = retryAttempt.responseText;
|
|
@@ -1464,7 +1622,8 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1464
1622
|
instructions: extractionPrompt.instructions,
|
|
1465
1623
|
input: extractionPrompt.input,
|
|
1466
1624
|
maxOutputTokens: memoryExtractionMaxOutputTokens,
|
|
1467
|
-
temperature: 0.1
|
|
1625
|
+
temperature: 0.1,
|
|
1626
|
+
reasoning: { effort: 'low' as const }
|
|
1468
1627
|
});
|
|
1469
1628
|
const extractionText = await getTextWithFallback(extractionResult, 'memory_extraction');
|
|
1470
1629
|
const extractedItems = parseMemoryExtraction(extractionText);
|
|
@@ -1501,16 +1660,25 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1501
1660
|
};
|
|
1502
1661
|
|
|
1503
1662
|
if (memoryExtractionEnabled && (!input.isScheduledTask || memoryExtractScheduled)) {
|
|
1663
|
+
const runMemoryExtractionWithRetry = async (maxRetries = 2): Promise<void> => {
|
|
1664
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1665
|
+
try {
|
|
1666
|
+
await runMemoryExtraction();
|
|
1667
|
+
return;
|
|
1668
|
+
} catch (err) {
|
|
1669
|
+
log(`Memory extraction attempt ${attempt + 1} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1670
|
+
if (attempt < maxRetries) {
|
|
1671
|
+
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
log('Memory extraction failed after all retries');
|
|
1676
|
+
};
|
|
1677
|
+
|
|
1504
1678
|
if (memoryExtractionAsync && isDaemon) {
|
|
1505
|
-
void
|
|
1506
|
-
log(`Memory extraction failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1507
|
-
});
|
|
1679
|
+
void runMemoryExtractionWithRetry().catch(() => {});
|
|
1508
1680
|
} else {
|
|
1509
|
-
|
|
1510
|
-
await runMemoryExtraction();
|
|
1511
|
-
} catch (err) {
|
|
1512
|
-
log(`Memory extraction failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1513
|
-
}
|
|
1681
|
+
await runMemoryExtractionWithRetry();
|
|
1514
1682
|
}
|
|
1515
1683
|
}
|
|
1516
1684
|
|