openhorse 0.2.11 → 0.2.13
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/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +681 -6
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/types.d.ts +3 -1
- package/dist/commands/types.d.ts.map +1 -1
- package/dist/commands/types.js.map +1 -1
- package/dist/core/checkpoint.d.ts +1 -0
- package/dist/core/checkpoint.d.ts.map +1 -1
- package/dist/core/checkpoint.js +59 -10
- package/dist/core/checkpoint.js.map +1 -1
- package/dist/core/tool-artifacts.d.ts +10 -0
- package/dist/core/tool-artifacts.d.ts.map +1 -1
- package/dist/core/tool-artifacts.js +81 -3
- package/dist/core/tool-artifacts.js.map +1 -1
- package/dist/framework/index.d.ts +2 -2
- package/dist/framework/index.d.ts.map +1 -1
- package/dist/framework/index.js +5 -1
- package/dist/framework/index.js.map +1 -1
- package/dist/framework/query.d.ts +104 -1
- package/dist/framework/query.d.ts.map +1 -1
- package/dist/framework/query.js +454 -18
- package/dist/framework/query.js.map +1 -1
- package/dist/framework/store.d.ts +5 -0
- package/dist/framework/store.d.ts.map +1 -1
- package/dist/framework/store.js +4 -0
- package/dist/framework/store.js.map +1 -1
- package/dist/framework/tool-scheduler.d.ts +9 -0
- package/dist/framework/tool-scheduler.d.ts.map +1 -1
- package/dist/framework/tool-scheduler.js +34 -0
- package/dist/framework/tool-scheduler.js.map +1 -1
- package/dist/ink-ui/components/StatusLine.d.ts.map +1 -1
- package/dist/ink-ui/components/StatusLine.js +7 -1
- package/dist/ink-ui/components/StatusLine.js.map +1 -1
- package/dist/runtime/agent-runtime-protocol.d.ts +13 -1
- package/dist/runtime/agent-runtime-protocol.d.ts.map +1 -1
- package/dist/runtime/agent-runtime-protocol.js +24 -0
- package/dist/runtime/agent-runtime-protocol.js.map +1 -1
- package/dist/runtime/chat-controller.d.ts +3 -0
- package/dist/runtime/chat-controller.d.ts.map +1 -1
- package/dist/runtime/chat-controller.js +930 -5
- package/dist/runtime/chat-controller.js.map +1 -1
- package/dist/runtime/loop-budget.d.ts +5 -0
- package/dist/runtime/loop-budget.d.ts.map +1 -0
- package/dist/runtime/loop-budget.js +77 -0
- package/dist/runtime/loop-budget.js.map +1 -0
- package/dist/runtime/ui-events.d.ts +39 -1
- package/dist/runtime/ui-events.d.ts.map +1 -1
- package/dist/runtime/ui-events.js.map +1 -1
- package/dist/services/config-dir.d.ts +2 -0
- package/dist/services/config-dir.d.ts.map +1 -1
- package/dist/services/config-dir.js +5 -0
- package/dist/services/config-dir.js.map +1 -1
- package/dist/services/config.d.ts +4 -2
- package/dist/services/config.d.ts.map +1 -1
- package/dist/services/config.js +19 -0
- package/dist/services/config.js.map +1 -1
- package/dist/services/doctor.d.ts.map +1 -1
- package/dist/services/doctor.js +34 -4
- package/dist/services/doctor.js.map +1 -1
- package/dist/services/global-config.d.ts +17 -0
- package/dist/services/global-config.d.ts.map +1 -1
- package/dist/services/global-config.js.map +1 -1
- package/dist/services/llm.d.ts +18 -0
- package/dist/services/llm.d.ts.map +1 -1
- package/dist/services/llm.js +175 -163
- package/dist/services/llm.js.map +1 -1
- package/dist/services/provider-diagnostics.d.ts +30 -0
- package/dist/services/provider-diagnostics.d.ts.map +1 -0
- package/dist/services/provider-diagnostics.js +308 -0
- package/dist/services/provider-diagnostics.js.map +1 -0
- package/dist/services/redaction.d.ts +2 -0
- package/dist/services/redaction.d.ts.map +1 -0
- package/dist/services/redaction.js +27 -0
- package/dist/services/redaction.js.map +1 -0
- package/dist/services/session-index.d.ts.map +1 -1
- package/dist/services/session-index.js +7 -3
- package/dist/services/session-index.js.map +1 -1
- package/dist/services/session-storage.d.ts +100 -0
- package/dist/services/session-storage.d.ts.map +1 -1
- package/dist/services/session-storage.js +154 -3
- package/dist/services/session-storage.js.map +1 -1
- package/dist/services/verification-profile.d.ts +36 -0
- package/dist/services/verification-profile.d.ts.map +1 -0
- package/dist/services/verification-profile.js +239 -0
- package/dist/services/verification-profile.js.map +1 -0
- package/dist/services/workspace-state.d.ts +25 -0
- package/dist/services/workspace-state.d.ts.map +1 -0
- package/dist/services/workspace-state.js +115 -0
- package/dist/services/workspace-state.js.map +1 -0
- package/dist/skills/loader.d.ts +5 -1
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +34 -0
- package/dist/skills/loader.js.map +1 -1
- package/dist/skills/registry.d.ts +5 -1
- package/dist/skills/registry.d.ts.map +1 -1
- package/dist/skills/registry.js +23 -0
- package/dist/skills/registry.js.map +1 -1
- package/dist/skills/runtime.d.ts +6 -0
- package/dist/skills/runtime.d.ts.map +1 -1
- package/dist/skills/runtime.js +88 -4
- package/dist/skills/runtime.js.map +1 -1
- package/dist/skills/types.d.ts +19 -0
- package/dist/skills/types.d.ts.map +1 -1
- package/dist/skills/types.js.map +1 -1
- package/package.json +1 -1
package/dist/framework/query.js
CHANGED
|
@@ -10,12 +10,17 @@
|
|
|
10
10
|
* chunks directly to stdout via the callback.
|
|
11
11
|
*/
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.QueryLoopError = exports.DEFAULT_LOOP_BUDGET = exports.DEFAULT_MAX_MODEL_VISIBLE_TOOL_RESULT_BYTES = void 0;
|
|
14
|
+
exports.createLocalFastPathLoopStats = createLocalFastPathLoopStats;
|
|
15
|
+
exports.createFailedLoopStats = createFailedLoopStats;
|
|
13
16
|
exports.query = query;
|
|
14
17
|
const tool_1 = require("./tool");
|
|
15
18
|
const strategy_tracker_1 = require("../core/strategy-tracker");
|
|
16
19
|
const auto_compact_1 = require("../services/compact/auto-compact");
|
|
17
20
|
const tool_scheduler_1 = require("./tool-scheduler");
|
|
18
21
|
const token_estimate_1 = require("../utils/token-estimate");
|
|
22
|
+
const tool_serializer_1 = require("./tool-serializer");
|
|
23
|
+
exports.DEFAULT_MAX_MODEL_VISIBLE_TOOL_RESULT_BYTES = 4096;
|
|
19
24
|
function isAborted(signal) {
|
|
20
25
|
return signal?.aborted === true;
|
|
21
26
|
}
|
|
@@ -26,6 +31,14 @@ function cancelledEvent(llm) {
|
|
|
26
31
|
model: llm.getModel(),
|
|
27
32
|
};
|
|
28
33
|
}
|
|
34
|
+
function cancelledCompleteEvent(llm, stats) {
|
|
35
|
+
return {
|
|
36
|
+
type: 'complete',
|
|
37
|
+
content: 'Operation cancelled.',
|
|
38
|
+
model: llm.getModel(),
|
|
39
|
+
stats: cloneLoopStats(stats, 'cancelled'),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
29
42
|
function isRecord(value) {
|
|
30
43
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
31
44
|
}
|
|
@@ -65,6 +78,326 @@ function parseBatchReadEvidenceSteps(result) {
|
|
|
65
78
|
}];
|
|
66
79
|
});
|
|
67
80
|
}
|
|
81
|
+
exports.DEFAULT_LOOP_BUDGET = {
|
|
82
|
+
maxLlmRequestsPerUserTurn: 24,
|
|
83
|
+
maxToolCallsPerUserTurn: 120,
|
|
84
|
+
maxReadOnlyFragmentation: 3,
|
|
85
|
+
maxModelVisibleToolBytes: 64 * 1024,
|
|
86
|
+
profile: 'default',
|
|
87
|
+
baseProfile: 'default',
|
|
88
|
+
configOverride: false,
|
|
89
|
+
};
|
|
90
|
+
const COMPLEX_LOOP_BUDGET = {
|
|
91
|
+
maxLlmRequestsPerUserTurn: 48,
|
|
92
|
+
maxToolCallsPerUserTurn: 180,
|
|
93
|
+
maxModelVisibleToolBytes: 96 * 1024,
|
|
94
|
+
};
|
|
95
|
+
class QueryLoopError extends Error {
|
|
96
|
+
constructor(error, stats) {
|
|
97
|
+
super(error instanceof Error ? error.message : String(error));
|
|
98
|
+
this.name = 'QueryLoopError';
|
|
99
|
+
this.originalError = error;
|
|
100
|
+
this.stats = stats;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
exports.QueryLoopError = QueryLoopError;
|
|
104
|
+
function createLoopStats() {
|
|
105
|
+
return {
|
|
106
|
+
turnsStarted: 0,
|
|
107
|
+
llmRequests: 0,
|
|
108
|
+
toolCalls: 0,
|
|
109
|
+
readOnlyToolCalls: 0,
|
|
110
|
+
unsafeToolCalls: 0,
|
|
111
|
+
toolResultBytes: 0,
|
|
112
|
+
modelVisibleToolBytes: 0,
|
|
113
|
+
summarizedBytes: 0,
|
|
114
|
+
finishReason: 'running',
|
|
115
|
+
providerRetryCount: 0,
|
|
116
|
+
providerRetryDelayMs: 0,
|
|
117
|
+
providerRetryErrorTypes: [],
|
|
118
|
+
providerFallbackCount: 0,
|
|
119
|
+
providerUsingFallback: false,
|
|
120
|
+
singleReadOnlyStreak: 0,
|
|
121
|
+
batchReadSuggestionCount: 0,
|
|
122
|
+
localFastPathUsed: false,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function cloneLoopStats(stats, finishReason) {
|
|
126
|
+
return {
|
|
127
|
+
...stats,
|
|
128
|
+
finishReason: finishReason ?? stats.finishReason,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function byteLength(text) {
|
|
132
|
+
return Buffer.byteLength(text, 'utf8');
|
|
133
|
+
}
|
|
134
|
+
function createLocalFastPathLoopStats(overrides = {}) {
|
|
135
|
+
return {
|
|
136
|
+
...createLoopStats(),
|
|
137
|
+
turnsStarted: 1,
|
|
138
|
+
finishReason: 'completed',
|
|
139
|
+
localFastPathUsed: true,
|
|
140
|
+
...overrides,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function createFailedLoopStats(options = {}) {
|
|
144
|
+
const { loopBudget, diagnostics, ...overrides } = options;
|
|
145
|
+
const stats = {
|
|
146
|
+
...createLoopStats(),
|
|
147
|
+
turnsStarted: 1,
|
|
148
|
+
llmRequests: 1,
|
|
149
|
+
finishReason: 'failed',
|
|
150
|
+
...overrides,
|
|
151
|
+
};
|
|
152
|
+
applyLoopBudgetStats(stats, resolveLoopBudget(loopBudget));
|
|
153
|
+
applyLlmRequestDiagnostics(stats, diagnostics);
|
|
154
|
+
return stats;
|
|
155
|
+
}
|
|
156
|
+
function isLoopBudgetSource(value) {
|
|
157
|
+
return value === 'default' || value === 'complex' || value === 'release' || value === 'config';
|
|
158
|
+
}
|
|
159
|
+
function isLoopBudgetBaseProfile(value) {
|
|
160
|
+
return value === 'default' || value === 'complex' || value === 'release';
|
|
161
|
+
}
|
|
162
|
+
function resolveLoopBudget(partial) {
|
|
163
|
+
const numericOverrides = Object.fromEntries(Object.entries(partial ?? {}).filter(([, value]) => typeof value === 'number' && Number.isFinite(value) && value > 0));
|
|
164
|
+
const hasNumericOverrides = Object.keys(numericOverrides).length > 0;
|
|
165
|
+
const hasExplicitProfile = partial?.profile !== undefined || partial?.baseProfile !== undefined;
|
|
166
|
+
const inferredConfigOverride = hasNumericOverrides && !hasExplicitProfile;
|
|
167
|
+
const configOverride = partial?.configOverride === true || inferredConfigOverride;
|
|
168
|
+
return {
|
|
169
|
+
...exports.DEFAULT_LOOP_BUDGET,
|
|
170
|
+
...numericOverrides,
|
|
171
|
+
profile: isLoopBudgetSource(partial?.profile)
|
|
172
|
+
? partial.profile
|
|
173
|
+
: configOverride
|
|
174
|
+
? 'config'
|
|
175
|
+
: exports.DEFAULT_LOOP_BUDGET.profile,
|
|
176
|
+
baseProfile: isLoopBudgetBaseProfile(partial?.baseProfile) ? partial.baseProfile : exports.DEFAULT_LOOP_BUDGET.baseProfile,
|
|
177
|
+
configOverride,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function applyLoopBudgetStats(stats, budget) {
|
|
181
|
+
stats.loopBudgetSource = budget.profile ?? 'default';
|
|
182
|
+
stats.loopBudgetBaseProfile = budget.baseProfile ?? (budget.profile === 'config' ? 'default' : budget.profile ?? 'default');
|
|
183
|
+
stats.loopBudgetMaxLlmRequests = budget.maxLlmRequestsPerUserTurn;
|
|
184
|
+
stats.loopBudgetMaxToolCalls = budget.maxToolCallsPerUserTurn;
|
|
185
|
+
stats.loopBudgetMaxReadOnlyFragmentation = budget.maxReadOnlyFragmentation;
|
|
186
|
+
stats.loopBudgetMaxModelVisibleBytes = budget.maxModelVisibleToolBytes;
|
|
187
|
+
stats.loopBudgetConfigOverride = budget.configOverride === true;
|
|
188
|
+
}
|
|
189
|
+
function shouldPromoteDefaultBudget(stats, budget) {
|
|
190
|
+
if (budget.configOverride === true || budget.profile !== 'default')
|
|
191
|
+
return false;
|
|
192
|
+
if (stats.llmRequests < budget.maxLlmRequestsPerUserTurn)
|
|
193
|
+
return false;
|
|
194
|
+
return stats.toolCalls >= 8
|
|
195
|
+
|| stats.readOnlyToolCalls >= 6
|
|
196
|
+
|| stats.modelVisibleToolBytes >= 16 * 1024;
|
|
197
|
+
}
|
|
198
|
+
function promoteDefaultBudgetToComplex(budget) {
|
|
199
|
+
return {
|
|
200
|
+
...budget,
|
|
201
|
+
maxLlmRequestsPerUserTurn: Math.max(budget.maxLlmRequestsPerUserTurn, COMPLEX_LOOP_BUDGET.maxLlmRequestsPerUserTurn),
|
|
202
|
+
maxToolCallsPerUserTurn: Math.max(budget.maxToolCallsPerUserTurn, COMPLEX_LOOP_BUDGET.maxToolCallsPerUserTurn),
|
|
203
|
+
maxModelVisibleToolBytes: Math.max(budget.maxModelVisibleToolBytes, COMPLEX_LOOP_BUDGET.maxModelVisibleToolBytes),
|
|
204
|
+
profile: 'complex',
|
|
205
|
+
baseProfile: 'complex',
|
|
206
|
+
configOverride: false,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function getLlmRequestDiagnostics(llm) {
|
|
210
|
+
const diagnosticsReader = llm.getLastRequestDiagnostics;
|
|
211
|
+
return typeof diagnosticsReader === 'function' ? diagnosticsReader.call(llm) : undefined;
|
|
212
|
+
}
|
|
213
|
+
function applyLlmRequestDiagnostics(stats, diagnostics) {
|
|
214
|
+
if (!diagnostics)
|
|
215
|
+
return;
|
|
216
|
+
const existingTypes = stats.providerRetryErrorTypes ?? [];
|
|
217
|
+
const retryTypes = diagnostics.retryErrorTypes.filter((type, index, values) => !existingTypes.includes(type) && values.indexOf(type) === index);
|
|
218
|
+
stats.providerRetryCount = (stats.providerRetryCount ?? 0) + diagnostics.retryCount;
|
|
219
|
+
stats.providerRetryDelayMs = (stats.providerRetryDelayMs ?? 0) + diagnostics.retryDelayMs;
|
|
220
|
+
stats.providerRetryErrorTypes = [...existingTypes, ...retryTypes];
|
|
221
|
+
stats.providerLastRetryErrorType = diagnostics.lastRetryErrorType ?? stats.providerLastRetryErrorType;
|
|
222
|
+
stats.providerLastRetryStatus = diagnostics.lastRetryStatus ?? stats.providerLastRetryStatus;
|
|
223
|
+
stats.providerFinalModel = diagnostics.finalModel;
|
|
224
|
+
stats.providerUsingFallback = diagnostics.usingFallback;
|
|
225
|
+
if (diagnostics.fallbackTriggered) {
|
|
226
|
+
stats.providerFallbackCount = (stats.providerFallbackCount ?? 0) + 1;
|
|
227
|
+
stats.providerFallbackFromModel = diagnostics.fallbackFromModel;
|
|
228
|
+
stats.providerFallbackToModel = diagnostics.fallbackToModel;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function budgetExceededEvent(llm, stats, reason) {
|
|
232
|
+
const continuationActions = [
|
|
233
|
+
'reply_continue',
|
|
234
|
+
'narrow_instruction',
|
|
235
|
+
'inspect_loop_stats',
|
|
236
|
+
'raise_budget',
|
|
237
|
+
];
|
|
238
|
+
const continuationHint = 'Reply `继续` to continue the same objective, give a narrower next step, inspect /loop-stats, or raise agentLoop.budget for intentional long work.';
|
|
239
|
+
return {
|
|
240
|
+
type: 'complete',
|
|
241
|
+
content: [
|
|
242
|
+
`Agent loop budget reached: ${reason}.`,
|
|
243
|
+
'I stopped this turn to avoid unnecessary model requests and preserved the current session state.',
|
|
244
|
+
'To continue the same objective, reply `继续` or provide the next concrete step.',
|
|
245
|
+
'Use /loop-stats to inspect request/tool counts. For intentional long work, raise agentLoop.budget in openhorse.json.',
|
|
246
|
+
].join('\n'),
|
|
247
|
+
model: llm.getModel(),
|
|
248
|
+
stats: cloneLoopStats({
|
|
249
|
+
...stats,
|
|
250
|
+
budgetExceededReason: reason,
|
|
251
|
+
continuationActions,
|
|
252
|
+
continuationHint,
|
|
253
|
+
}, 'budget_exceeded'),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function permissionBlockedEvent(llm, stats, toolName, error, source) {
|
|
257
|
+
const detail = error ? ` ${error}` : '';
|
|
258
|
+
const sourceText = source ? ` (${source})` : '';
|
|
259
|
+
return {
|
|
260
|
+
type: 'complete',
|
|
261
|
+
content: `Tool ${toolName} was not executed because permission was denied${sourceText}.${detail}`.trim(),
|
|
262
|
+
model: llm.getModel(),
|
|
263
|
+
stats: cloneLoopStats(stats, 'blocked'),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function compactPromptEvidence(items, maxItems = 12) {
|
|
267
|
+
return items.slice(0, maxItems).map(item => `${item.id}:${item.kind}:score=${item.score}:tokens=${item.tokens}`);
|
|
268
|
+
}
|
|
269
|
+
function takeUtf8Prefix(text, maxBytes) {
|
|
270
|
+
if (maxBytes <= 0)
|
|
271
|
+
return '';
|
|
272
|
+
let result = '';
|
|
273
|
+
let bytes = 0;
|
|
274
|
+
for (const char of text) {
|
|
275
|
+
const nextBytes = byteLength(char);
|
|
276
|
+
if (bytes + nextBytes > maxBytes)
|
|
277
|
+
break;
|
|
278
|
+
result += char;
|
|
279
|
+
bytes += nextBytes;
|
|
280
|
+
}
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
function takeUtf8Suffix(text, maxBytes) {
|
|
284
|
+
if (maxBytes <= 0)
|
|
285
|
+
return '';
|
|
286
|
+
let result = '';
|
|
287
|
+
let bytes = 0;
|
|
288
|
+
const chars = Array.from(text);
|
|
289
|
+
for (let index = chars.length - 1; index >= 0; index--) {
|
|
290
|
+
const char = chars[index];
|
|
291
|
+
const nextBytes = byteLength(char);
|
|
292
|
+
if (bytes + nextBytes > maxBytes)
|
|
293
|
+
break;
|
|
294
|
+
result = char + result;
|
|
295
|
+
bytes += nextBytes;
|
|
296
|
+
}
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
function truncateForModel(text, maxBytes) {
|
|
300
|
+
if (byteLength(text) <= maxBytes)
|
|
301
|
+
return text;
|
|
302
|
+
const marker = '\n...[truncated]';
|
|
303
|
+
if (maxBytes <= 128) {
|
|
304
|
+
return `${takeUtf8Prefix(text, Math.max(0, maxBytes - byteLength(marker)))}${marker}`;
|
|
305
|
+
}
|
|
306
|
+
const middle = `\n...[truncated ${byteLength(text)}B output for model context]...\n`;
|
|
307
|
+
const contentBudget = Math.max(0, maxBytes - byteLength(middle));
|
|
308
|
+
const headBytes = Math.floor(contentBudget * 0.65);
|
|
309
|
+
const tailBytes = contentBudget - headBytes;
|
|
310
|
+
return [
|
|
311
|
+
takeUtf8Prefix(text, headBytes),
|
|
312
|
+
middle,
|
|
313
|
+
takeUtf8Suffix(text, tailBytes),
|
|
314
|
+
].join('');
|
|
315
|
+
}
|
|
316
|
+
function summarizeModelVisibleToolResult(executed, maxBytes) {
|
|
317
|
+
const rawBytes = byteLength(executed.result);
|
|
318
|
+
const fullOutputBytes = executed.outputBytes ?? rawBytes;
|
|
319
|
+
if (rawBytes <= maxBytes && !executed.artifactRef) {
|
|
320
|
+
return { result: executed.result, bytes: rawBytes, summarizedBytes: 0 };
|
|
321
|
+
}
|
|
322
|
+
const envelope = (0, tool_serializer_1.parseToolResultEnvelope)(executed.result);
|
|
323
|
+
const output = typeof envelope.output === 'string' ? envelope.output : executed.result;
|
|
324
|
+
const summary = executed.summary || envelope.summary;
|
|
325
|
+
const compactSummary = summary ? truncateForModel(summary, 192) : undefined;
|
|
326
|
+
const rawError = executed.error ?? envelope.error;
|
|
327
|
+
const compactError = rawError ? truncateForModel(rawError, 192) : undefined;
|
|
328
|
+
const artifactText = executed.artifactRef
|
|
329
|
+
? ` Full output is available as artifact ${executed.artifactRef.id} (${executed.artifactRef.outputBytes}B).`
|
|
330
|
+
: '';
|
|
331
|
+
const serializeCompact = (compactOutput) => (0, tool_serializer_1.serializeToolResult)({
|
|
332
|
+
success: executed.success,
|
|
333
|
+
output: compactOutput,
|
|
334
|
+
error: compactError,
|
|
335
|
+
summary: compactSummary,
|
|
336
|
+
outputBytes: fullOutputBytes,
|
|
337
|
+
artifactRef: executed.artifactRef ?? envelope.artifactRef,
|
|
338
|
+
metadata: {
|
|
339
|
+
...(envelope.metadata ?? {}),
|
|
340
|
+
modelVisibleCompressed: true,
|
|
341
|
+
originalResultBytes: rawBytes,
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
let outputBudget = Math.max(128, maxBytes - 768);
|
|
345
|
+
let compact = '';
|
|
346
|
+
for (let attempt = 0; attempt < 6; attempt++) {
|
|
347
|
+
compact = serializeCompact([
|
|
348
|
+
compactSummary ? `Summary: ${compactSummary}` : 'Tool output was summarized for model context.',
|
|
349
|
+
artifactText.trim(),
|
|
350
|
+
truncateForModel(output, outputBudget),
|
|
351
|
+
].filter(Boolean).join('\n'));
|
|
352
|
+
const compactBytes = byteLength(compact);
|
|
353
|
+
if (compactBytes <= maxBytes || outputBudget <= 128)
|
|
354
|
+
break;
|
|
355
|
+
outputBudget = Math.max(128, outputBudget - Math.max(compactBytes - maxBytes, 128));
|
|
356
|
+
}
|
|
357
|
+
if (byteLength(compact) > maxBytes) {
|
|
358
|
+
compact = serializeCompact([
|
|
359
|
+
compactSummary ? `Summary: ${compactSummary}` : 'Tool output was summarized for model context.',
|
|
360
|
+
artifactText.trim(),
|
|
361
|
+
`Output body omitted from model context (${fullOutputBytes}B total).`,
|
|
362
|
+
].filter(Boolean).join('\n'));
|
|
363
|
+
}
|
|
364
|
+
if (byteLength(compact) > maxBytes) {
|
|
365
|
+
compact = (0, tool_serializer_1.serializeToolResult)({
|
|
366
|
+
success: executed.success,
|
|
367
|
+
output: `Tool result omitted from model context (${fullOutputBytes}B total).`,
|
|
368
|
+
outputBytes: fullOutputBytes,
|
|
369
|
+
metadata: {
|
|
370
|
+
modelVisibleCompressed: true,
|
|
371
|
+
originalResultBytes: rawBytes,
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
const bytes = byteLength(compact);
|
|
376
|
+
return {
|
|
377
|
+
result: compact,
|
|
378
|
+
bytes,
|
|
379
|
+
summarizedBytes: Math.max(0, fullOutputBytes - bytes),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
function omitModelVisibleToolResult(executed, maxAggregateBytes) {
|
|
383
|
+
const fullBytes = executed.outputBytes ?? byteLength(executed.result);
|
|
384
|
+
const compact = (0, tool_serializer_1.serializeToolResult)({
|
|
385
|
+
success: executed.success,
|
|
386
|
+
output: `Tool result omitted from model context because the per-turn model-visible tool budget (${maxAggregateBytes}B) was reached.`,
|
|
387
|
+
outputBytes: fullBytes,
|
|
388
|
+
artifactRef: executed.artifactRef,
|
|
389
|
+
metadata: {
|
|
390
|
+
modelVisibleCompressed: true,
|
|
391
|
+
modelVisibleBudgetExceeded: true,
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
const bytes = byteLength(compact);
|
|
395
|
+
return {
|
|
396
|
+
result: compact,
|
|
397
|
+
bytes,
|
|
398
|
+
summarizedBytes: Math.max(0, fullBytes - bytes),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
68
401
|
// ============================================================================
|
|
69
402
|
// query() — async generator
|
|
70
403
|
// ============================================================================
|
|
@@ -89,25 +422,40 @@ async function* query(params) {
|
|
|
89
422
|
harness, input, maxParallelToolCalls = 6, } = params;
|
|
90
423
|
const openaiTools = (0, tool_1.toOpenAITools)(tools);
|
|
91
424
|
let turn = 0;
|
|
425
|
+
const stats = createLoopStats();
|
|
426
|
+
let loopBudget = resolveLoopBudget(params.loopBudget);
|
|
427
|
+
applyLoopBudgetStats(stats, loopBudget);
|
|
428
|
+
const maxModelVisibleToolResultBytes = Math.max(512, params.maxModelVisibleToolResultBytes ?? exports.DEFAULT_MAX_MODEL_VISIBLE_TOOL_RESULT_BYTES);
|
|
429
|
+
const fragmentedReadOnlyTools = new Set(['read_file', 'glob', 'grep', 'list_files', 'git_status']);
|
|
92
430
|
// 无限循环,依赖安全机制停止
|
|
93
431
|
while (true) {
|
|
94
432
|
turn++;
|
|
95
433
|
// Check abort
|
|
96
434
|
if (isAborted(abortSignal)) {
|
|
97
|
-
yield
|
|
435
|
+
yield cancelledCompleteEvent(llm, stats);
|
|
98
436
|
return;
|
|
99
437
|
}
|
|
100
|
-
// Request start
|
|
101
|
-
yield { type: 'request_start', model: llm.getModel(), turn };
|
|
102
438
|
// Safety valve: check maxTurns if specified (optional)
|
|
103
439
|
if (maxTurns && turn > maxTurns) {
|
|
104
440
|
yield {
|
|
105
441
|
type: 'complete',
|
|
106
442
|
content: `Reached maximum turns (${maxTurns}). Task may be incomplete.`,
|
|
107
443
|
model: llm.getModel(),
|
|
444
|
+
stats: cloneLoopStats(stats, 'max_turns'),
|
|
108
445
|
};
|
|
109
446
|
return;
|
|
110
447
|
}
|
|
448
|
+
if (shouldPromoteDefaultBudget(stats, loopBudget)) {
|
|
449
|
+
loopBudget = promoteDefaultBudgetToComplex(loopBudget);
|
|
450
|
+
applyLoopBudgetStats(stats, loopBudget);
|
|
451
|
+
}
|
|
452
|
+
if (stats.llmRequests >= loopBudget.maxLlmRequestsPerUserTurn) {
|
|
453
|
+
yield budgetExceededEvent(llm, stats, `LLM request budget ${loopBudget.maxLlmRequestsPerUserTurn} reached`);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
// Request start
|
|
457
|
+
yield { type: 'request_start', model: llm.getModel(), turn };
|
|
458
|
+
stats.turnsStarted = turn;
|
|
111
459
|
const autoCompact = (0, auto_compact_1.getAutoCompact)({
|
|
112
460
|
modelId: llm.getModel(),
|
|
113
461
|
getContextCapsule: harness ? () => harness.getCapsule() : undefined,
|
|
@@ -122,34 +470,64 @@ async function* query(params) {
|
|
|
122
470
|
const predictedTokens = (0, token_estimate_1.estimateMessagesTokens)(requestMessages);
|
|
123
471
|
const preCompacted = await autoCompact.checkPredictiveAndCompact(messages, predictedTokens);
|
|
124
472
|
if (preCompacted !== messages) {
|
|
473
|
+
stats.compactTrigger = 'pre_turn';
|
|
125
474
|
messages.length = 0;
|
|
126
475
|
messages.push(...preCompacted);
|
|
127
476
|
requestMessages = harness
|
|
128
477
|
? harness.assembleMessages(messages, { input, tools: tools.map(tool => ({ name: tool.name, description: tool.description })) })
|
|
129
478
|
: messages;
|
|
130
479
|
}
|
|
480
|
+
const assemblyStats = harness?.toJSON().promptAssemblyStats;
|
|
481
|
+
if (assemblyStats) {
|
|
482
|
+
yield {
|
|
483
|
+
type: 'prompt_assembly',
|
|
484
|
+
modelId: assemblyStats.modelId,
|
|
485
|
+
estimatedTokens: assemblyStats.estimatedTokens,
|
|
486
|
+
budgetTokens: assemblyStats.budgetTokens,
|
|
487
|
+
coreTokens: assemblyStats.coreTokens,
|
|
488
|
+
evidenceBudgetTokens: assemblyStats.evidenceBudgetTokens,
|
|
489
|
+
recentTurnBudgetTokens: assemblyStats.recentTurnBudgetTokens,
|
|
490
|
+
sections: assemblyStats.sections.slice(0, 12),
|
|
491
|
+
includedEvidence: compactPromptEvidence(assemblyStats.includedEvidence),
|
|
492
|
+
omittedEvidence: compactPromptEvidence(assemblyStats.omittedEvidence, 8),
|
|
493
|
+
includedEvidenceCount: assemblyStats.includedEvidence.length,
|
|
494
|
+
omittedEvidenceCount: assemblyStats.omittedEvidence.length,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
131
497
|
if (isAborted(abortSignal)) {
|
|
132
|
-
yield
|
|
498
|
+
yield cancelledCompleteEvent(llm, stats);
|
|
133
499
|
return;
|
|
134
500
|
}
|
|
135
|
-
|
|
501
|
+
let response;
|
|
502
|
+
try {
|
|
503
|
+
stats.llmRequests++;
|
|
504
|
+
response = await llm.chatStream(requestMessages, streamCallbacks, openaiTools, { abortSignal });
|
|
505
|
+
applyLlmRequestDiagnostics(stats, getLlmRequestDiagnostics(llm));
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
applyLlmRequestDiagnostics(stats, getLlmRequestDiagnostics(llm));
|
|
509
|
+
throw new QueryLoopError(error, cloneLoopStats(stats, 'failed'));
|
|
510
|
+
}
|
|
136
511
|
if (isAborted(abortSignal)) {
|
|
137
|
-
yield
|
|
512
|
+
yield cancelledCompleteEvent(llm, stats);
|
|
138
513
|
return;
|
|
139
514
|
}
|
|
140
|
-
// Save assistant message to history
|
|
141
|
-
const assistantMsg = {
|
|
142
|
-
role: 'assistant',
|
|
143
|
-
content: response.content,
|
|
144
|
-
};
|
|
145
|
-
if (response.toolCalls) {
|
|
146
|
-
assistantMsg.tool_calls = response.toolCalls;
|
|
147
|
-
}
|
|
148
|
-
messages.push(assistantMsg);
|
|
149
|
-
harness?.recordAssistantResponse(response);
|
|
150
515
|
// Handle tool calls
|
|
151
516
|
if (response.toolCalls && response.toolCalls.length > 0) {
|
|
152
517
|
const toolCalls = response.toolCalls;
|
|
518
|
+
if (stats.toolCalls + toolCalls.length > loopBudget.maxToolCallsPerUserTurn) {
|
|
519
|
+
yield budgetExceededEvent(llm, stats, `tool call budget ${loopBudget.maxToolCallsPerUserTurn} would be exceeded by ${toolCalls.length} requested tools`);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
// Save assistant tool-call message only after budget checks, so future
|
|
523
|
+
// requests never inherit unresolved tool calls if this turn is stopped.
|
|
524
|
+
const assistantMsg = {
|
|
525
|
+
role: 'assistant',
|
|
526
|
+
content: response.content,
|
|
527
|
+
tool_calls: toolCalls,
|
|
528
|
+
};
|
|
529
|
+
messages.push(assistantMsg);
|
|
530
|
+
harness?.recordAssistantResponse(response);
|
|
153
531
|
yield {
|
|
154
532
|
type: 'assistant_tool_calls',
|
|
155
533
|
content: response.content,
|
|
@@ -169,6 +547,21 @@ async function* query(params) {
|
|
|
169
547
|
harnessDriftCheck: harness ? ({ name, args }) => harness.beforeToolUse({ name, args }) : undefined,
|
|
170
548
|
harnessBlockedResult: harness ? (drift) => harness.asToolBlockedResult(drift) : undefined,
|
|
171
549
|
});
|
|
550
|
+
stats.toolCalls += preparedCalls.length;
|
|
551
|
+
for (const prepared of preparedCalls) {
|
|
552
|
+
if (prepared.tool?.isReadOnly?.(prepared.args) === true) {
|
|
553
|
+
stats.readOnlyToolCalls++;
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
stats.unsafeToolCalls++;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const singleFragmentedReadOnlyCall = preparedCalls.length === 1
|
|
560
|
+
&& preparedCalls[0].tool?.isReadOnly?.(preparedCalls[0].args) === true
|
|
561
|
+
&& fragmentedReadOnlyTools.has(preparedCalls[0].tc.function.name);
|
|
562
|
+
stats.singleReadOnlyStreak = singleFragmentedReadOnlyCall
|
|
563
|
+
? stats.singleReadOnlyStreak + 1
|
|
564
|
+
: 0;
|
|
172
565
|
for (const prepared of preparedCalls) {
|
|
173
566
|
yield {
|
|
174
567
|
type: 'tool_call',
|
|
@@ -189,11 +582,18 @@ async function* query(params) {
|
|
|
189
582
|
maxParallelToolCalls,
|
|
190
583
|
})) {
|
|
191
584
|
if (isAborted(abortSignal)) {
|
|
192
|
-
yield
|
|
585
|
+
yield cancelledCompleteEvent(llm, stats);
|
|
193
586
|
return;
|
|
194
587
|
}
|
|
195
588
|
const { prepared } = executed;
|
|
196
589
|
const { tc, args, attemptId } = prepared;
|
|
590
|
+
const remainingModelVisibleBytes = loopBudget.maxModelVisibleToolBytes - stats.modelVisibleToolBytes;
|
|
591
|
+
const modelVisible = remainingModelVisibleBytes < 512
|
|
592
|
+
? omitModelVisibleToolResult(executed, loopBudget.maxModelVisibleToolBytes)
|
|
593
|
+
: summarizeModelVisibleToolResult(executed, Math.min(maxModelVisibleToolResultBytes, remainingModelVisibleBytes));
|
|
594
|
+
stats.toolResultBytes += executed.outputBytes ?? byteLength(executed.result);
|
|
595
|
+
stats.modelVisibleToolBytes += modelVisible.bytes;
|
|
596
|
+
stats.summarizedBytes += modelVisible.summarizedBytes;
|
|
197
597
|
strategyTracker.recordResult(attemptId, executed.strategyResult, executed.strategyError, executed.duration);
|
|
198
598
|
harness?.recordToolResult({
|
|
199
599
|
name: tc.function.name,
|
|
@@ -222,12 +622,24 @@ async function* query(params) {
|
|
|
222
622
|
});
|
|
223
623
|
}
|
|
224
624
|
}
|
|
625
|
+
if (executed.permissionDecision) {
|
|
626
|
+
yield {
|
|
627
|
+
type: 'permission_decision',
|
|
628
|
+
name: tc.function.name,
|
|
629
|
+
args,
|
|
630
|
+
callId: tc.id,
|
|
631
|
+
decision: executed.permissionDecision,
|
|
632
|
+
batchCount: toolCalls.length,
|
|
633
|
+
batchIndex: prepared.index,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
225
636
|
yield {
|
|
226
637
|
type: 'tool_result',
|
|
227
638
|
name: tc.function.name,
|
|
228
639
|
args,
|
|
229
640
|
callId: tc.id,
|
|
230
641
|
result: executed.result,
|
|
642
|
+
modelVisibleResult: modelVisible.result,
|
|
231
643
|
duration: executed.duration,
|
|
232
644
|
success: executed.success,
|
|
233
645
|
artifactRef: executed.artifactRef,
|
|
@@ -239,9 +651,13 @@ async function* query(params) {
|
|
|
239
651
|
};
|
|
240
652
|
messages.push({
|
|
241
653
|
role: 'tool',
|
|
242
|
-
content:
|
|
654
|
+
content: modelVisible.result,
|
|
243
655
|
tool_call_id: tc.id,
|
|
244
656
|
});
|
|
657
|
+
if (executed.permissionDecision?.approved === false) {
|
|
658
|
+
yield permissionBlockedEvent(llm, stats, tc.function.name, executed.error, executed.permissionDecision.source);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
245
661
|
if (strategyTracker.isExhausted()) {
|
|
246
662
|
const suggestion = strategyTracker.suggestAlternative();
|
|
247
663
|
if (suggestion) {
|
|
@@ -254,13 +670,31 @@ async function* query(params) {
|
|
|
254
670
|
}
|
|
255
671
|
}
|
|
256
672
|
}
|
|
673
|
+
if (stats.singleReadOnlyStreak >= loopBudget.maxReadOnlyFragmentation) {
|
|
674
|
+
messages.push({
|
|
675
|
+
role: 'system',
|
|
676
|
+
content: [
|
|
677
|
+
'[OpenHorse loop hint]',
|
|
678
|
+
`You have made ${stats.singleReadOnlyStreak} consecutive turns with a single read-only local tool call.`,
|
|
679
|
+
'For independent local exploration, prefer batch_read with up to 8 steps using git_status, list_files, glob, grep, and read_file.',
|
|
680
|
+
].join(' '),
|
|
681
|
+
});
|
|
682
|
+
stats.batchReadSuggestionCount++;
|
|
683
|
+
}
|
|
257
684
|
// Continue to next turn
|
|
258
685
|
continue;
|
|
259
686
|
}
|
|
687
|
+
const assistantMsg = {
|
|
688
|
+
role: 'assistant',
|
|
689
|
+
content: response.content,
|
|
690
|
+
};
|
|
691
|
+
messages.push(assistantMsg);
|
|
692
|
+
harness?.recordAssistantResponse(response);
|
|
260
693
|
// No tool calls — done, unless the harness requires one more pass.
|
|
261
694
|
const completionGate = harness?.beforeComplete();
|
|
262
695
|
if (completionGate && !completionGate.canComplete) {
|
|
263
696
|
messages.push(harness.asCompletionBlockedMessage(completionGate));
|
|
697
|
+
stats.finishReason = 'completion_gate';
|
|
264
698
|
continue;
|
|
265
699
|
}
|
|
266
700
|
yield { type: 'message', role: 'assistant', content: response.content };
|
|
@@ -278,6 +712,7 @@ async function* query(params) {
|
|
|
278
712
|
});
|
|
279
713
|
const compacted = await autoCompact.checkAndCompact(messages, totalTokens);
|
|
280
714
|
if (compacted !== messages) {
|
|
715
|
+
stats.compactTrigger = stats.compactTrigger ?? 'post_turn';
|
|
281
716
|
messages.length = 0;
|
|
282
717
|
messages.push(...compacted);
|
|
283
718
|
}
|
|
@@ -286,6 +721,7 @@ async function* query(params) {
|
|
|
286
721
|
content: response.content,
|
|
287
722
|
usage: response.usage,
|
|
288
723
|
model: response.model,
|
|
724
|
+
stats: cloneLoopStats(stats, 'completed'),
|
|
289
725
|
};
|
|
290
726
|
return;
|
|
291
727
|
}
|