@nanhara/hara 0.99.3 → 0.100.0
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 +19 -0
- package/README.md +1 -1
- package/dist/agent/compact.js +17 -0
- package/dist/agent/loop.js +28 -1
- package/dist/agent/reminders.js +38 -0
- package/dist/index.js +2 -11
- package/dist/org/roles.js +34 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ All notable changes to `@nanhara/hara`.
|
|
|
5
5
|
> Versioning (pre-1.0, SemVer-style): the **minor** (middle) number bumps for a **new feature**; the
|
|
6
6
|
> **patch** (last) number bumps for **optimizations/fixes of existing features**.
|
|
7
7
|
|
|
8
|
+
## 0.100.0 — the agent keeps its own attention: system-reminders + anti-drift compaction
|
|
9
|
+
|
|
10
|
+
Distilled from a source-level study of Claude Code v1.0.33's agent internals (the Ie1/WD5 reminder
|
|
11
|
+
layer and the AU2 compaction template).
|
|
12
|
+
|
|
13
|
+
- **system-reminder injection layer.** An event queue (`agent/reminders.ts`) that lands queued context
|
|
14
|
+
as ONE `<system-reminder>`-wrapped message before the next model call — visible to the model, never
|
|
15
|
+
rendered in the transcript, and always carrying the "ignore unless relevant" disclaimer so a nudge
|
|
16
|
+
can't derail unrelated work. Quiet (sub-agent) runs neither drain nor push, so parallel fan-outs
|
|
17
|
+
can't steal the main conversation's reminders. First wired event:
|
|
18
|
+
- **Todo attention-refresh.** When a checklist has unfinished items and goes 5 tool-rounds untouched,
|
|
19
|
+
the model gets a reminder re-showing the authoritative list and asking for a status pass — so long
|
|
20
|
+
tasks stop silently abandoning their own plan. Any `todo_write` resets the clock; after firing it
|
|
21
|
+
re-arms (at most one nag per 5 rounds).
|
|
22
|
+
- **Compaction brief: 6 → 8 sections (anti-drift).** `/compact` and auto-compaction now also preserve
|
|
23
|
+
**All user messages** — your own words survive verbatim, in order, however hard the history is
|
|
24
|
+
squeezed — and **Key technical concepts**, so the next turn doesn't re-derive the stack.
|
|
25
|
+
(`COMPACT_SYSTEM` moved to `agent/compact.ts` where tests pin the structure.)
|
|
26
|
+
|
|
8
27
|
## 0.99.3 — TUI: rock-steady input box (constant-height chrome) + plan mode grows a real handshake
|
|
9
28
|
|
|
10
29
|
Built from a source-level study of codex-rs (bottom-anchored viewport, plan cells) and Claude Code
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
**Highlights**
|
|
14
14
|
- **An org, not just an agent** — `hara org "<task>"` routes work to the role that *owns* it; `hara plan "<task>"` decomposes a task into a verified DAG of atoms (frame → atomize → sequence → execute → **verify gate**), and `hara plan --parallel` runs independent atoms concurrently.
|
|
15
15
|
- **Drive it from chat** — `hara gateway` runs your local hara from **Telegram · WeChat · Discord · Feishu/Lark · Slack · Mattermost · Matrix · DingTalk · WeCom · Signal** (10 platforms), with **two-way images** (send a photo → it sees it; ask for a file → it sends one), per-chat resumable sessions, and project roaming. Connects out — no public webhook. See **[docs/gateway.md](docs/gateway.md)**.
|
|
16
|
-
- **Real terminal UX** — an **ink TUI**: bottom-pinned input box, **plan mode** (read-only →
|
|
16
|
+
- **Real terminal UX** — an **ink TUI**: bottom-pinned input box, **plan mode** (read-only investigation → the model submits its plan via `exit_plan` → approve → execute), selectable approvals with "don't ask again", windowed reasoning, **paste images** (Ctrl+V) for vision models, light/dark theme.
|
|
17
17
|
- **Persistent memory + self-evolution** — `memory_*` tools over global/project `MEMORY.md`; the agent recalls before acting, **proactively saves** durable facts, and grows its own playbooks (a lexical guard screens what it writes). Inspect/consolidate it with **`hara memory show`** and **`hara memory distill`** (promote recent daily logs → durable memory). Lexical-first by design — semantic search is opt-in, never required.
|
|
18
18
|
- **Multi-provider, all streamed** — Anthropic (Claude) or any OpenAI-compatible endpoint (Qwen/DashScope, GLM, Kimi, OpenAI) with live Markdown + visible reasoning.
|
|
19
19
|
- **Delegate to other agents** — the **`external_agent`** tool hands a self-contained task to **Claude Code** or **Codex** running headless, and returns the result — so you pick the best engine per task. Gated by approval, trust-tiered, and never exposed to read-only sub-agents.
|
package/dist/agent/compact.js
CHANGED
|
@@ -3,6 +3,23 @@
|
|
|
3
3
|
// which reuses the manual /compact path; this just decides *when* to fire.
|
|
4
4
|
/** Auto-compact once the last turn used ≥ this % of the model's context window. */
|
|
5
5
|
export const AUTO_COMPACT_PCT = 85;
|
|
6
|
+
/** The compaction brief (shared by /compact and auto-compaction). Eight sections, mirroring Claude
|
|
7
|
+
* Code's AU2 template — the two that matter most beyond the obvious: **All user messages** (the
|
|
8
|
+
* user's own words survive verbatim, so however hard the history is squeezed, intent never drifts)
|
|
9
|
+
* and **Key technical concepts** (the frameworks/APIs in play, so the next turn doesn't re-derive
|
|
10
|
+
* the stack). Lives here (not index.ts) so tests can pin the structure. */
|
|
11
|
+
export const COMPACT_SYSTEM = "Summarize the conversation so far into a structured, complete brief so the assistant can continue with NO " +
|
|
12
|
+
"loss of context. First think privately in a brief <analysis> scratchpad (what matters, what's in flight), " +
|
|
13
|
+
"then output ONLY the summary under these exact headings:\n" +
|
|
14
|
+
"1. Goal — the user's overall intent, in their own framing.\n" +
|
|
15
|
+
"2. Key technical concepts — the frameworks, tools, APIs, and domain ideas central to the work.\n" +
|
|
16
|
+
"3. Key decisions — choices made and why (so they aren't relitigated).\n" +
|
|
17
|
+
"4. Files & code — files created/changed and the important snippets, with why each matters.\n" +
|
|
18
|
+
"5. Errors & fixes — failures hit, how they were resolved, and any correction the user gave (quote pointed feedback verbatim).\n" +
|
|
19
|
+
"6. Current state — what works now / what is verified.\n" +
|
|
20
|
+
"7. All user messages — EVERY user message so far (excluding tool results), verbatim and in order; abbreviate only huge pasted blobs with […]. These are the ground truth of intent.\n" +
|
|
21
|
+
"8. Next step — the immediate next action, INCLUDING a direct verbatim quote of the user's most recent request so there is no drift.\n" +
|
|
22
|
+
"Be specific and concrete. Drop the <analysis>; output only the headed summary.";
|
|
6
23
|
/** Whether to auto-compact now: enabled, the history is substantial enough to be worth summarizing, and the
|
|
7
24
|
* last turn filled the context past the threshold (so the NEXT turn would risk overflow). */
|
|
8
25
|
export function shouldAutoCompact(ctxPct, historyLen, autoCompact, threshold = AUTO_COMPACT_PCT) {
|
package/dist/agent/loop.js
CHANGED
|
@@ -10,7 +10,8 @@ import { decideCommand, loadPermissionRules } from "../security/permissions.js";
|
|
|
10
10
|
import { classifyRisk, guardianVeto, guardianEnabled, newBreaker, recordBlock } from "../security/guardian.js";
|
|
11
11
|
import { subdirHint } from "../context/subdir-hints.js";
|
|
12
12
|
import { classifyError, failoverAction, errorHint } from "./failover.js";
|
|
13
|
-
import { currentTodos } from "../tools/todo.js";
|
|
13
|
+
import { currentTodos, renderTodos } from "../tools/todo.js";
|
|
14
|
+
import { drainReminders, wrapReminders, pushReminder, todoStaleReminder, TODO_STALE_ROUNDS } from "./reminders.js";
|
|
14
15
|
/** Spinner verb (terminal mode + reused by TUI tests): when the agent has an in_progress todo,
|
|
15
16
|
* surface its activeForm/text so the bottom-of-screen line reads concretely ("▶ updating tests… 3s")
|
|
16
17
|
* instead of "working 3s". Pure: takes a snapshot + elapsed seconds. */
|
|
@@ -86,6 +87,9 @@ export async function runAgent(history, opts) {
|
|
|
86
87
|
const guardianOn = !!opts.guardian && (opts.guardian.enabled ?? true) && guardianEnabled();
|
|
87
88
|
const breaker = newBreaker();
|
|
88
89
|
let breakerHalt = false; // set when a tripped breaker aborts this run
|
|
90
|
+
// Todo attention-refresh (à la Claude Code): tool rounds since the checklist was last touched while
|
|
91
|
+
// unfinished items exist. Main loop only — quiet (sub-agent) runs share the global list and must not nag.
|
|
92
|
+
let todoIdleRounds = 0;
|
|
89
93
|
for (;;) {
|
|
90
94
|
// Type-ahead steering: fold in anything the user submitted while the previous step ran, so it
|
|
91
95
|
// reaches the model on this next call (drained after the last tool round; empty on the 1st pass).
|
|
@@ -93,6 +97,14 @@ export async function runAgent(history, opts) {
|
|
|
93
97
|
for (const m of await opts.pendingInput())
|
|
94
98
|
history.push(m);
|
|
95
99
|
}
|
|
100
|
+
// system-reminder injection: event-driven context queued since the last call (todo staleness today)
|
|
101
|
+
// lands as ONE wrapped user message the UI never renders. Quiet runs don't drain — a parallel
|
|
102
|
+
// sub-agent must not steal the main conversation's reminders.
|
|
103
|
+
if (!opts.quiet) {
|
|
104
|
+
const reminders = drainReminders();
|
|
105
|
+
if (reminders.length)
|
|
106
|
+
history.push({ role: "user", content: wrapReminders(reminders) });
|
|
107
|
+
}
|
|
96
108
|
const baseSpecs = opts.toolFilter ? toolSpecs().filter((t) => opts.toolFilter(t.name)) : toolSpecs();
|
|
97
109
|
const specs = opts.extraTools?.length
|
|
98
110
|
? [...baseSpecs, ...opts.extraTools.map((t) => ({ name: t.name, description: t.description, input_schema: t.input_schema }))]
|
|
@@ -349,6 +361,21 @@ export async function runAgent(history, opts) {
|
|
|
349
361
|
}
|
|
350
362
|
await flush();
|
|
351
363
|
history.push({ role: "tool", results });
|
|
364
|
+
// Todo attention-refresh: a round that touched the checklist resets the clock; rounds that leave
|
|
365
|
+
// unfinished items untouched accumulate, and at TODO_STALE_ROUNDS the model gets a system-reminder
|
|
366
|
+
// re-showing the authoritative list (then the counter re-arms — at most one nag per N rounds).
|
|
367
|
+
if (!opts.quiet) {
|
|
368
|
+
if (r.toolUses.some((tu) => tu.name === "todo_write")) {
|
|
369
|
+
todoIdleRounds = 0;
|
|
370
|
+
}
|
|
371
|
+
else if (currentTodos().some((t) => t.status !== "done")) {
|
|
372
|
+
todoIdleRounds++;
|
|
373
|
+
if (todoIdleRounds >= TODO_STALE_ROUNDS) {
|
|
374
|
+
pushReminder(todoStaleReminder(renderTodos(currentTodos())));
|
|
375
|
+
todoIdleRounds = 0;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
352
379
|
if (breakerHalt) {
|
|
353
380
|
// A tripped-and-declined circuit-breaker is a hard stop: end the run cleanly (the denial messages are
|
|
354
381
|
// already in `results` so the model/user see why). Never spin further.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// system-reminder injection (à la Claude Code's Ie1/WD5 event layer): event-driven context the model
|
|
2
|
+
// should see on its NEXT call, injected as ONE `<system-reminder>`-wrapped user message. It rides the
|
|
3
|
+
// provider history only — the UI transcript never renders it, so the user isn't bothered while the
|
|
4
|
+
// model stays synchronized with system state (todo staleness today; file-change/diagnostic events can
|
|
5
|
+
// plug in later via pushReminder).
|
|
6
|
+
//
|
|
7
|
+
// Claude Code's disclaimer is preserved: the model is told the context may be irrelevant, so an
|
|
8
|
+
// injected nudge never derails an unrelated task.
|
|
9
|
+
const queue = [];
|
|
10
|
+
/** Queue a reminder for injection before the next model call (main loop only — quiet/sub-agent runs
|
|
11
|
+
* neither push nor drain, so a parallel fan-out can't steal the main conversation's reminders). */
|
|
12
|
+
export function pushReminder(text) {
|
|
13
|
+
const t = text.trim();
|
|
14
|
+
if (t)
|
|
15
|
+
queue.push(t);
|
|
16
|
+
}
|
|
17
|
+
/** Take everything queued (FIFO), clearing the queue. */
|
|
18
|
+
export function drainReminders() {
|
|
19
|
+
if (!queue.length)
|
|
20
|
+
return [];
|
|
21
|
+
return queue.splice(0, queue.length);
|
|
22
|
+
}
|
|
23
|
+
/** Merge queued reminders into the single injected message. */
|
|
24
|
+
export function wrapReminders(items) {
|
|
25
|
+
return ("<system-reminder>\n" +
|
|
26
|
+
items.join("\n\n") +
|
|
27
|
+
"\n\nThis context may or may not be relevant to your task — do not respond to it directly; ignore it unless it is relevant.\n" +
|
|
28
|
+
"</system-reminder>");
|
|
29
|
+
}
|
|
30
|
+
/** How many tool rounds a checklist may sit untouched (with unfinished items) before the model gets an
|
|
31
|
+
* attention refresh. Reset on every todo_write; re-arms after firing so it nags at most once per N. */
|
|
32
|
+
export const TODO_STALE_ROUNDS = 5;
|
|
33
|
+
/** The staleness nudge: re-show the authoritative list + ask for a status pass. */
|
|
34
|
+
export function todoStaleReminder(renderedTodos) {
|
|
35
|
+
return (`Your todo list has not been updated in a while. Current state:\n\n${renderedTodos}\n\n` +
|
|
36
|
+
"If you have completed or started items, update them with todo_write now (statuses drive the user's progress view). " +
|
|
37
|
+
"If the list no longer matches the work, rewrite it.");
|
|
38
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -24,7 +24,7 @@ import { clearEnrollment, enrollDevice, heartbeat, syncOrgRoles } from "./org-fl
|
|
|
24
24
|
import { loadActiveProfile, listProfiles, useProfile, addProfile, upsertProfile, removeProfile, setModel as setProfileModel, resetModel as resetProfileModel, getProfile, effectiveModel, routingLabel, routeHost, activeId, resolveActive, setFlagOverride, writePin, removePin, pinFilePath, DEFAULT_ORG_ID, PERSONAL_ID, } from "./profile/profile.js";
|
|
25
25
|
import { loadPermissionRules, scaffoldPermissions, globalPermissionsPath, projectPermissionsPath } from "./security/permissions.js";
|
|
26
26
|
import { routingProvider } from "./agent/route.js";
|
|
27
|
-
import { shouldAutoCompact } from "./agent/compact.js";
|
|
27
|
+
import { shouldAutoCompact, COMPACT_SYSTEM } from "./agent/compact.js";
|
|
28
28
|
import { formatContextReport } from "./agent/context-report.js";
|
|
29
29
|
import { userTurnPreviews, rewindTo } from "./agent/rewind.js";
|
|
30
30
|
import { checkpoint, listCheckpoints, restoreCheckpoint } from "./checkpoints.js";
|
|
@@ -716,16 +716,7 @@ const MEMORY_DISTILL_SYSTEM = "You consolidate an agent's short-term daily memor
|
|
|
716
716
|
"conventions / user preferences from the logs that are NOT already captured, and persist each with " +
|
|
717
717
|
"memory_write (target=memory, or target=user for preferences; pick the right scope=project|global). " +
|
|
718
718
|
"Skip the ephemeral, the one-off, and anything already known. Be terse and de-duplicated. Then reply DONE.";
|
|
719
|
-
|
|
720
|
-
"loss of context. First think privately in a brief <analysis> scratchpad (what matters, what's in flight), " +
|
|
721
|
-
"then output ONLY the summary under these exact headings:\n" +
|
|
722
|
-
"1. Goal — the user's overall intent, in their own framing.\n" +
|
|
723
|
-
"2. Key decisions — choices made and why (so they aren't relitigated).\n" +
|
|
724
|
-
"3. Files & code — files created/changed and the important snippets, with why each matters.\n" +
|
|
725
|
-
"4. Errors & fixes — failures hit, how they were resolved, and any correction the user gave (quote pointed feedback verbatim).\n" +
|
|
726
|
-
"5. Current state — what works now / what is verified.\n" +
|
|
727
|
-
"6. Next step — the immediate next action, INCLUDING a direct verbatim quote of the user's most recent request so there is no drift.\n" +
|
|
728
|
-
"Be specific and concrete. Drop the <analysis>; output only the headed summary.";
|
|
719
|
+
// COMPACT_SYSTEM (the 8-section brief) lives in agent/compact.ts so tests can pin its structure.
|
|
729
720
|
const workingSetFromSummary = (s) => s
|
|
730
721
|
.split("\n")
|
|
731
722
|
.map((l) => l.replace(/^[-*\d.\s]+/, "").trim())
|
package/dist/org/roles.js
CHANGED
|
@@ -22,13 +22,37 @@ export function orgRolesDir() {
|
|
|
22
22
|
export function claudeAgentsDir(cwd) {
|
|
23
23
|
return join(findProjectRoot(cwd), ".claude", "agents");
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
/** Claude-Code tool names → hara tool names, for `.claude/agents` interop. Without this, a CC agent
|
|
26
|
+
* with `tools: Read, Edit, Bash` produced allowTools that matched ZERO hara tools — the role spawned
|
|
27
|
+
* with an empty toolbox. Unknown names pass through verbatim (they may be hara names already). */
|
|
28
|
+
const CLAUDE_TOOL_MAP = {
|
|
29
|
+
read: "read_file",
|
|
30
|
+
edit: "edit_file",
|
|
31
|
+
write: "write_file",
|
|
32
|
+
bash: "bash",
|
|
33
|
+
grep: "grep",
|
|
34
|
+
glob: "glob",
|
|
35
|
+
ls: "ls",
|
|
36
|
+
webfetch: "web_fetch",
|
|
37
|
+
websearch: "web_search",
|
|
38
|
+
agent: "agent",
|
|
39
|
+
task: "agent",
|
|
40
|
+
todowrite: "todo_write",
|
|
41
|
+
notebookedit: "edit_file",
|
|
42
|
+
};
|
|
43
|
+
/** Accept Claude-Code `tools:` (comma string or list) as an alias for hara's allowTools —
|
|
44
|
+
* translating CC tool names to hara's. "All tools" / "*" means unrestricted → undefined. */
|
|
45
|
+
export function claudeTools(v) {
|
|
46
|
+
const raw = Array.isArray(v)
|
|
47
|
+
? v
|
|
48
|
+
: typeof v === "string" && v.trim()
|
|
49
|
+
? v.split(",").map((s) => s.trim()).filter(Boolean)
|
|
50
|
+
: null;
|
|
51
|
+
if (!raw || !raw.length)
|
|
52
|
+
return undefined;
|
|
53
|
+
if (raw.some((t) => /^(\*|all tools?)$/i.test(t)))
|
|
54
|
+
return undefined; // unrestricted
|
|
55
|
+
return raw.map((t) => CLAUDE_TOOL_MAP[t.toLowerCase().replace(/[^a-z]/g, "")] ?? t);
|
|
32
56
|
}
|
|
33
57
|
function parseFrontmatter(text) {
|
|
34
58
|
const m = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(text);
|
|
@@ -83,7 +107,9 @@ export function loadRoles(cwd) {
|
|
|
83
107
|
description: fm.description || "",
|
|
84
108
|
owns: Array.isArray(fm.owns) ? fm.owns : [],
|
|
85
109
|
rejects: Array.isArray(fm.rejects) ? fm.rejects : [],
|
|
86
|
-
model
|
|
110
|
+
// Claude-Code model ALIASES (sonnet/opus/haiku/inherit) aren't hara model ids — treat as
|
|
111
|
+
// "inherit the session model" rather than passing a string no provider resolves.
|
|
112
|
+
model: fm.model && !/^(sonnet|opus|haiku|inherit)$/i.test(String(fm.model)) ? fm.model : undefined,
|
|
87
113
|
allowTools: Array.isArray(fm.allowTools) ? fm.allowTools : claudeTools(fm.tools),
|
|
88
114
|
denyTools: Array.isArray(fm.denyTools) ? fm.denyTools : undefined,
|
|
89
115
|
system: body,
|