@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 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
- const maxTurns = opts.maxTurns ?? 25;
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
- for (let turn = 1; turn <= maxTurns; turn++) {
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
- // Budget wrap-up signal: when context usage crosses 80%, inject a
28
- // single system nudge to wind down instead of hard-cutting mid-thought
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
- const text = `Turn budget exhausted (${maxTurns}). Last state preserved in the session log.`;
112
- events?.onFinal?.(text, maxTurns);
113
- return { text, turns: maxTurns, toolUses, messages: working, usage };
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",
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",