@dotsetlabs/dotclaw 2.6.0 → 2.6.1
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/container/agent-runner/package-lock.json +2 -2
- package/container/agent-runner/package.json +1 -1
- package/container/agent-runner/src/index.ts +14 -22
- package/container/agent-runner/src/memory.ts +106 -6
- package/container/agent-runner/src/openrouter-followup.ts +87 -0
- package/package.json +1 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dotclaw-agent-runner",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "dotclaw-agent-runner",
|
|
9
|
-
"version": "2.3.
|
|
9
|
+
"version": "2.3.1",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@openrouter/sdk": "^0.3.0",
|
|
12
12
|
"cron-parser": "^5.0.0",
|
|
@@ -54,6 +54,10 @@ import {
|
|
|
54
54
|
loadImageAttachmentsForInput,
|
|
55
55
|
messagesToOpenRouterInput,
|
|
56
56
|
} from './openrouter-input.js';
|
|
57
|
+
import {
|
|
58
|
+
extractFunctionCallsForReplay,
|
|
59
|
+
toReplayFunctionCallItems,
|
|
60
|
+
} from './openrouter-followup.js';
|
|
57
61
|
|
|
58
62
|
type OpenRouterResult = ReturnType<OpenRouter['callModel']>;
|
|
59
63
|
|
|
@@ -208,21 +212,6 @@ function extractTextFromApiResponse(response: any): string {
|
|
|
208
212
|
return '';
|
|
209
213
|
}
|
|
210
214
|
|
|
211
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
212
|
-
function extractFunctionCalls(response: any): Array<{ id: string; name: string; arguments: any }> {
|
|
213
|
-
const calls: Array<{ id: string; name: string; arguments: unknown }> = [];
|
|
214
|
-
for (const item of response?.output || []) {
|
|
215
|
-
if (item?.type === 'function_call') {
|
|
216
|
-
let args = item.arguments;
|
|
217
|
-
if (typeof args === 'string') {
|
|
218
|
-
try { args = JSON.parse(args); } catch { /* keep as string */ }
|
|
219
|
-
}
|
|
220
|
-
calls.push({ id: item.callId, name: item.name, arguments: args });
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
return calls;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
215
|
function writeOutput(output: ContainerOutput): void {
|
|
227
216
|
console.log(OUTPUT_START_MARKER);
|
|
228
217
|
console.log(JSON.stringify(output));
|
|
@@ -1311,7 +1300,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1311
1300
|
}
|
|
1312
1301
|
|
|
1313
1302
|
responseText = extractTextFromApiResponse(lastResponse);
|
|
1314
|
-
let pendingCalls =
|
|
1303
|
+
let pendingCalls = extractFunctionCallsForReplay(lastResponse);
|
|
1315
1304
|
const callSignatureCounts = new Map<string, number>();
|
|
1316
1305
|
let previousRoundSignature = '';
|
|
1317
1306
|
let repeatedRoundCount = 0;
|
|
@@ -1430,8 +1419,10 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1430
1419
|
reason: nudgeReason,
|
|
1431
1420
|
attempt: toolRequirementNudgeAttempt
|
|
1432
1421
|
});
|
|
1433
|
-
|
|
1434
|
-
|
|
1422
|
+
if (responseText) {
|
|
1423
|
+
conversationInput = [...conversationInput, { role: 'assistant', content: responseText }];
|
|
1424
|
+
}
|
|
1425
|
+
conversationInput = [...conversationInput, { role: 'user', content: nudgePrompt }];
|
|
1435
1426
|
try {
|
|
1436
1427
|
const nudgeResult = openrouter.callModel({
|
|
1437
1428
|
model: currentModel,
|
|
@@ -1448,7 +1439,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1448
1439
|
responseText = nudgeText;
|
|
1449
1440
|
writeStreamChunk(nudgeText);
|
|
1450
1441
|
}
|
|
1451
|
-
pendingCalls =
|
|
1442
|
+
pendingCalls = extractFunctionCallsForReplay(lastResponse);
|
|
1452
1443
|
} catch (nudgeErr) {
|
|
1453
1444
|
log(`Tool requirement nudge failed: ${nudgeErr instanceof Error ? nudgeErr.message : String(nudgeErr)}`);
|
|
1454
1445
|
break;
|
|
@@ -1588,7 +1579,8 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1588
1579
|
|
|
1589
1580
|
// Build follow-up input with FULL conversation context:
|
|
1590
1581
|
// original messages + model output + tool results (accumulated each round)
|
|
1591
|
-
|
|
1582
|
+
const replayFunctionCalls = toReplayFunctionCallItems(pendingCalls);
|
|
1583
|
+
conversationInput = [...conversationInput, ...replayFunctionCalls, ...toolResults];
|
|
1592
1584
|
|
|
1593
1585
|
// Compact oversized tool payloads before follow-up calls to reduce context bloat.
|
|
1594
1586
|
const compactedConversation = compactToolConversationItems(conversationInput, {
|
|
@@ -1695,7 +1687,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1695
1687
|
responseText = retryText;
|
|
1696
1688
|
writeStreamChunk(retryText);
|
|
1697
1689
|
}
|
|
1698
|
-
pendingCalls =
|
|
1690
|
+
pendingCalls = extractFunctionCallsForReplay(lastResponse);
|
|
1699
1691
|
continue;
|
|
1700
1692
|
} catch (retryErr) {
|
|
1701
1693
|
log(`Context overflow retry failed: ${retryErr instanceof Error ? retryErr.message : String(retryErr)}`);
|
|
@@ -1715,7 +1707,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1715
1707
|
writeStreamChunk(followupText);
|
|
1716
1708
|
}
|
|
1717
1709
|
|
|
1718
|
-
pendingCalls =
|
|
1710
|
+
pendingCalls = extractFunctionCallsForReplay(lastResponse);
|
|
1719
1711
|
}
|
|
1720
1712
|
|
|
1721
1713
|
if (toolExecutionRequirement.required && toolCalls.length === 0) {
|
|
@@ -158,21 +158,121 @@ export function appendHistory(ctx: SessionContext, role: 'user' | 'assistant', c
|
|
|
158
158
|
return message;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
function safeStringifyHistoryValue(value: unknown): string {
|
|
162
|
+
try {
|
|
163
|
+
const serialized = JSON.stringify(value);
|
|
164
|
+
return typeof serialized === 'string' ? serialized : String(value);
|
|
165
|
+
} catch {
|
|
166
|
+
return String(value);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function coerceHistoryContentToString(value: unknown): string {
|
|
171
|
+
if (typeof value === 'string') return value;
|
|
172
|
+
if (value == null) return '';
|
|
173
|
+
|
|
174
|
+
if (Array.isArray(value)) {
|
|
175
|
+
const parts = value
|
|
176
|
+
.map((part) => {
|
|
177
|
+
if (!part || typeof part !== 'object') return null;
|
|
178
|
+
const record = part as Record<string, unknown>;
|
|
179
|
+
if (typeof record.text === 'string' && record.text.trim()) return record.text;
|
|
180
|
+
if (typeof record.content === 'string' && record.content.trim()) return record.content;
|
|
181
|
+
if (typeof record.output === 'string' && record.output.trim()) return record.output;
|
|
182
|
+
if (typeof record.refusal === 'string' && record.refusal.trim()) return record.refusal;
|
|
183
|
+
return null;
|
|
184
|
+
})
|
|
185
|
+
.filter((part): part is string => typeof part === 'string');
|
|
186
|
+
if (parts.length > 0) return parts.join('\n');
|
|
187
|
+
return safeStringifyHistoryValue(value);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (typeof value === 'object') {
|
|
191
|
+
const record = value as Record<string, unknown>;
|
|
192
|
+
if (typeof record.text === 'string' && record.text.trim()) return record.text;
|
|
193
|
+
if (typeof record.content === 'string' && record.content.trim()) return record.content;
|
|
194
|
+
if (typeof record.output === 'string' && record.output.trim()) return record.output;
|
|
195
|
+
if (typeof record.refusal === 'string' && record.refusal.trim()) return record.refusal;
|
|
196
|
+
return safeStringifyHistoryValue(value);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return String(value);
|
|
200
|
+
}
|
|
201
|
+
|
|
161
202
|
export function loadHistory(ctx: SessionContext): Message[] {
|
|
162
203
|
if (!fs.existsSync(ctx.historyPath)) return [];
|
|
163
|
-
const
|
|
204
|
+
const raw = fs.readFileSync(ctx.historyPath, 'utf-8');
|
|
205
|
+
if (!raw.trim()) return [];
|
|
206
|
+
const lines = raw.trim().split('\n');
|
|
164
207
|
const messages: Message[] = [];
|
|
208
|
+
let needsRewrite = false;
|
|
209
|
+
let highestSeq = 0;
|
|
165
210
|
for (const line of lines) {
|
|
166
|
-
if (!line.trim())
|
|
211
|
+
if (!line.trim()) {
|
|
212
|
+
needsRewrite = true;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
167
215
|
try {
|
|
168
|
-
const parsed = JSON.parse(line)
|
|
169
|
-
|
|
170
|
-
|
|
216
|
+
const parsed = JSON.parse(line) as Record<string, unknown>;
|
|
217
|
+
const role = parsed?.role === 'user' || parsed?.role === 'assistant'
|
|
218
|
+
? parsed.role
|
|
219
|
+
: null;
|
|
220
|
+
if (!role) {
|
|
221
|
+
needsRewrite = true;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const content = coerceHistoryContentToString(parsed.content);
|
|
226
|
+
if (typeof parsed.content !== 'string') {
|
|
227
|
+
needsRewrite = true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const timestamp = typeof parsed.timestamp === 'string' && parsed.timestamp
|
|
231
|
+
? parsed.timestamp
|
|
232
|
+
: new Date().toISOString();
|
|
233
|
+
if (timestamp !== parsed.timestamp) {
|
|
234
|
+
needsRewrite = true;
|
|
171
235
|
}
|
|
236
|
+
|
|
237
|
+
const parsedSeq = Number(parsed.seq);
|
|
238
|
+
const seq = Number.isFinite(parsedSeq) && parsedSeq > 0
|
|
239
|
+
? Math.floor(parsedSeq)
|
|
240
|
+
: (highestSeq + 1);
|
|
241
|
+
if (seq !== parsedSeq) {
|
|
242
|
+
needsRewrite = true;
|
|
243
|
+
}
|
|
244
|
+
highestSeq = Math.max(highestSeq, seq);
|
|
245
|
+
|
|
246
|
+
messages.push({
|
|
247
|
+
role,
|
|
248
|
+
content,
|
|
249
|
+
timestamp,
|
|
250
|
+
seq
|
|
251
|
+
});
|
|
172
252
|
} catch {
|
|
173
|
-
|
|
253
|
+
needsRewrite = true;
|
|
174
254
|
}
|
|
175
255
|
}
|
|
256
|
+
|
|
257
|
+
messages.sort((a, b) => a.seq - b.seq);
|
|
258
|
+
let nextSeq = 1;
|
|
259
|
+
for (const message of messages) {
|
|
260
|
+
if (message.seq < nextSeq) {
|
|
261
|
+
message.seq = nextSeq;
|
|
262
|
+
needsRewrite = true;
|
|
263
|
+
}
|
|
264
|
+
nextSeq = message.seq + 1;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (ctx.meta.nextSeq < nextSeq) {
|
|
268
|
+
ctx.meta.nextSeq = nextSeq;
|
|
269
|
+
saveSessionMeta(ctx);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (needsRewrite) {
|
|
273
|
+
writeHistory(ctx, messages);
|
|
274
|
+
}
|
|
275
|
+
|
|
176
276
|
return messages;
|
|
177
277
|
}
|
|
178
278
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
type RawRecord = Record<string, unknown>;
|
|
2
|
+
|
|
3
|
+
export type ExtractedFunctionCall = {
|
|
4
|
+
id: string;
|
|
5
|
+
itemId?: string;
|
|
6
|
+
name: string;
|
|
7
|
+
arguments: unknown;
|
|
8
|
+
argumentsText: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type ReplayFunctionCallItem = {
|
|
12
|
+
type: 'function_call';
|
|
13
|
+
id: string;
|
|
14
|
+
callId: string;
|
|
15
|
+
name: string;
|
|
16
|
+
arguments: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function normalizeCallId(record: RawRecord): { callId?: string; itemId?: string } {
|
|
20
|
+
const callId = typeof record.callId === 'string' && record.callId.trim()
|
|
21
|
+
? record.callId.trim()
|
|
22
|
+
: (typeof record.id === 'string' && record.id.trim() ? record.id.trim() : undefined);
|
|
23
|
+
const itemId = typeof record.id === 'string' && record.id.trim()
|
|
24
|
+
? record.id.trim()
|
|
25
|
+
: undefined;
|
|
26
|
+
return { callId, itemId };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeArguments(raw: unknown): { arguments: unknown; argumentsText: string } {
|
|
30
|
+
if (typeof raw === 'string') {
|
|
31
|
+
const trimmed = raw.trim();
|
|
32
|
+
if (!trimmed) return { arguments: {}, argumentsText: '{}' };
|
|
33
|
+
try {
|
|
34
|
+
return { arguments: JSON.parse(trimmed), argumentsText: trimmed };
|
|
35
|
+
} catch {
|
|
36
|
+
return { arguments: raw, argumentsText: raw };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (raw == null) return { arguments: {}, argumentsText: '{}' };
|
|
40
|
+
try {
|
|
41
|
+
const serialized = JSON.stringify(raw);
|
|
42
|
+
if (typeof serialized === 'string') {
|
|
43
|
+
return { arguments: raw, argumentsText: serialized };
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// fall through
|
|
47
|
+
}
|
|
48
|
+
return { arguments: raw, argumentsText: String(raw) };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function extractFromOutput(output: unknown[]): ExtractedFunctionCall[] {
|
|
52
|
+
const calls: ExtractedFunctionCall[] = [];
|
|
53
|
+
for (const item of output) {
|
|
54
|
+
if (!item || typeof item !== 'object') continue;
|
|
55
|
+
const record = item as RawRecord;
|
|
56
|
+
if (record.type !== 'function_call') continue;
|
|
57
|
+
const { callId, itemId } = normalizeCallId(record);
|
|
58
|
+
const name = typeof record.name === 'string' ? record.name.trim() : '';
|
|
59
|
+
if (!callId || !name) continue;
|
|
60
|
+
const normalizedArguments = normalizeArguments(record.arguments);
|
|
61
|
+
calls.push({
|
|
62
|
+
id: callId,
|
|
63
|
+
itemId,
|
|
64
|
+
name,
|
|
65
|
+
arguments: normalizedArguments.arguments,
|
|
66
|
+
argumentsText: normalizedArguments.argumentsText
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return calls;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function extractFunctionCallsForReplay(response: unknown): ExtractedFunctionCall[] {
|
|
73
|
+
if (!response || typeof response !== 'object') return [];
|
|
74
|
+
const output = (response as { output?: unknown }).output;
|
|
75
|
+
if (!Array.isArray(output)) return [];
|
|
76
|
+
return extractFromOutput(output);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function toReplayFunctionCallItems(calls: ExtractedFunctionCall[]): ReplayFunctionCallItem[] {
|
|
80
|
+
return calls.map((call) => ({
|
|
81
|
+
type: 'function_call',
|
|
82
|
+
id: call.itemId || call.id,
|
|
83
|
+
callId: call.id,
|
|
84
|
+
name: call.name,
|
|
85
|
+
arguments: call.argumentsText || '{}'
|
|
86
|
+
}));
|
|
87
|
+
}
|