@hmharness/kernel 0.5.4 → 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
@@ -13,24 +13,40 @@ 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
- // Turn limit scales with the model's context window: 25 for small models,
17
- // up to 80 for 1M-window models. The context compaction keeps the
18
- // transcript within budget throughout, so the real ceiling is how many
19
- // reasoning turns the model can sustain, not raw token count.
20
- const maxTurns = opts.maxTurns ?? adaptiveMaxTurns(provider);
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;
21
22
  const budget = opts.maxContextChars ?? adaptiveContextChars(provider);
22
23
  const working = [...opts.messages];
23
24
  let toolUses = 0;
24
25
  const usage = { promptTokens: 0, completionTokens: 0 };
25
26
  const tools = registry.toOpenAITools();
26
27
  let wrapupSignaled = false;
27
- 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
+ }
28
45
  const compacted = opts.summarizeContext
29
46
  ? await compactWithDigest(working, budget, opts.summarizeContext)
30
47
  : compactMessages(working, budget);
31
- // Budget wrap-up signal: when context usage crosses 80%, inject a
32
- // single system nudge to wind down instead of hard-cutting mid-thought
33
- // (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).
34
50
  if (!wrapupSignaled && transcriptChars(compacted) > budget * 0.8) {
35
51
  wrapupSignaled = true;
36
52
  working.push({
@@ -38,6 +54,14 @@ export async function runLoop(opts) {
38
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.',
39
55
  });
40
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
+ }
41
65
  const chatRes = await modelCall(provider, compacted, tools, {
42
66
  onDelta: events?.onDelta,
43
67
  });
@@ -111,8 +135,14 @@ export async function runLoop(opts) {
111
135
  content: p.output.length > 60_000 ? p.output.slice(0, 60_000) + '\n...[truncated]' : p.output,
112
136
  });
113
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.
114
141
  }
115
- const text = `Turn limit reached (${maxTurns} turns). The session is preserved — continue with "hmh resume" or just send another message to pick up where this left off.`;
116
- events?.onFinal?.(text, maxTurns);
117
- 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 };
118
148
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hmharness/kernel",
3
- "version": "0.5.4",
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",