@hmharness/kernel 0.5.3 → 0.5.5
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/loop.d.ts +4 -0
- package/dist/loop.js +43 -9
- package/dist/window.d.ts +10 -0
- package/dist/window.js +10 -0
- package/package.json +1 -1
package/dist/loop.d.ts
CHANGED
|
@@ -45,4 +45,8 @@ export declare function runLoop(opts: {
|
|
|
45
45
|
previousDigest: string | null;
|
|
46
46
|
evicted: string[];
|
|
47
47
|
}) => Promise<string>;
|
|
48
|
+
/** Hard total turn cap — the safety valve (default: 5x the adaptive soft limit, max 400). */
|
|
49
|
+
maxTotalTurns?: number;
|
|
50
|
+
/** Hard total token spend cap — prompt + completion combined (default: 10M). */
|
|
51
|
+
maxTotalTokens?: number;
|
|
48
52
|
}): Promise<LoopResult>;
|
package/dist/loop.js
CHANGED
|
@@ -8,25 +8,45 @@
|
|
|
8
8
|
* transcript is compacted against the context budget.
|
|
9
9
|
*/
|
|
10
10
|
import { compactMessages, compactWithDigest, transcriptChars } from "./context.js";
|
|
11
|
-
import { adaptiveContextChars } from "./window.js";
|
|
11
|
+
import { adaptiveContextChars, adaptiveMaxTurns } from "./window.js";
|
|
12
12
|
import { chat } from "./provider.js";
|
|
13
13
|
export async function runLoop(opts) {
|
|
14
14
|
const { provider, registry, ctx, events } = opts;
|
|
15
15
|
const modelCall = opts.chatImpl ?? chat;
|
|
16
|
-
|
|
16
|
+
// Soft limit: where the wrap-up nudge fires (adaptive to context window).
|
|
17
|
+
const softTurnLimit = adaptiveMaxTurns(provider);
|
|
18
|
+
// Hard limit: the actual safety valve. If the model is still calling tools
|
|
19
|
+
// at the soft limit, we auto-continue — the loop only hard-stops here.
|
|
20
|
+
const hardTurnLimit = opts.maxTotalTurns ?? Math.min(softTurnLimit * 5, 400);
|
|
21
|
+
const hardTokenLimit = opts.maxTotalTokens ?? 10_000_000;
|
|
17
22
|
const budget = opts.maxContextChars ?? adaptiveContextChars(provider);
|
|
18
23
|
const working = [...opts.messages];
|
|
19
24
|
let toolUses = 0;
|
|
20
25
|
const usage = { promptTokens: 0, completionTokens: 0 };
|
|
21
26
|
const tools = registry.toOpenAITools();
|
|
22
27
|
let wrapupSignaled = false;
|
|
23
|
-
|
|
28
|
+
let turn = 0;
|
|
29
|
+
let reason = 'final';
|
|
30
|
+
// The loop runs until the model gives a final answer (no tool calls) OR a
|
|
31
|
+
// safety valve fires. The old "maxTurns stop" is replaced by a soft
|
|
32
|
+
// checkpoint: if the model is still actively working (calling tools) at
|
|
33
|
+
// the soft limit, the loop auto-continues — long-running tasks feel
|
|
34
|
+
// unlimited while the hard valves protect against runaway loops and cost.
|
|
35
|
+
while (true) {
|
|
36
|
+
turn++;
|
|
37
|
+
if (turn > hardTurnLimit) {
|
|
38
|
+
reason = 'turn-valve';
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
if (usage.promptTokens + usage.completionTokens > hardTokenLimit) {
|
|
42
|
+
reason = 'token-valve';
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
24
45
|
const compacted = opts.summarizeContext
|
|
25
46
|
? await compactWithDigest(working, budget, opts.summarizeContext)
|
|
26
47
|
: compactMessages(working, budget);
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
// (Codex's token-budget-context pattern; DeepSeek's 80% pressure trigger).
|
|
48
|
+
// Context budget wrap-up signal: when usage crosses 80%, nudge the model
|
|
49
|
+
// to wind down (Codex token_budget_context + DeepSeek 80% pressure).
|
|
30
50
|
if (!wrapupSignaled && transcriptChars(compacted) > budget * 0.8) {
|
|
31
51
|
wrapupSignaled = true;
|
|
32
52
|
working.push({
|
|
@@ -34,6 +54,14 @@ export async function runLoop(opts) {
|
|
|
34
54
|
content: '[context budget] Context is running low. Wrap up the current task: summarize what was accomplished, suggest concrete next steps, and stop starting new sub-tasks.',
|
|
35
55
|
});
|
|
36
56
|
}
|
|
57
|
+
// Soft turn checkpoint: at the adaptive limit, nudge to conclude — but
|
|
58
|
+
// the model can keep working if it's mid-task (auto-continue).
|
|
59
|
+
if (turn === softTurnLimit) {
|
|
60
|
+
working.push({
|
|
61
|
+
role: 'system',
|
|
62
|
+
content: `[turn checkpoint] You have been working for ${turn} turns. If the task is substantially complete, give your final answer now. If not, continue — the turn limit has been lifted; work until done.`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
37
65
|
const chatRes = await modelCall(provider, compacted, tools, {
|
|
38
66
|
onDelta: events?.onDelta,
|
|
39
67
|
});
|
|
@@ -107,8 +135,14 @@ export async function runLoop(opts) {
|
|
|
107
135
|
content: p.output.length > 60_000 ? p.output.slice(0, 60_000) + '\n...[truncated]' : p.output,
|
|
108
136
|
});
|
|
109
137
|
}
|
|
138
|
+
// Loop continues: the model was actively calling tools, so we keep going
|
|
139
|
+
// (auto-continuation past the soft turn limit). Only the safety valves
|
|
140
|
+
// (hardTurnLimit / hardTokenLimit) can stop us now.
|
|
110
141
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
142
|
+
// Safety valve fired
|
|
143
|
+
const text = reason === 'turn-valve'
|
|
144
|
+
? `Safety turn limit reached (${turn - 1} turns, hard cap ${hardTurnLimit}). The session is preserved — send another message to continue with fresh limits.`
|
|
145
|
+
: `Token budget limit reached (~${usage.promptTokens + usage.completionTokens} tokens, cap ${hardTokenLimit}). The session is preserved — send another message to continue.`;
|
|
146
|
+
events?.onFinal?.(text, turn - 1);
|
|
147
|
+
return { text, turns: turn - 1, toolUses, messages: working, usage };
|
|
114
148
|
}
|
package/dist/window.d.ts
CHANGED
|
@@ -38,3 +38,13 @@ export declare function adaptiveContextChars(p: {
|
|
|
38
38
|
model: string;
|
|
39
39
|
contextWindow?: number;
|
|
40
40
|
}): number;
|
|
41
|
+
/** Adaptive turn limit: larger context windows sustain more turns before
|
|
42
|
+
* quality degrades (the context budget system compacts throughout, so the
|
|
43
|
+
* real ceiling is how many turns the model can reason over, not raw token
|
|
44
|
+
* count). Floor 25 (small models), cap 80 (even 1M windows don't need more).
|
|
45
|
+
* This fixes the "25 turns auto-stop" complaint — the context engineering
|
|
46
|
+
* was working fine, the turn cap was the bottleneck. */
|
|
47
|
+
export declare function adaptiveMaxTurns(p: {
|
|
48
|
+
model: string;
|
|
49
|
+
contextWindow?: number;
|
|
50
|
+
}): number;
|
package/dist/window.js
CHANGED
|
@@ -45,3 +45,13 @@ export function contextBudgetChars(windowTokens) {
|
|
|
45
45
|
export function adaptiveContextChars(p) {
|
|
46
46
|
return contextBudgetChars(contextWindowFor(p).windowTokens);
|
|
47
47
|
}
|
|
48
|
+
/** Adaptive turn limit: larger context windows sustain more turns before
|
|
49
|
+
* quality degrades (the context budget system compacts throughout, so the
|
|
50
|
+
* real ceiling is how many turns the model can reason over, not raw token
|
|
51
|
+
* count). Floor 25 (small models), cap 80 (even 1M windows don't need more).
|
|
52
|
+
* This fixes the "25 turns auto-stop" complaint — the context engineering
|
|
53
|
+
* was working fine, the turn cap was the bottleneck. */
|
|
54
|
+
export function adaptiveMaxTurns(p) {
|
|
55
|
+
const budget = contextBudgetChars(contextWindowFor(p).windowTokens);
|
|
56
|
+
return Math.max(25, Math.min(80, Math.floor(budget / 4000)));
|
|
57
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hmharness/kernel",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "hmharness kernel: tool registry, provider adapters, the agent loop, session log, config. Zero runtime dependencies (Node >=22 native fetch).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|