actoviq-agent-sdk 0.3.0 → 0.3.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/dist/src/cli/actoviq-react.js +16 -7
- package/dist/src/cli/actoviq-react.js.map +1 -1
- package/dist/src/config/anthropicEnvMapping.d.ts +18 -0
- package/dist/src/config/anthropicEnvMapping.d.ts.map +1 -0
- package/dist/src/config/anthropicEnvMapping.js +40 -0
- package/dist/src/config/anthropicEnvMapping.js.map +1 -0
- package/dist/src/config/resolveRuntimeConfig.d.ts.map +1 -1
- package/dist/src/config/resolveRuntimeConfig.js +37 -13
- package/dist/src/config/resolveRuntimeConfig.js.map +1 -1
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/memory/actoviqMemory.d.ts.map +1 -1
- package/dist/src/memory/actoviqMemory.js +10 -6
- package/dist/src/memory/actoviqMemory.js.map +1 -1
- package/dist/src/memory/actoviqTranscriptLogger.d.ts.map +1 -1
- package/dist/src/memory/actoviqTranscriptLogger.js +2 -2
- package/dist/src/memory/actoviqTranscriptLogger.js.map +1 -1
- package/dist/src/parity/actoviqBridgeEvents.d.ts.map +1 -1
- package/dist/src/parity/actoviqBridgeEvents.js +4 -2
- package/dist/src/parity/actoviqBridgeEvents.js.map +1 -1
- package/dist/src/parity/actoviqBridgeSdk.d.ts.map +1 -1
- package/dist/src/parity/actoviqBridgeSdk.js +9 -1
- package/dist/src/parity/actoviqBridgeSdk.js.map +1 -1
- package/dist/src/parity/actoviqCleanBridgeCompatSdk.d.ts +277 -0
- package/dist/src/parity/actoviqCleanBridgeCompatSdk.d.ts.map +1 -0
- package/dist/src/parity/actoviqCleanBridgeCompatSdk.js +1391 -0
- package/dist/src/parity/actoviqCleanBridgeCompatSdk.js.map +1 -0
- package/dist/src/parity/portableSessions.d.ts.map +1 -1
- package/dist/src/parity/portableSessions.js +6 -2
- package/dist/src/parity/portableSessions.js.map +1 -1
- package/dist/src/provider/client.d.ts +3 -0
- package/dist/src/provider/client.d.ts.map +1 -1
- package/dist/src/provider/client.js +77 -6
- package/dist/src/provider/client.js.map +1 -1
- package/dist/src/provider/openai-client.d.ts.map +1 -1
- package/dist/src/provider/openai-client.js +43 -4
- package/dist/src/provider/openai-client.js.map +1 -1
- package/dist/src/runtime/actoviqAgents.d.ts +6 -0
- package/dist/src/runtime/actoviqAgents.d.ts.map +1 -1
- package/dist/src/runtime/actoviqAgents.js +110 -14
- package/dist/src/runtime/actoviqAgents.js.map +1 -1
- package/dist/src/runtime/actoviqApiMicrocompact.d.ts +14 -0
- package/dist/src/runtime/actoviqApiMicrocompact.d.ts.map +1 -1
- package/dist/src/runtime/actoviqApiMicrocompact.js +102 -0
- package/dist/src/runtime/actoviqApiMicrocompact.js.map +1 -1
- package/dist/src/runtime/actoviqBackgroundTasks.d.ts +1 -0
- package/dist/src/runtime/actoviqBackgroundTasks.d.ts.map +1 -1
- package/dist/src/runtime/actoviqBackgroundTasks.js +1 -0
- package/dist/src/runtime/actoviqBackgroundTasks.js.map +1 -1
- package/dist/src/runtime/actoviqCompact.d.ts +43 -0
- package/dist/src/runtime/actoviqCompact.d.ts.map +1 -1
- package/dist/src/runtime/actoviqCompact.js +237 -7
- package/dist/src/runtime/actoviqCompact.js.map +1 -1
- package/dist/src/runtime/actoviqPermissions.js +18 -18
- package/dist/src/runtime/actoviqPermissions.js.map +1 -1
- package/dist/src/runtime/agentClient.d.ts +11 -0
- package/dist/src/runtime/agentClient.d.ts.map +1 -1
- package/dist/src/runtime/agentClient.js +302 -4
- package/dist/src/runtime/agentClient.js.map +1 -1
- package/dist/src/runtime/conversationEngine.d.ts.map +1 -1
- package/dist/src/runtime/conversationEngine.js +496 -36
- package/dist/src/runtime/conversationEngine.js.map +1 -1
- package/dist/src/runtime/defaultActoviqAgents.d.ts +3 -0
- package/dist/src/runtime/defaultActoviqAgents.d.ts.map +1 -0
- package/dist/src/runtime/defaultActoviqAgents.js +44 -0
- package/dist/src/runtime/defaultActoviqAgents.js.map +1 -0
- package/dist/src/runtime/safetyChecks.d.ts.map +1 -1
- package/dist/src/runtime/safetyChecks.js +15 -9
- package/dist/src/runtime/safetyChecks.js.map +1 -1
- package/dist/src/runtime/sessionManager.d.ts.map +1 -1
- package/dist/src/runtime/sessionManager.js +10 -5
- package/dist/src/runtime/sessionManager.js.map +1 -1
- package/dist/src/runtime/tools.d.ts.map +1 -1
- package/dist/src/runtime/tools.js +1 -0
- package/dist/src/runtime/tools.js.map +1 -1
- package/dist/src/scheduling/cron.d.ts.map +1 -1
- package/dist/src/scheduling/cron.js +3 -1
- package/dist/src/scheduling/cron.js.map +1 -1
- package/dist/src/storage/backgroundTaskStore.d.ts.map +1 -1
- package/dist/src/storage/backgroundTaskStore.js +3 -2
- package/dist/src/storage/backgroundTaskStore.js.map +1 -1
- package/dist/src/storage/mailboxStore.d.ts.map +1 -1
- package/dist/src/storage/mailboxStore.js +3 -3
- package/dist/src/storage/mailboxStore.js.map +1 -1
- package/dist/src/storage/pathSafety.d.ts +4 -0
- package/dist/src/storage/pathSafety.d.ts.map +1 -0
- package/dist/src/storage/pathSafety.js +28 -0
- package/dist/src/storage/pathSafety.js.map +1 -0
- package/dist/src/storage/sessionStore.d.ts +2 -2
- package/dist/src/storage/sessionStore.d.ts.map +1 -1
- package/dist/src/storage/sessionStore.js +23 -8
- package/dist/src/storage/sessionStore.js.map +1 -1
- package/dist/src/storage/teammateStore.d.ts.map +1 -1
- package/dist/src/storage/teammateStore.js +3 -2
- package/dist/src/storage/teammateStore.js.map +1 -1
- package/dist/src/tools/actoviqCoreTools.d.ts +7 -2
- package/dist/src/tools/actoviqCoreTools.d.ts.map +1 -1
- package/dist/src/tools/actoviqCoreTools.js +2 -2
- package/dist/src/tools/actoviqCoreTools.js.map +1 -1
- package/dist/src/tools/actoviqFileTools.d.ts.map +1 -1
- package/dist/src/tools/actoviqFileTools.js +16 -5
- package/dist/src/tools/actoviqFileTools.js.map +1 -1
- package/dist/src/tools/actoviqNotebookEdit.d.ts.map +1 -1
- package/dist/src/tools/actoviqNotebookEdit.js +121 -4
- package/dist/src/tools/actoviqNotebookEdit.js.map +1 -1
- package/dist/src/tools/actoviqShellTools.d.ts.map +1 -1
- package/dist/src/tools/actoviqShellTools.js +38 -9
- package/dist/src/tools/actoviqShellTools.js.map +1 -1
- package/dist/src/tools/actoviqWebTools.d.ts.map +1 -1
- package/dist/src/tools/actoviqWebTools.js +50 -18
- package/dist/src/tools/actoviqWebTools.js.map +1 -1
- package/dist/src/tools/bash/BashTool.js +4 -4
- package/dist/src/tools/bash/BashTool.js.map +1 -1
- package/dist/src/tools/todo/TodoWriteTool.d.ts +14 -0
- package/dist/src/tools/todo/TodoWriteTool.d.ts.map +1 -1
- package/dist/src/tools/todo/TodoWriteTool.js +45 -2
- package/dist/src/tools/todo/TodoWriteTool.js.map +1 -1
- package/dist/src/types.d.ts +116 -1
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js.map +1 -1
- package/dist/src/workflow/workflowEngine.js +26 -3
- package/dist/src/workflow/workflowEngine.js.map +1 -1
- package/dist/src/workspace/actoviqWorkspace.d.ts.map +1 -1
- package/dist/src/workspace/actoviqWorkspace.js +25 -2
- package/dist/src/workspace/actoviqWorkspace.js.map +1 -1
- package/package.json +25 -1
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { ActoviqProviderApiError, ActoviqSdkError, RunAbortedError, ToolExecutionError } from '../errors.js';
|
|
4
|
+
import { getActoviqTodoSnapshot, formatActoviqTodoListLines, TODO_WRITE_TOOL_NAME, } from '../tools/todo/TodoWriteTool.js';
|
|
2
5
|
import { asError, deepClone, nowIso, signalAborted } from './helpers.js';
|
|
3
6
|
import { resolveActoviqPostSamplingHooks, resolveActoviqStopHooks } from '../hooks/actoviqHooks.js';
|
|
4
|
-
import {
|
|
7
|
+
import { compactActoviqConversationIfNeeded } from './actoviqCompact.js';
|
|
8
|
+
import { getActoviqApiContextManagement, prepareActoviqProviderRequestMessages, } from './actoviqApiMicrocompact.js';
|
|
5
9
|
import { decideActoviqToolPermission } from './actoviqPermissions.js';
|
|
6
|
-
import { assistantMessageToParam, buildUserMessage, extractTextFromContent, } from './messageUtils.js';
|
|
10
|
+
import { assistantMessageToParam, buildUserMessage, extractTextFromContent, extractTextFromToolResultContent, } from './messageUtils.js';
|
|
11
|
+
const MAX_CONCURRENT_TOOL_USES = 10;
|
|
12
|
+
const TODO_REMINDER_INTERVAL = 10;
|
|
13
|
+
const MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3;
|
|
7
14
|
export async function executeConversation(options) {
|
|
8
15
|
const startedAt = nowIso();
|
|
9
|
-
|
|
16
|
+
let model = options.model ?? options.config.model;
|
|
10
17
|
const promptText = typeof options.input === 'string' ? options.input : extractTextFromContent(options.input);
|
|
11
18
|
const postSamplingHooks = resolveActoviqPostSamplingHooks(options.hooks);
|
|
12
19
|
const conversation = deepClone(options.messages ?? []);
|
|
@@ -32,16 +39,50 @@ export async function executeConversation(options) {
|
|
|
32
39
|
let toolResults = [];
|
|
33
40
|
let consecutiveFailures = 0;
|
|
34
41
|
let lastFailedTool = '';
|
|
42
|
+
let maxTokensRecoveryCount = 0;
|
|
43
|
+
let modelFallbackUsed = false;
|
|
44
|
+
let iterationsSinceTodoWrite = 0;
|
|
45
|
+
let streamInterruptionRetryIteration = 0;
|
|
46
|
+
let streamInterruptionRetries = 0;
|
|
35
47
|
while (true) {
|
|
36
48
|
ensureNotAborted(options.signal);
|
|
37
49
|
iteration += 1;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
// In-loop auto-compact: keep a single long run within the context window
|
|
51
|
+
// by summarizing old turns before each provider request. Mirrors Claude
|
|
52
|
+
// Code's per-iteration autocompact and never throws.
|
|
53
|
+
const loopCompact = await compactActoviqConversationIfNeeded(conversation, {
|
|
54
|
+
model,
|
|
55
|
+
modelApi: options.modelApi,
|
|
56
|
+
compactConfig: options.config.compact,
|
|
57
|
+
maxTokens: options.maxTokens ?? options.config.maxTokens,
|
|
58
|
+
runKey: options.runId,
|
|
59
|
+
signal: options.signal,
|
|
43
60
|
});
|
|
44
|
-
|
|
61
|
+
if (loopCompact.compacted) {
|
|
62
|
+
conversation.splice(0, conversation.length, ...loopCompact.messages);
|
|
63
|
+
options.emit?.({
|
|
64
|
+
type: 'conversation.compacted',
|
|
65
|
+
runId: options.runId,
|
|
66
|
+
iteration,
|
|
67
|
+
tokenEstimateBefore: loopCompact.tokenEstimateBefore,
|
|
68
|
+
tokenEstimateAfter: loopCompact.tokenEstimateAfter,
|
|
69
|
+
messagesSummarized: loopCompact.messagesSummarized,
|
|
70
|
+
preservedMessages: loopCompact.preservedMessages,
|
|
71
|
+
clearedToolResults: loopCompact.clearedToolResults,
|
|
72
|
+
timestamp: nowIso(),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const useAnthropicContextManagement = isAnthropicAPI(options.config.baseURL);
|
|
76
|
+
const preparedMessages = prepareActoviqProviderRequestMessages(conversation, options.config.compact, { localToolResultMicrocompact: !useAnthropicContextManagement });
|
|
77
|
+
let localMicrocompact = preparedMessages.clearedToolResults > 0
|
|
78
|
+
? {
|
|
79
|
+
enabled: true,
|
|
80
|
+
clearedToolResults: preparedMessages.clearedToolResults,
|
|
81
|
+
tokenEstimateBefore: preparedMessages.tokenEstimateBefore,
|
|
82
|
+
tokenEstimateAfter: preparedMessages.tokenEstimateAfter,
|
|
83
|
+
}
|
|
84
|
+
: undefined;
|
|
85
|
+
let request = {
|
|
45
86
|
model,
|
|
46
87
|
max_tokens: options.maxTokens ?? options.config.maxTokens,
|
|
47
88
|
system: options.systemPrompt ?? options.config.systemPrompt,
|
|
@@ -53,15 +94,102 @@ export async function executeConversation(options) {
|
|
|
53
94
|
: undefined,
|
|
54
95
|
// Skip context_management for third-party providers — their APIs
|
|
55
96
|
// may not support server-side message edits, causing undefined behavior.
|
|
56
|
-
context_management:
|
|
97
|
+
context_management: useAnthropicContextManagement
|
|
57
98
|
? getActoviqApiContextManagement(conversation, options.config.compact)
|
|
58
99
|
: undefined,
|
|
59
|
-
messages: deepClone(
|
|
100
|
+
messages: deepClone(preparedMessages.messages),
|
|
60
101
|
signal: options.signal,
|
|
61
102
|
};
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
103
|
+
let requestByteLength = getRequestByteLength(request);
|
|
104
|
+
const maxRequestBytes = options.config.compact.apiMicrocompactMaxRequestBytes;
|
|
105
|
+
if (!useAnthropicContextManagement &&
|
|
106
|
+
maxRequestBytes &&
|
|
107
|
+
requestByteLength > maxRequestBytes) {
|
|
108
|
+
const forcedMessages = prepareActoviqProviderRequestMessages(conversation, options.config.compact, { localToolResultMicrocompact: true, force: true });
|
|
109
|
+
const requestByteLengthBefore = requestByteLength;
|
|
110
|
+
request = {
|
|
111
|
+
...request,
|
|
112
|
+
messages: deepClone(forcedMessages.messages),
|
|
113
|
+
};
|
|
114
|
+
requestByteLength = getRequestByteLength(request);
|
|
115
|
+
localMicrocompact = {
|
|
116
|
+
enabled: true,
|
|
117
|
+
clearedToolResults: forcedMessages.clearedToolResults,
|
|
118
|
+
tokenEstimateBefore: forcedMessages.tokenEstimateBefore,
|
|
119
|
+
tokenEstimateAfter: forcedMessages.tokenEstimateAfter,
|
|
120
|
+
requestByteLengthBefore,
|
|
121
|
+
requestByteLengthAfter: requestByteLength,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
// Prompt caching: a single cache breakpoint on the last message caches the
|
|
125
|
+
// entire prefix (tools + system + conversation). Anthropic API hosts only.
|
|
126
|
+
if (useAnthropicContextManagement && options.config.promptCachingEnabled !== false) {
|
|
127
|
+
applyCacheControlToLastMessage(request.messages);
|
|
128
|
+
}
|
|
129
|
+
options.emit?.({
|
|
130
|
+
type: 'request.started',
|
|
131
|
+
runId: options.runId,
|
|
132
|
+
iteration,
|
|
133
|
+
requestTokenEstimate: localMicrocompact?.tokenEstimateAfter ?? preparedMessages.tokenEstimateAfter,
|
|
134
|
+
requestByteLength,
|
|
135
|
+
localMicrocompact,
|
|
136
|
+
timestamp: nowIso(),
|
|
137
|
+
});
|
|
138
|
+
let message;
|
|
139
|
+
try {
|
|
140
|
+
message = options.streaming
|
|
141
|
+
? await consumeStream(request, options.modelApi, iteration, options.emit, options.runId)
|
|
142
|
+
: await options.modelApi.createMessage(request);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
// Mid-stream interruptions (socket loss after response headers) never hit
|
|
146
|
+
// the provider-level retry loop; retry the whole iteration here.
|
|
147
|
+
if (streamInterruptionRetryIteration !== iteration) {
|
|
148
|
+
streamInterruptionRetryIteration = iteration;
|
|
149
|
+
streamInterruptionRetries = 0;
|
|
150
|
+
}
|
|
151
|
+
if (isRetryableStreamInterruption(error) &&
|
|
152
|
+
streamInterruptionRetries < MAX_STREAM_INTERRUPTION_RETRIES) {
|
|
153
|
+
streamInterruptionRetries += 1;
|
|
154
|
+
options.emit?.({
|
|
155
|
+
type: 'request.interrupted',
|
|
156
|
+
runId: options.runId,
|
|
157
|
+
iteration,
|
|
158
|
+
retry: streamInterruptionRetries,
|
|
159
|
+
maxRetries: MAX_STREAM_INTERRUPTION_RETRIES,
|
|
160
|
+
reason: asError(error).message,
|
|
161
|
+
timestamp: nowIso(),
|
|
162
|
+
});
|
|
163
|
+
await sleep(1000 * streamInterruptionRetries);
|
|
164
|
+
iteration -= 1;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
// Fallback model: after transport-level retries are exhausted, switch to
|
|
168
|
+
// the configured fallback model once and retry this iteration.
|
|
169
|
+
const fallbackModel = options.config.fallbackModel;
|
|
170
|
+
if (fallbackModel &&
|
|
171
|
+
!modelFallbackUsed &&
|
|
172
|
+
fallbackModel !== model &&
|
|
173
|
+
isModelFallbackEligibleError(error)) {
|
|
174
|
+
modelFallbackUsed = true;
|
|
175
|
+
const fromModel = model;
|
|
176
|
+
model = fallbackModel;
|
|
177
|
+
options.emit?.({
|
|
178
|
+
type: 'model.fallback',
|
|
179
|
+
runId: options.runId,
|
|
180
|
+
iteration,
|
|
181
|
+
fromModel,
|
|
182
|
+
toModel: fallbackModel,
|
|
183
|
+
reason: asError(error).message,
|
|
184
|
+
timestamp: nowIso(),
|
|
185
|
+
});
|
|
186
|
+
iteration -= 1;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
streamInterruptionRetryIteration = 0;
|
|
192
|
+
streamInterruptionRetries = 0;
|
|
65
193
|
if (!options.streaming) {
|
|
66
194
|
const text = extractTextFromContent(message.content);
|
|
67
195
|
if (text) {
|
|
@@ -171,8 +299,26 @@ export async function executeConversation(options) {
|
|
|
171
299
|
usage: message.usage,
|
|
172
300
|
text: extractTextFromContent(message.content),
|
|
173
301
|
createdAt: nowIso(),
|
|
302
|
+
requestTokenEstimate: localMicrocompact?.tokenEstimateAfter ?? preparedMessages.tokenEstimateAfter,
|
|
303
|
+
requestByteLength,
|
|
304
|
+
localMicrocompact,
|
|
174
305
|
});
|
|
175
306
|
const toolUses = message.content.filter((block) => block.type === 'tool_use');
|
|
307
|
+
// max_tokens recovery: when the response was truncated mid-thought with no
|
|
308
|
+
// tool calls, nudge the model to resume instead of ending the run on a
|
|
309
|
+
// half-finished answer. Mirrors Claude Code's recovery loop (limit 3).
|
|
310
|
+
if (!preventContinuation &&
|
|
311
|
+
toolUses.length === 0 &&
|
|
312
|
+
message.stop_reason === 'max_tokens' &&
|
|
313
|
+
maxTokensRecoveryCount < MAX_OUTPUT_TOKENS_RECOVERY_LIMIT) {
|
|
314
|
+
maxTokensRecoveryCount += 1;
|
|
315
|
+
conversation.push({
|
|
316
|
+
role: 'user',
|
|
317
|
+
content: 'Output token limit hit. Resume directly — no apology, no recap of what you were doing. ' +
|
|
318
|
+
'Pick up mid-thought if that is where the cut happened. Break remaining work into smaller pieces.',
|
|
319
|
+
});
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
176
322
|
if (preventContinuation || toolUses.length === 0) {
|
|
177
323
|
const completedAt = nowIso();
|
|
178
324
|
if (!finalMessage) {
|
|
@@ -196,6 +342,17 @@ export async function executeConversation(options) {
|
|
|
196
342
|
};
|
|
197
343
|
}
|
|
198
344
|
if (iteration >= options.config.maxToolIterations) {
|
|
345
|
+
if (toolUses.length > 0) {
|
|
346
|
+
conversation.push({
|
|
347
|
+
role: 'user',
|
|
348
|
+
content: toolUses.map(toolUse => ({
|
|
349
|
+
type: 'tool_result',
|
|
350
|
+
tool_use_id: toolUse.id,
|
|
351
|
+
is_error: true,
|
|
352
|
+
content: `The run exceeded the max tool iteration limit (${options.config.maxToolIterations}) before this tool could execute.`,
|
|
353
|
+
})),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
199
356
|
const completedAt = nowIso();
|
|
200
357
|
if (finalMessage) {
|
|
201
358
|
return {
|
|
@@ -206,6 +363,8 @@ export async function executeConversation(options) {
|
|
|
206
363
|
message: finalMessage,
|
|
207
364
|
messages: conversation,
|
|
208
365
|
stopReason: finalMessage.stop_reason ?? null,
|
|
366
|
+
incompleteReason: `max_tool_iterations_exceeded:${options.config.maxToolIterations}`,
|
|
367
|
+
maxToolIterationsExceeded: true,
|
|
209
368
|
hookStopReason,
|
|
210
369
|
usage: finalMessage.usage,
|
|
211
370
|
requests: requestSummaries,
|
|
@@ -217,7 +376,7 @@ export async function executeConversation(options) {
|
|
|
217
376
|
}
|
|
218
377
|
throw new ActoviqSdkError(`The run exceeded the max tool iteration limit (${options.config.maxToolIterations}).`);
|
|
219
378
|
}
|
|
220
|
-
|
|
379
|
+
const runSingleToolUse = async (toolUse) => {
|
|
221
380
|
ensureNotAborted(options.signal);
|
|
222
381
|
const started = nowIso();
|
|
223
382
|
const startedClock = Date.now();
|
|
@@ -306,10 +465,22 @@ export async function executeConversation(options) {
|
|
|
306
465
|
model,
|
|
307
466
|
provider: options.config.provider,
|
|
308
467
|
}, onProgress);
|
|
309
|
-
|
|
468
|
+
// Per-tool declared cap first (default 50k via tool factory), clamped
|
|
469
|
+
// by the global artifact ceiling. MCP tools without a declared cap use
|
|
470
|
+
// the global ceiling only.
|
|
471
|
+
const artifactMaxChars = Math.min(adapter.maxResultSizeChars ?? Number.POSITIVE_INFINITY, options.config.compact.toolResultArtifactMaxChars ?? 80_000);
|
|
472
|
+
const modelFacingExecution = await artifactToolExecutionIfLarge(execution, {
|
|
473
|
+
runId: options.runId,
|
|
474
|
+
iteration,
|
|
475
|
+
toolUseId: toolUse.id,
|
|
476
|
+
toolName: toolUse.name,
|
|
477
|
+
workDir: options.config.workDir,
|
|
478
|
+
maxChars: artifactMaxChars,
|
|
479
|
+
});
|
|
480
|
+
outputText = modelFacingExecution.text;
|
|
310
481
|
output = execution.rawOutput;
|
|
311
482
|
isError = execution.isError ?? false;
|
|
312
|
-
content =
|
|
483
|
+
content = modelFacingExecution.content;
|
|
313
484
|
}
|
|
314
485
|
catch (error) {
|
|
315
486
|
const normalized = error instanceof ToolExecutionError
|
|
@@ -328,20 +499,61 @@ export async function executeConversation(options) {
|
|
|
328
499
|
completedAt: nowIso(),
|
|
329
500
|
durationMs: Date.now() - startedClock,
|
|
330
501
|
};
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
502
|
+
return {
|
|
503
|
+
record,
|
|
504
|
+
resultBlock: {
|
|
505
|
+
type: 'tool_result',
|
|
506
|
+
tool_use_id: toolUse.id,
|
|
507
|
+
content,
|
|
508
|
+
is_error: isError,
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
};
|
|
512
|
+
// Execute tool batches: consecutive concurrency-safe (read-only) tools run
|
|
513
|
+
// in parallel (limit 10), everything else serially. Results are recorded
|
|
514
|
+
// in the original tool_use order regardless of completion order.
|
|
515
|
+
for (const batch of partitionToolUsesForConcurrency(toolUses, toolMap)) {
|
|
516
|
+
const outcomes = batch.concurrent && batch.toolUses.length > 1
|
|
517
|
+
? await runWithConcurrencyLimit(batch.toolUses, MAX_CONCURRENT_TOOL_USES, runSingleToolUse)
|
|
518
|
+
: await runSequentially(batch.toolUses, runSingleToolUse);
|
|
519
|
+
for (const outcome of outcomes) {
|
|
520
|
+
toolCalls.push(outcome.record);
|
|
521
|
+
toolResults.push(outcome.resultBlock);
|
|
522
|
+
options.emit?.({
|
|
523
|
+
type: 'tool.result',
|
|
524
|
+
runId: options.runId,
|
|
525
|
+
iteration,
|
|
526
|
+
result: outcome.record,
|
|
527
|
+
timestamp: outcome.record.completedAt,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
// Aggregate per-message budget: N parallel tools can each pass the
|
|
532
|
+
// per-tool cap yet collectively flood one user message. Artifact the
|
|
533
|
+
// largest results until the batch fits (mirrors Claude Code's 200k cap).
|
|
534
|
+
await enforceToolResultsAggregateBudget(toolResults, {
|
|
535
|
+
runId: options.runId,
|
|
536
|
+
iteration,
|
|
537
|
+
workDir: options.config.workDir,
|
|
538
|
+
maxTotalChars: options.config.compact.toolResultsPerMessageMaxChars ?? 200_000,
|
|
539
|
+
nameByToolUseId: new Map(toolCalls.slice(-toolUses.length).map((record) => [record.id, record.publicName])),
|
|
540
|
+
});
|
|
541
|
+
// Todo continuity reminder: when TodoWrite is available but unused for a
|
|
542
|
+
// stretch of iterations, re-inject the current todo state so long runs
|
|
543
|
+
// stay anchored to the plan (mirrors Claude Code's 10-turn reminder).
|
|
544
|
+
if (toolUses.some((toolUse) => toolUse.name === TODO_WRITE_TOOL_NAME)) {
|
|
545
|
+
iterationsSinceTodoWrite = 0;
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
iterationsSinceTodoWrite += 1;
|
|
549
|
+
if (toolMap.has(TODO_WRITE_TOOL_NAME) && iterationsSinceTodoWrite >= TODO_REMINDER_INTERVAL) {
|
|
550
|
+
const reminder = buildTodoReminderText(getActoviqTodoSnapshot(options.sessionId ?? options.runId));
|
|
551
|
+
const lastResult = toolResults.at(-1);
|
|
552
|
+
if (lastResult) {
|
|
553
|
+
appendTextToToolResultContent(lastResult, reminder);
|
|
554
|
+
iterationsSinceTodoWrite = 0;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
345
557
|
}
|
|
346
558
|
// Detect repeated tool failures to prevent retry loops
|
|
347
559
|
// Only check newly added results (from this iteration)
|
|
@@ -361,6 +573,14 @@ export async function executeConversation(options) {
|
|
|
361
573
|
lastFailedTool = '';
|
|
362
574
|
}
|
|
363
575
|
}
|
|
576
|
+
// Always push tool results before any early return so the conversation
|
|
577
|
+
// never ends with dangling tool_use blocks (which would make a persisted
|
|
578
|
+
// session unusable: providers reject unpaired tool_use ids on resume).
|
|
579
|
+
conversation.push({
|
|
580
|
+
role: 'user',
|
|
581
|
+
content: toolResults,
|
|
582
|
+
});
|
|
583
|
+
toolResults = [];
|
|
364
584
|
if (consecutiveFailures >= 3 && lastFailedTool) {
|
|
365
585
|
const completedAt = nowIso();
|
|
366
586
|
if (finalMessage) {
|
|
@@ -372,6 +592,7 @@ export async function executeConversation(options) {
|
|
|
372
592
|
message: finalMessage,
|
|
373
593
|
messages: conversation,
|
|
374
594
|
stopReason: finalMessage.stop_reason ?? null,
|
|
595
|
+
incompleteReason: `consecutive_tool_failures:${lastFailedTool}`,
|
|
375
596
|
hookStopReason,
|
|
376
597
|
usage: finalMessage.usage,
|
|
377
598
|
requests: requestSummaries,
|
|
@@ -383,13 +604,252 @@ export async function executeConversation(options) {
|
|
|
383
604
|
}
|
|
384
605
|
throw new ActoviqSdkError(`Tool "${lastFailedTool}" failed ${consecutiveFailures} times consecutively. Stopping to prevent retry loop.`);
|
|
385
606
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const ARTIFACTED_OUTPUT_MARKER = 'Tool output was large (';
|
|
610
|
+
async function writeToolResultArtifact(text, options) {
|
|
611
|
+
const artifactDir = path.join(options.workDir, '.actoviq-artifacts', 'tool-results', sanitizeArtifactSegment(options.runId));
|
|
612
|
+
await mkdir(artifactDir, { recursive: true });
|
|
613
|
+
const artifactPath = path.join(artifactDir, `${String(options.iteration).padStart(3, '0')}-${sanitizeArtifactSegment(options.toolUseId)}-${sanitizeArtifactSegment(options.toolName)}.txt`);
|
|
614
|
+
await writeFile(artifactPath, text, 'utf8');
|
|
615
|
+
const preview = text.slice(0, Math.max(options.previewChars, 0));
|
|
616
|
+
const omittedChars = Math.max(text.length - preview.length, 0);
|
|
617
|
+
return [
|
|
618
|
+
`${ARTIFACTED_OUTPUT_MARKER}${text.length} characters).`,
|
|
619
|
+
`Full output saved to: ${artifactPath}`,
|
|
620
|
+
omittedChars > 0 ? `Preview (${preview.length} characters, ${omittedChars} omitted):` : 'Preview:',
|
|
621
|
+
preview,
|
|
622
|
+
].join('\n');
|
|
623
|
+
}
|
|
624
|
+
async function artifactToolExecutionIfLarge(execution, options) {
|
|
625
|
+
if (options.maxChars <= 0 || execution.text.length <= options.maxChars) {
|
|
626
|
+
return {
|
|
627
|
+
text: execution.text,
|
|
628
|
+
content: execution.content,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
const summary = await writeToolResultArtifact(execution.text, {
|
|
632
|
+
runId: options.runId,
|
|
633
|
+
iteration: options.iteration,
|
|
634
|
+
toolUseId: options.toolUseId,
|
|
635
|
+
toolName: options.toolName,
|
|
636
|
+
workDir: options.workDir,
|
|
637
|
+
previewChars: Math.min(options.maxChars, 4_000),
|
|
638
|
+
});
|
|
639
|
+
return {
|
|
640
|
+
text: summary,
|
|
641
|
+
content: summary,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Enforce an aggregate character budget over all tool_result blocks produced
|
|
646
|
+
* in one iteration. The largest non-error, not-yet-artifacted results are
|
|
647
|
+
* persisted to disk (largest first) until the batch fits the budget.
|
|
648
|
+
*/
|
|
649
|
+
async function enforceToolResultsAggregateBudget(toolResults, options) {
|
|
650
|
+
if (options.maxTotalChars <= 0 || toolResults.length === 0) {
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const measured = toolResults.map((block, index) => ({
|
|
654
|
+
block,
|
|
655
|
+
index,
|
|
656
|
+
length: extractTextFromToolResultContent(block.content).length,
|
|
657
|
+
}));
|
|
658
|
+
let totalChars = measured.reduce((sum, entry) => sum + entry.length, 0);
|
|
659
|
+
if (totalChars <= options.maxTotalChars) {
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const candidates = measured
|
|
663
|
+
.filter((entry) => {
|
|
664
|
+
if (entry.block.is_error)
|
|
665
|
+
return false;
|
|
666
|
+
const text = extractTextFromToolResultContent(entry.block.content);
|
|
667
|
+
return !text.startsWith(ARTIFACTED_OUTPUT_MARKER);
|
|
668
|
+
})
|
|
669
|
+
.sort((a, b) => b.length - a.length);
|
|
670
|
+
for (const candidate of candidates) {
|
|
671
|
+
if (totalChars <= options.maxTotalChars) {
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
const text = extractTextFromToolResultContent(candidate.block.content);
|
|
675
|
+
if (!text) {
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
const summary = await writeToolResultArtifact(text, {
|
|
679
|
+
runId: options.runId,
|
|
680
|
+
iteration: options.iteration,
|
|
681
|
+
toolUseId: candidate.block.tool_use_id,
|
|
682
|
+
toolName: options.nameByToolUseId.get(candidate.block.tool_use_id) ?? 'tool',
|
|
683
|
+
workDir: options.workDir,
|
|
684
|
+
previewChars: 2_000,
|
|
389
685
|
});
|
|
390
|
-
|
|
686
|
+
candidate.block.content = summary;
|
|
687
|
+
totalChars = totalChars - text.length + summary.length;
|
|
391
688
|
}
|
|
392
689
|
}
|
|
690
|
+
/**
|
|
691
|
+
* Partition tool calls into batches: consecutive concurrency-safe tools are
|
|
692
|
+
* grouped for parallel execution, everything else runs as a serial batch of
|
|
693
|
+
* one. Mirrors Claude Code's read-only batching behavior.
|
|
694
|
+
*/
|
|
695
|
+
function partitionToolUsesForConcurrency(toolUses, toolMap) {
|
|
696
|
+
const batches = [];
|
|
697
|
+
for (const toolUse of toolUses) {
|
|
698
|
+
const adapter = toolMap.get(toolUse.name);
|
|
699
|
+
let safe = false;
|
|
700
|
+
if (adapter && adapter.requiresUserInteraction?.() !== true) {
|
|
701
|
+
try {
|
|
702
|
+
safe = adapter.isConcurrencySafe?.() ?? adapter.isReadOnly?.(toolUse.input) ?? false;
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
safe = false;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
const last = batches.at(-1);
|
|
709
|
+
if (safe && last?.concurrent) {
|
|
710
|
+
last.toolUses.push(toolUse);
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
batches.push({ concurrent: safe, toolUses: [toolUse] });
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return batches;
|
|
717
|
+
}
|
|
718
|
+
async function runSequentially(items, run) {
|
|
719
|
+
const results = [];
|
|
720
|
+
for (const item of items) {
|
|
721
|
+
results.push(await run(item));
|
|
722
|
+
}
|
|
723
|
+
return results;
|
|
724
|
+
}
|
|
725
|
+
async function runWithConcurrencyLimit(items, limit, run) {
|
|
726
|
+
const results = new Array(items.length);
|
|
727
|
+
let nextIndex = 0;
|
|
728
|
+
const workers = Array.from({ length: Math.min(Math.max(limit, 1), items.length) }, async () => {
|
|
729
|
+
while (true) {
|
|
730
|
+
const index = nextIndex;
|
|
731
|
+
nextIndex += 1;
|
|
732
|
+
if (index >= items.length) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
results[index] = await run(items[index]);
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
await Promise.all(workers);
|
|
739
|
+
return results;
|
|
740
|
+
}
|
|
741
|
+
function appendTextToToolResultContent(block, text) {
|
|
742
|
+
if (block.content === undefined || block.content === null) {
|
|
743
|
+
block.content = text;
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (typeof block.content === 'string') {
|
|
747
|
+
block.content = `${block.content}\n\n${text}`;
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
if (Array.isArray(block.content)) {
|
|
751
|
+
block.content.push({ type: 'text', text });
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
function buildTodoReminderText(todos) {
|
|
755
|
+
if (todos.length === 0) {
|
|
756
|
+
return [
|
|
757
|
+
'<system-reminder>',
|
|
758
|
+
'The TodoWrite tool has not been used recently. If you are working on a multi-step task, use TodoWrite to track progress and keep exactly one item in_progress.',
|
|
759
|
+
'Do not mention this reminder to the user.',
|
|
760
|
+
'</system-reminder>',
|
|
761
|
+
].join('\n');
|
|
762
|
+
}
|
|
763
|
+
return [
|
|
764
|
+
'<system-reminder>',
|
|
765
|
+
'Current todo list state (re-injected for continuity):',
|
|
766
|
+
formatActoviqTodoListLines(todos),
|
|
767
|
+
'Continue working through pending items, update statuses with TodoWrite as you progress, and do not mention this reminder to the user.',
|
|
768
|
+
'</system-reminder>',
|
|
769
|
+
].join('\n');
|
|
770
|
+
}
|
|
771
|
+
function isModelFallbackEligibleError(error) {
|
|
772
|
+
if (error instanceof ActoviqProviderApiError) {
|
|
773
|
+
const status = error.status ?? 0;
|
|
774
|
+
return status === 429 || status === 529 || (status >= 500 && status < 600);
|
|
775
|
+
}
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
const MAX_STREAM_INTERRUPTION_RETRIES = 3;
|
|
779
|
+
const TRANSPORT_ERROR_CODES = new Set([
|
|
780
|
+
'ECONNRESET',
|
|
781
|
+
'ECONNREFUSED',
|
|
782
|
+
'EPIPE',
|
|
783
|
+
'ETIMEDOUT',
|
|
784
|
+
'EAI_AGAIN',
|
|
785
|
+
]);
|
|
786
|
+
const TRANSPORT_ERROR_PATTERN = /terminated|fetch failed|socket|other side closed|premature|network|connection (?:closed|reset|error)/i;
|
|
787
|
+
/**
|
|
788
|
+
* Transport-level failures that occur after the provider accepted the request
|
|
789
|
+
* (mid-stream socket loss, abrupt connection close). HTTP-status errors are
|
|
790
|
+
* excluded — the provider client already retried those before throwing — and
|
|
791
|
+
* so are non-network errors, which must keep propagating.
|
|
792
|
+
*/
|
|
793
|
+
function isRetryableStreamInterruption(error) {
|
|
794
|
+
if (error instanceof ActoviqProviderApiError ||
|
|
795
|
+
error instanceof RunAbortedError ||
|
|
796
|
+
error instanceof ActoviqSdkError ||
|
|
797
|
+
error instanceof ToolExecutionError) {
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
if (!(error instanceof Error) || error.name === 'AbortError') {
|
|
801
|
+
return false;
|
|
802
|
+
}
|
|
803
|
+
const cause = error.cause;
|
|
804
|
+
const causeCode = typeof cause?.code === 'string' ? cause.code : '';
|
|
805
|
+
if (causeCode.startsWith('UND_ERR') || TRANSPORT_ERROR_CODES.has(causeCode)) {
|
|
806
|
+
return true;
|
|
807
|
+
}
|
|
808
|
+
const causeMessage = typeof cause?.message === 'string' ? cause.message : '';
|
|
809
|
+
return TRANSPORT_ERROR_PATTERN.test(`${error.message} ${causeMessage}`);
|
|
810
|
+
}
|
|
811
|
+
function sleep(ms) {
|
|
812
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Mark the last content block of the last message with an ephemeral
|
|
816
|
+
* cache_control breakpoint. One breakpoint caches the entire request prefix
|
|
817
|
+
* (tools + system + conversation) on Anthropic API hosts. String-content
|
|
818
|
+
* messages (e.g. the first user prompt) are converted to an equivalent single
|
|
819
|
+
* text block so the breakpoint still applies — otherwise the whole request
|
|
820
|
+
* goes uncached whenever the last message is a plain string.
|
|
821
|
+
*/
|
|
822
|
+
function applyCacheControlToLastMessage(messages) {
|
|
823
|
+
const last = messages.at(-1);
|
|
824
|
+
if (!last) {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
if (typeof last.content === 'string') {
|
|
828
|
+
if (last.content.length === 0) {
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
last.content = [
|
|
832
|
+
{ type: 'text', text: last.content, cache_control: { type: 'ephemeral' } },
|
|
833
|
+
];
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (!Array.isArray(last.content) || last.content.length === 0) {
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
const lastBlock = last.content[last.content.length - 1];
|
|
840
|
+
if (lastBlock && typeof lastBlock === 'object') {
|
|
841
|
+
lastBlock.cache_control = { type: 'ephemeral' };
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
function sanitizeArtifactSegment(value) {
|
|
845
|
+
return value.replace(/[^A-Za-z0-9_.-]+/g, '_').slice(0, 80) || 'artifact';
|
|
846
|
+
}
|
|
847
|
+
function getRequestByteLength(request) {
|
|
848
|
+
return Buffer.byteLength(JSON.stringify({
|
|
849
|
+
...request,
|
|
850
|
+
signal: undefined,
|
|
851
|
+
}), 'utf8');
|
|
852
|
+
}
|
|
393
853
|
async function consumeStream(request, modelApi, iteration, emit, runId) {
|
|
394
854
|
const stream = modelApi.streamMessage(request);
|
|
395
855
|
let textSnapshot = '';
|