@kolisachint/hoocode-agent 0.4.69 → 0.4.71
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/CHANGELOG.md +4 -0
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +10 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +1 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +1 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +16 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +210 -20
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/model-registry.d.ts +6 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +33 -0
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/routing/local-inference.d.ts +99 -0
- package/dist/core/routing/local-inference.d.ts.map +1 -0
- package/dist/core/routing/local-inference.js +118 -0
- package/dist/core/routing/local-inference.js.map +1 -0
- package/dist/core/routing/metrics.d.ts +27 -0
- package/dist/core/routing/metrics.d.ts.map +1 -0
- package/dist/core/routing/metrics.js +35 -0
- package/dist/core/routing/metrics.js.map +1 -0
- package/dist/core/routing/mlx-server.d.ts +43 -0
- package/dist/core/routing/mlx-server.d.ts.map +1 -0
- package/dist/core/routing/mlx-server.js +115 -0
- package/dist/core/routing/mlx-server.js.map +1 -0
- package/dist/core/routing/tool-result-prompts.d.ts +25 -0
- package/dist/core/routing/tool-result-prompts.d.ts.map +1 -0
- package/dist/core/routing/tool-result-prompts.js +49 -0
- package/dist/core/routing/tool-result-prompts.js.map +1 -0
- package/dist/core/sdk.d.ts +6 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +1 -0
- package/dist/core/sdk.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +1 -0
- package/dist/main.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { basename, dirname, resolve } from "node:path";
|
|
17
|
-
import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, modelsAreEqual, resetApiProviders, } from "@kolisachint/hoocode-ai";
|
|
17
|
+
import { clampThinkingLevel, cleanupSessionResources, completeSimple, getSupportedThinkingLevels, isContextOverflow, modelsAreEqual, resetApiProviders, } from "@kolisachint/hoocode-ai";
|
|
18
18
|
import { theme } from "../modes/interactive/theme/theme.js";
|
|
19
19
|
import { stripFrontmatter } from "../utils/frontmatter.js";
|
|
20
20
|
import { sleep } from "../utils/sleep.js";
|
|
@@ -29,6 +29,10 @@ import { ExtensionRunner, wrapRegisteredTools, } from "./extensions/index.js";
|
|
|
29
29
|
import { emitSessionShutdownEvent } from "./extensions/runner.js";
|
|
30
30
|
import { expandPromptTemplate, tryExpandPromptTemplate } from "./prompt-templates.js";
|
|
31
31
|
import { clearProviderExhaustion, isProviderQuotaError, markProviderExhausted } from "./provider-health.js";
|
|
32
|
+
import { LocalInferenceRouter, resolveRoutingMode } from "./routing/local-inference.js";
|
|
33
|
+
import { logLocalInferenceFallback, logRoutingMetrics } from "./routing/metrics.js";
|
|
34
|
+
import { MlxServerManager } from "./routing/mlx-server.js";
|
|
35
|
+
import { buildToolResultPrompt, stripThinkTags, TOOL_RESULT_SYSTEM_PROMPT } from "./routing/tool-result-prompts.js";
|
|
32
36
|
import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-manager.js";
|
|
33
37
|
import { createSyntheticSourceInfo } from "./source-info.js";
|
|
34
38
|
import { updateSubagentSkillPaths } from "./subagent-pool-instance.js";
|
|
@@ -108,6 +112,12 @@ export class AgentSession {
|
|
|
108
112
|
_extensionErrorUnsubscriber;
|
|
109
113
|
// Model registry for API key resolution
|
|
110
114
|
_modelRegistry;
|
|
115
|
+
// Local-inference routing (opt-in via enableLocalInference). Lazily built.
|
|
116
|
+
_enableLocalInference;
|
|
117
|
+
_localRouter;
|
|
118
|
+
_localRouterBuilt = false;
|
|
119
|
+
_mlxServer;
|
|
120
|
+
_mlxServerBuilt = false;
|
|
111
121
|
// Tool registry for extension getTools/setTools
|
|
112
122
|
_toolRegistry = new Map();
|
|
113
123
|
_toolDefinitions = new Map();
|
|
@@ -126,6 +136,7 @@ export class AgentSession {
|
|
|
126
136
|
this._customTools = config.customTools ?? [];
|
|
127
137
|
this._cwd = config.cwd;
|
|
128
138
|
this._modelRegistry = config.modelRegistry;
|
|
139
|
+
this._enableLocalInference = config.enableLocalInference ?? false;
|
|
129
140
|
this._extensionRunnerRef = config.extensionRunnerRef;
|
|
130
141
|
this._initialActiveToolNames = config.initialActiveToolNames;
|
|
131
142
|
this._allowedToolNames = config.allowedToolNames ? new Set(config.allowedToolNames) : undefined;
|
|
@@ -148,6 +159,57 @@ export class AgentSession {
|
|
|
148
159
|
get modelRegistry() {
|
|
149
160
|
return this._modelRegistry;
|
|
150
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Local-inference router, built lazily from the flag + env + models.json
|
|
164
|
+
* routing config. Returns undefined when routing is not active.
|
|
165
|
+
*/
|
|
166
|
+
get localRouter() {
|
|
167
|
+
if (!this._localRouterBuilt) {
|
|
168
|
+
this._localRouterBuilt = true;
|
|
169
|
+
const mode = resolveRoutingMode({
|
|
170
|
+
enableFlag: this._enableLocalInference,
|
|
171
|
+
envMode: process.env.HOOCODE_ROUTING_MODE,
|
|
172
|
+
configMode: this._modelRegistry.getRoutingConfig()?.mode,
|
|
173
|
+
});
|
|
174
|
+
this._localRouter =
|
|
175
|
+
mode === "primary-only"
|
|
176
|
+
? undefined
|
|
177
|
+
: LocalInferenceRouter.create({
|
|
178
|
+
mode,
|
|
179
|
+
config: this._modelRegistry.getRoutingConfig(),
|
|
180
|
+
registry: this._modelRegistry,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return this._localRouter;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Ensure the harness-managed executor server (if configured) is healthy before
|
|
187
|
+
* the executor is used. Returns false when routing is inactive or the server
|
|
188
|
+
* could not be started (caller degrades to primary/raw). Reuses an externally
|
|
189
|
+
* started server when one is already healthy.
|
|
190
|
+
*/
|
|
191
|
+
async _ensureExecutorServer(signal) {
|
|
192
|
+
const router = this.localRouter;
|
|
193
|
+
if (!router?.isExecutorAvailable())
|
|
194
|
+
return false;
|
|
195
|
+
const serverConfig = router.getExecutorConfig()?.server;
|
|
196
|
+
if (!serverConfig)
|
|
197
|
+
return true; // user-managed server; assume available
|
|
198
|
+
if (!this._mlxServerBuilt) {
|
|
199
|
+
this._mlxServerBuilt = true;
|
|
200
|
+
const executor = router.getExecutorModel();
|
|
201
|
+
if (executor) {
|
|
202
|
+
this._mlxServer = new MlxServerManager({
|
|
203
|
+
config: serverConfig,
|
|
204
|
+
modelId: executor.id,
|
|
205
|
+
baseUrl: executor.baseUrl,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (!this._mlxServer)
|
|
210
|
+
return false;
|
|
211
|
+
return this._mlxServer.ensureStarted(signal);
|
|
212
|
+
}
|
|
151
213
|
async _getRequiredRequestAuth(model) {
|
|
152
214
|
const result = await this._modelRegistry.getApiKeyAndHeaders(model);
|
|
153
215
|
if (!result.ok) {
|
|
@@ -199,28 +261,113 @@ export class AgentSession {
|
|
|
199
261
|
};
|
|
200
262
|
this.agent.afterToolCall = async ({ toolCall, args, result, isError }) => {
|
|
201
263
|
const runner = this._extensionRunner;
|
|
202
|
-
|
|
203
|
-
|
|
264
|
+
let content = result.content;
|
|
265
|
+
let details = result.details;
|
|
266
|
+
let resolvedIsError = isError;
|
|
267
|
+
let changed = false;
|
|
268
|
+
// Extensions see the real (uncompressed) tool result first.
|
|
269
|
+
if (runner.hasHandlers("tool_result")) {
|
|
270
|
+
const hookResult = await runner.emitToolResult({
|
|
271
|
+
type: "tool_result",
|
|
272
|
+
toolName: toolCall.name,
|
|
273
|
+
toolCallId: toolCall.id,
|
|
274
|
+
input: args,
|
|
275
|
+
content: result.content,
|
|
276
|
+
details: result.details,
|
|
277
|
+
isError,
|
|
278
|
+
});
|
|
279
|
+
if (hookResult) {
|
|
280
|
+
content = hookResult.content ?? content;
|
|
281
|
+
details = hookResult.details ?? details;
|
|
282
|
+
resolvedIsError = hookResult.isError ?? isError;
|
|
283
|
+
changed = true;
|
|
284
|
+
}
|
|
204
285
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (!hookResult) {
|
|
215
|
-
return undefined;
|
|
286
|
+
// Optionally compress large read/bash output before it enters context.
|
|
287
|
+
// Only when local-inference tool-result routing is active; on any
|
|
288
|
+
// failure the original (uncompressed) content is kept (fallback to raw).
|
|
289
|
+
if (!resolvedIsError) {
|
|
290
|
+
const compressed = await this._maybeCompressToolResult(toolCall.name, content);
|
|
291
|
+
if (compressed) {
|
|
292
|
+
content = compressed;
|
|
293
|
+
changed = true;
|
|
294
|
+
}
|
|
216
295
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
isError: hookResult.isError ?? isError,
|
|
221
|
-
};
|
|
296
|
+
if (!changed)
|
|
297
|
+
return undefined;
|
|
298
|
+
return { content, details, isError: resolvedIsError };
|
|
222
299
|
};
|
|
223
300
|
}
|
|
301
|
+
/**
|
|
302
|
+
* Compress a tool result via the local executor when routing is active and the
|
|
303
|
+
* tool/size qualify. Returns the compressed content blocks, or undefined to
|
|
304
|
+
* keep the original (no routing, not compressible, too small, or any failure).
|
|
305
|
+
*/
|
|
306
|
+
async _maybeCompressToolResult(toolName, content) {
|
|
307
|
+
const router = this.localRouter;
|
|
308
|
+
if (!router)
|
|
309
|
+
return undefined;
|
|
310
|
+
const text = extractTextContent(content);
|
|
311
|
+
if (text === undefined)
|
|
312
|
+
return undefined;
|
|
313
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
314
|
+
if (!router.shouldCompressToolResult(toolName, bytes))
|
|
315
|
+
return undefined;
|
|
316
|
+
const executor = router.getExecutorModel();
|
|
317
|
+
const prompt = buildToolResultPrompt(toolName, text);
|
|
318
|
+
if (!executor || !prompt)
|
|
319
|
+
return undefined;
|
|
320
|
+
const startedAt = Date.now();
|
|
321
|
+
try {
|
|
322
|
+
if (!(await this._ensureExecutorServer())) {
|
|
323
|
+
throw new Error("executor server unavailable");
|
|
324
|
+
}
|
|
325
|
+
const { apiKey, headers } = await this._getRequiredRequestAuth(executor);
|
|
326
|
+
const response = await completeSimple(executor, {
|
|
327
|
+
systemPrompt: TOOL_RESULT_SYSTEM_PROMPT,
|
|
328
|
+
messages: [
|
|
329
|
+
{
|
|
330
|
+
role: "user",
|
|
331
|
+
content: [{ type: "text", text: prompt }],
|
|
332
|
+
timestamp: Date.now(),
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
}, { apiKey, headers });
|
|
336
|
+
if (response.stopReason === "error") {
|
|
337
|
+
throw new Error(response.errorMessage || "executor error");
|
|
338
|
+
}
|
|
339
|
+
const rawSummary = response.content
|
|
340
|
+
.filter((c) => c.type === "text")
|
|
341
|
+
.map((c) => c.text)
|
|
342
|
+
.join("\n");
|
|
343
|
+
const summary = stripThinkTags(rawSummary);
|
|
344
|
+
if (summary === "")
|
|
345
|
+
throw new Error("empty compression");
|
|
346
|
+
const compressedBytes = Buffer.byteLength(summary, "utf8");
|
|
347
|
+
// Guard: if the model failed to compress (output not smaller), keep raw.
|
|
348
|
+
if (compressedBytes >= bytes)
|
|
349
|
+
return undefined;
|
|
350
|
+
logRoutingMetrics({
|
|
351
|
+
turnKind: "tool-result",
|
|
352
|
+
provider: executor.provider,
|
|
353
|
+
model: executor.id,
|
|
354
|
+
latencyMs: Date.now() - startedAt,
|
|
355
|
+
bytesBefore: bytes,
|
|
356
|
+
bytesAfter: compressedBytes,
|
|
357
|
+
fallback: false,
|
|
358
|
+
});
|
|
359
|
+
return [
|
|
360
|
+
{
|
|
361
|
+
type: "text",
|
|
362
|
+
text: `${summary}\n\n[compressed from ${bytes} to ${compressedBytes} bytes by local executor]`,
|
|
363
|
+
},
|
|
364
|
+
];
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
logLocalInferenceFallback("tool-result", error);
|
|
368
|
+
return undefined;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
224
371
|
// =========================================================================
|
|
225
372
|
// Event Subscription
|
|
226
373
|
// =========================================================================
|
|
@@ -531,6 +678,7 @@ export class AgentSession {
|
|
|
531
678
|
this._extensionRunner.invalidate("This extension ctx is stale after session replacement or reload. Do not use a captured extension API or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().");
|
|
532
679
|
this._disconnectFromAgent();
|
|
533
680
|
this._eventListeners = [];
|
|
681
|
+
this._mlxServer?.stop();
|
|
534
682
|
cleanupSessionResources(this.sessionId);
|
|
535
683
|
}
|
|
536
684
|
// =========================================================================
|
|
@@ -1317,7 +1465,7 @@ export class AgentSession {
|
|
|
1317
1465
|
}
|
|
1318
1466
|
}
|
|
1319
1467
|
const generated = extensionCompaction ??
|
|
1320
|
-
(await
|
|
1468
|
+
(await this._compactWithRouting(preparation, model, apiKey, headers, customInstructions, signal));
|
|
1321
1469
|
if (signal.aborted) {
|
|
1322
1470
|
return { status: "cancelled" };
|
|
1323
1471
|
}
|
|
@@ -1338,6 +1486,33 @@ export class AgentSession {
|
|
|
1338
1486
|
result: { summary, firstKeptEntryId, tokensBefore, tokensAfter: tokensAfter ?? tokensBefore, details },
|
|
1339
1487
|
};
|
|
1340
1488
|
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Run compaction, routing the summarization to the local executor model when
|
|
1491
|
+
* local-inference routing is active for summarization. Any executor
|
|
1492
|
+
* failure falls back to the primary model so compaction never hard-fails.
|
|
1493
|
+
*/
|
|
1494
|
+
async _compactWithRouting(preparation, primaryModel, apiKey, headers, customInstructions, signal) {
|
|
1495
|
+
const router = this.localRouter;
|
|
1496
|
+
const executor = router?.selectModel("summarization", primaryModel);
|
|
1497
|
+
if (router && executor && executor !== primaryModel) {
|
|
1498
|
+
try {
|
|
1499
|
+
if (!(await this._ensureExecutorServer(signal))) {
|
|
1500
|
+
throw new Error("executor server unavailable");
|
|
1501
|
+
}
|
|
1502
|
+
const { apiKey: exKey, headers: exHeaders } = await this._getRequiredRequestAuth(executor);
|
|
1503
|
+
return await compact(preparation, executor, exKey, exHeaders, customInstructions, signal,
|
|
1504
|
+
// Executor runs without thinking (validated config); summary quality
|
|
1505
|
+
// relies on /no_think via the model's promptSuffix compat setting.
|
|
1506
|
+
"off");
|
|
1507
|
+
}
|
|
1508
|
+
catch (error) {
|
|
1509
|
+
if (signal.aborted)
|
|
1510
|
+
throw error;
|
|
1511
|
+
logLocalInferenceFallback("summarization", error);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
return compact(preparation, primaryModel, apiKey, headers, customInstructions, signal, this.thinkingLevel);
|
|
1515
|
+
}
|
|
1341
1516
|
/**
|
|
1342
1517
|
* Manually compact the session context.
|
|
1343
1518
|
* Aborts current agent operation first.
|
|
@@ -2523,4 +2698,19 @@ export class AgentSession {
|
|
|
2523
2698
|
return this._extensionRunner;
|
|
2524
2699
|
}
|
|
2525
2700
|
}
|
|
2701
|
+
/**
|
|
2702
|
+
* Join all text blocks of a tool result. Returns undefined if any non-text
|
|
2703
|
+
* (e.g. image) content is present, since those must not be compressed.
|
|
2704
|
+
*/
|
|
2705
|
+
function extractTextContent(content) {
|
|
2706
|
+
if (content.length === 0)
|
|
2707
|
+
return undefined;
|
|
2708
|
+
const texts = [];
|
|
2709
|
+
for (const block of content) {
|
|
2710
|
+
if (block.type !== "text")
|
|
2711
|
+
return undefined;
|
|
2712
|
+
texts.push(block.text);
|
|
2713
|
+
}
|
|
2714
|
+
return texts.join("\n");
|
|
2715
|
+
}
|
|
2526
2716
|
//# sourceMappingURL=agent-session.js.map
|