dsh-continual-evolve 0.10.6 → 0.12.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.
Files changed (126) hide show
  1. package/README.md +35 -122
  2. package/README.zh.md +26 -97
  3. package/cordis.patch.yml +2 -2
  4. package/lib/index.d.ts +44 -241
  5. package/lib/index.js +30 -253
  6. package/lib/memory-guide.d.ts +13 -14
  7. package/lib/memory-guide.js +32 -21
  8. package/lib/memory-section.d.ts +77 -0
  9. package/lib/memory-section.js +189 -0
  10. package/package.json +2 -17
  11. package/lib/apply.d.ts +0 -22
  12. package/lib/apply.js +0 -157
  13. package/lib/approval.d.ts +0 -72
  14. package/lib/approval.js +0 -127
  15. package/lib/auto.d.ts +0 -236
  16. package/lib/auto.js +0 -773
  17. package/lib/autocase.d.ts +0 -38
  18. package/lib/autocase.js +0 -76
  19. package/lib/benchmark-command.d.ts +0 -9
  20. package/lib/benchmark-command.js +0 -357
  21. package/lib/benchmark.d.ts +0 -151
  22. package/lib/benchmark.js +0 -250
  23. package/lib/command.d.ts +0 -49
  24. package/lib/command.js +0 -568
  25. package/lib/consolidate.d.ts +0 -90
  26. package/lib/consolidate.js +0 -180
  27. package/lib/copy.d.ts +0 -74
  28. package/lib/copy.js +0 -160
  29. package/lib/declines.d.ts +0 -101
  30. package/lib/declines.js +0 -175
  31. package/lib/evaluate.d.ts +0 -74
  32. package/lib/evaluate.js +0 -271
  33. package/lib/evolve-event.d.ts +0 -40
  34. package/lib/evolve-event.js +0 -59
  35. package/lib/failures.d.ts +0 -42
  36. package/lib/failures.js +0 -171
  37. package/lib/fate.d.ts +0 -130
  38. package/lib/fate.js +0 -350
  39. package/lib/goal-command.d.ts +0 -7
  40. package/lib/goal-command.js +0 -37
  41. package/lib/goal.d.ts +0 -72
  42. package/lib/goal.js +0 -72
  43. package/lib/inject.d.ts +0 -196
  44. package/lib/inject.js +0 -412
  45. package/lib/llm-text.d.ts +0 -104
  46. package/lib/llm-text.js +0 -140
  47. package/lib/logfile.d.ts +0 -71
  48. package/lib/logfile.js +0 -159
  49. package/lib/memory-agent.d.ts +0 -129
  50. package/lib/memory-agent.js +0 -568
  51. package/lib/memory-benchmark.d.ts +0 -76
  52. package/lib/memory-benchmark.js +0 -170
  53. package/lib/memory-index.d.ts +0 -114
  54. package/lib/memory-index.js +0 -197
  55. package/lib/message-source.d.ts +0 -23
  56. package/lib/message-source.js +0 -12
  57. package/lib/mount-command.d.ts +0 -10
  58. package/lib/mount-command.js +0 -48
  59. package/lib/mount.d.ts +0 -42
  60. package/lib/mount.js +0 -212
  61. package/lib/notify.d.ts +0 -59
  62. package/lib/notify.js +0 -97
  63. package/lib/plan.d.ts +0 -16
  64. package/lib/plan.js +0 -126
  65. package/lib/planner.d.ts +0 -61
  66. package/lib/planner.js +0 -144
  67. package/lib/pool.d.ts +0 -7
  68. package/lib/pool.js +0 -25
  69. package/lib/prefix-cache.d.ts +0 -78
  70. package/lib/prefix-cache.js +0 -168
  71. package/lib/project.d.ts +0 -28
  72. package/lib/project.js +0 -66
  73. package/lib/projection.d.ts +0 -22
  74. package/lib/projection.js +0 -112
  75. package/lib/promotion.d.ts +0 -94
  76. package/lib/promotion.js +0 -171
  77. package/lib/recall.d.ts +0 -85
  78. package/lib/recall.js +0 -142
  79. package/lib/record-language.d.ts +0 -85
  80. package/lib/record-language.js +0 -152
  81. package/lib/render.d.ts +0 -13
  82. package/lib/render.js +0 -80
  83. package/lib/review-scheduler.d.ts +0 -38
  84. package/lib/review-scheduler.js +0 -191
  85. package/lib/review.d.ts +0 -60
  86. package/lib/review.js +0 -145
  87. package/lib/rollback.d.ts +0 -9
  88. package/lib/rollback.js +0 -68
  89. package/lib/rubric.d.ts +0 -29
  90. package/lib/rubric.js +0 -119
  91. package/lib/runtime.d.ts +0 -24
  92. package/lib/runtime.js +0 -87
  93. package/lib/score.d.ts +0 -64
  94. package/lib/score.js +0 -191
  95. package/lib/search.d.ts +0 -83
  96. package/lib/search.js +0 -136
  97. package/lib/service.d.ts +0 -49
  98. package/lib/service.js +0 -180
  99. package/lib/skill-render.d.ts +0 -23
  100. package/lib/skill-render.js +0 -69
  101. package/lib/skill.d.ts +0 -15
  102. package/lib/skill.js +0 -80
  103. package/lib/skillquality.d.ts +0 -85
  104. package/lib/skillquality.js +0 -320
  105. package/lib/source.d.ts +0 -30
  106. package/lib/source.js +0 -43
  107. package/lib/state.d.ts +0 -26
  108. package/lib/state.js +0 -151
  109. package/lib/store.d.ts +0 -62
  110. package/lib/store.js +0 -164
  111. package/lib/token-usage.d.ts +0 -68
  112. package/lib/token-usage.js +0 -211
  113. package/lib/tool.d.ts +0 -34
  114. package/lib/tool.js +0 -320
  115. package/lib/turn-snapshot.d.ts +0 -72
  116. package/lib/turn-snapshot.js +0 -238
  117. package/lib/types.d.ts +0 -208
  118. package/lib/types.js +0 -127
  119. package/lib/usage.d.ts +0 -36
  120. package/lib/usage.js +0 -98
  121. package/lib/validate.d.ts +0 -28
  122. package/lib/validate.js +0 -181
  123. package/lib/wrapup-command.d.ts +0 -10
  124. package/lib/wrapup-command.js +0 -243
  125. package/lib/wrapup.d.ts +0 -260
  126. package/lib/wrapup.js +0 -539
package/lib/planner.js DELETED
@@ -1,144 +0,0 @@
1
- import { parseProposal } from "./plan.js";
2
- import { formatHarnessStateForPrompt, historyForPrompt } from "./render.js";
3
- import { recentUserText, sessionEventsOf } from "./inject.js";
4
- import { buildPrefixMessages, detectPlannerRoute, resolvePrefixCache } from "./prefix-cache.js";
5
- import { skillQualityGuide } from "./skillquality.js";
6
- import { asLlmSessionId, streamText } from "./llm-text.js";
7
- import { createTokenUsageObserver } from "./token-usage.js";
8
- import { recordLanguageInstruction, resolveRecordLanguage } from "./record-language.js";
9
- export const PLANNER_SYSTEM_PROMPT = `You are the /evolve continual harness subsystem.
10
-
11
- Your job is to improve the editable continual harness state. Instead of
12
- summarizing the conversation you emit precise Create, Update, or Delete edits
13
- to reusable state: prompt notes, memories, skills, and subagent specs.
14
-
15
- Rules:
16
- - The base system prompt is immutable and MUST NOT be rewritten (never edit id "base_system_prompt").
17
- - Prefer small evidence-backed edits. If no useful edit is justified, return an empty edits array.
18
- - Repeated evidence is mandatory for prompt/skill/subagent proposals: the same
19
- pattern must recur at least twice in the trajectory. A single one-off
20
- interaction never justifies a prompt, skill, or subagent edit — return an
21
- empty edits array with a rationale instead. Session-scoped local prompt
22
- landings converge on /evolve wrapup human review; the turn pipeline must
23
- not silently persist one-off abstractions.
24
- - prompt = narrow behavioral policy addendums; memory = ONE FACT per entry, typed via metadata.memoryType
25
- - (user = who the user is; feedback = a pitfall/correction or confirmed approach, MUST carry Why + How to apply;
26
- - project = work goal/constraint not derivable from the repo; reference = URL/dashboard/ticket pointer).
27
- - Memory rules: one fact per entry (title = one-line retrieval hook, content = the fact alone);
28
- - never save what the repo already records (code structure, past fixes, git history, AGENTS.md) or what only
29
- - matters to this conversation — save the non-obvious point instead; before creating, update the existing
30
- - entry when one already covers the fact (creates carry metadata {memoryType} or they are rejected);
31
- - skill = repeatable procedures (must carry a python reference {type:"python", import, callable}
32
- and an arguments object); subagent = reusable delegation roles.
33
- - Skill entries are authored to the DSH skill quality standard
34
- (skill-creator, distilled from the official deepseek-harness 11 skills;
35
- the full facts are provided in the <skill_quality_standard> block below):
36
- only for a REAL trigger scenario grounded in the trajectory
37
- (who, in what real task, what signal) — never invent one to pad the store;
38
- never duplicate the official 11 skills or existing entries; content is a
39
- SKILL.md document (frontmatter routing with "use when / do not use when"
40
- description, boundary declaration, prerequisites and exclusions, layered
41
- information, verifiable completion criteria). Self-check every proposed
42
- skill against the 7 structural features and state the result in its
43
- reason field.
44
- - Repeated multi-step workflows (session start/end routines, recurring
45
- wrap-up or handoff procedures) may be proposed as guidance skills:
46
- kind=skill, skill_kind="guidance", content = a SKILL.md document (no
47
- python reference — executable skills keep requiring reference +
48
- arguments). Only propose with repeated evidence in the trajectory, never
49
- for one-off flows. Guidance skills materialize as discoverable SKILL.md
50
- files under <skillsRoot>/<kebab-name>/SKILL.md and are always offered to
51
- the user for a decision before they land.
52
- - Local edits are session-scoped; global edits persist across sessions.
53
- - Gap C2: EVERY edit MUST include a "blastRadius" field indicating how broadly
54
- the edit applies: "general" (cross-project tactical rule, applies everywhere),
55
- "project" (valid for the current project/repo), or "session" (one-off,
56
- specific to this session's context). The review gate validates that
57
- local-scope edits use "session" or "project", and global-scope edits use
58
- "general" or "project". When in doubt, prefer narrower blast radius.
59
- - Ground every edit in evidence: the session trajectory (recent direct user
60
- messages) is provided when available; prefer edits backed by it over
61
- speculation, and never invent preferences the user did not express.
62
- - Stale entries (superseded by newer ones, never referenced in recent
63
- trajectories, obsolete facts): propose action "archive" instead of
64
- "delete" — archive hides the entry from injection while keeping its data
65
- restorable; it requires only kind + id.
66
- - Output JSON only, exactly this shape:
67
- {
68
- "summary": "one sentence",
69
- "rationale": "why these edits are justified by the evidence",
70
- "expectedOutcome": "what should improve and how to validate it",
71
- "edits": [
72
- {
73
- "action": "create|update|delete",
74
- "kind": "prompt|memory|skill|subagent",
75
- "id": "stable id for update/delete, optional for create",
76
- "title": "required for create/update except delete",
77
- "content": "required for create/update except delete",
78
- "path": "optional grouping path",
79
- "reference": {"type":"python","import":"pkg.mod","callable":"fn"} ,
80
- "arguments": {"name": {"type":"string","required":true,"description":"..."}},
81
- "skill_kind": "executable|guidance (optional; skill kind only — guidance = SKILL.md document without reference)",
82
- "metadata": {},
83
- "reason": "why this edit is useful",
84
- "blastRadius": "general|project|session"
85
- }
86
- ]
87
- }`;
88
- export async function planWithLlm(ctx, options) {
89
- const { agent, state, history } = options;
90
- if (!agent.options.provider || !agent.options.model) {
91
- throw new Error("evolve: the calling agent has no provider/model route to plan with");
92
- }
93
- const scopeInstruction = options.global
94
- ? "Requested scope: global. Only propose stable cross-session lessons, durable preferences, reusable skills/subagents, or explicitly project-qualified facts."
95
- : "Requested scope: local. Propose session-scoped edits only when the trajectory shows repeated evidence (>=2 occurrences of the same pattern); a single one-off interaction returns an empty edits array. Global entries are read-only context — do not propose update/delete for them.";
96
- // Ground the plan in the caller's session: Route A carries the context
97
- // as a session-derived message prefix (cache-eligible on providers with
98
- // prompt caching) and drops the auto-extracted trajectory block; Route B
99
- // keeps the legacy flat-text block. An empty prefix falls back to B.
100
- const events = options.trajectoryEvents ?? sessionEventsOf(agent);
101
- const routing = resolvePrefixCache(options.prefixCache);
102
- const route = detectPlannerRoute(events, routing.mode);
103
- let prefixMessages = [];
104
- if (route === "A") {
105
- prefixMessages = buildPrefixMessages(events, {
106
- ...(agent.options.provider ? { provider: agent.options.provider } : {}),
107
- ...(agent.options.model ? { model: agent.options.model } : {}),
108
- maxChars: routing.maxChars,
109
- });
110
- }
111
- const trajectory = prefixMessages.length > 0 ? (options.trajectory ?? "") : (options.trajectory ?? recentUserText(agent));
112
- const language = options.language ?? resolveRecordLanguage({ ctx, trajectoryText: trajectory });
113
- // The skill quality standard is always present: the skill-creator
114
- // template facts when installed, the builtin distilled guide otherwise
115
- // (~1KB — planning is low-frequency, and the standard keeps skill
116
- // proposals from drifting off the quality bar).
117
- const qualityGuide = skillQualityGuide(options.skillsRoot);
118
- const userPrompt = [
119
- `<current_harness_state>\n${formatHarnessStateForPrompt(state)}\n</current_harness_state>`,
120
- `<refinement_history>\n${historyForPrompt(history)}\n</refinement_history>`,
121
- `<scope_policy>\n${scopeInstruction}\n</scope_policy>`,
122
- trajectory ? `<session_trajectory>\n${trajectory}\n</session_trajectory>` : "",
123
- `<skill_quality_standard>\n${qualityGuide.text}\n</skill_quality_standard>`,
124
- options.instructions ? `<user_instructions>\n${options.instructions}\n</user_instructions>` : "",
125
- "Return only JSON edits. If no useful edit is justified, return an empty edits array with a rationale.",
126
- ]
127
- .filter(Boolean)
128
- .join("\n\n");
129
- const text = await streamText(ctx, {
130
- provider: agent.options.provider,
131
- model: agent.options.model,
132
- sessionId: asLlmSessionId(agent.id),
133
- system: `${PLANNER_SYSTEM_PROMPT}\n\n${recordLanguageInstruction(language)}`,
134
- prompt: userPrompt,
135
- maxTokens: options.maxOutputTokens ?? 8000,
136
- signal: options.signal,
137
- ...(prefixMessages.length > 0 ? { prefixMessages } : {}),
138
- ...(options.tokenUsage
139
- ? { onUsage: createTokenUsageObserver(options.tokenUsage, "planner", agent.options.provider, agent.options.model) }
140
- : {}),
141
- });
142
- return parseProposal(text);
143
- }
144
- //# sourceMappingURL=planner.js.map
package/lib/pool.d.ts DELETED
@@ -1,7 +0,0 @@
1
- /**
2
- * Bounded-concurrency map: run async workers over items with at most
3
- * `concurrency` in flight. Used by the evaluation matrix so case × run units
4
- * execute in parallel without unbounded subagent fan-out.
5
- */
6
- export declare function mapPool<T, R>(items: readonly T[], concurrency: number, worker: (item: T, index: number) => Promise<R>): Promise<R[]>;
7
- //# sourceMappingURL=pool.d.ts.map
package/lib/pool.js DELETED
@@ -1,25 +0,0 @@
1
- /**
2
- * Bounded-concurrency map: run async workers over items with at most
3
- * `concurrency` in flight. Used by the evaluation matrix so case × run units
4
- * execute in parallel without unbounded subagent fan-out.
5
- */
6
- export async function mapPool(items, concurrency, worker) {
7
- if (items.length === 0) {
8
- return [];
9
- }
10
- const results = new Array(items.length);
11
- const limit = Math.max(1, Math.min(concurrency, items.length));
12
- let next = 0;
13
- async function runner() {
14
- while (true) {
15
- const index = next;
16
- next += 1;
17
- if (index >= items.length)
18
- return;
19
- results[index] = await worker(items[index], index);
20
- }
21
- }
22
- await Promise.all(Array.from({ length: limit }, () => runner()));
23
- return results;
24
- }
25
- //# sourceMappingURL=pool.js.map
@@ -1,78 +0,0 @@
1
- /**
2
- * Prefix-cache-aware planning input routing (Route A/B).
3
- *
4
- * The planner and the review gate re-send session context on every call as
5
- * flat text. On providers with prompt caching that context is re-billed at
6
- * full price each time. Route A instead prepends session-derived messages —
7
- * the same evidence in structured form, eligible for cache-read pricing —
8
- * and drops the redundant flat trajectory block. Route B keeps the legacy
9
- * flat-text input byte-for-byte.
10
- *
11
- * Adapted from dsh-continual-harness (Route A/B + cache-detect): this port
12
- * deliberately does NOT reproduce the host loop's byte-identical cache key
13
- * (no deriveMessages/tools/sessionId plumbing on this seam), so a hit needs
14
- * a provider that keys on the message-prefix content. Sources are stamped
15
- * honestly and every reduction is documented below.
16
- */
17
- import { type AssistantMessage, type UserMessage } from "@deepseek-ai/dsh-llm";
18
- /** Planning input shape: A = session-derived message prefix, B = flat-text trajectory. */
19
- export type PlannerRoute = "A" | "B";
20
- /** Routing mode: auto-detect from cache evidence, always prefix, or never. */
21
- export type PlannerPrefixCacheMode = "auto" | "session" | "off";
22
- /** Per-call routing overrides; absent fields fall back to the resolved defaults. */
23
- export interface PrefixCacheOptions {
24
- mode?: PlannerPrefixCacheMode;
25
- maxChars?: number;
26
- }
27
- /** Default session-prefix budget for one Route A input, in characters. */
28
- export declare const DEFAULT_PREFIX_MAX_CHARS = 12000;
29
- /**
30
- * Resolve per-call overrides against the defaults.
31
- *
32
- * @param opts - sparse per-call overrides.
33
- * @returns the effective mode (default "auto") and char budget.
34
- * @throws When maxChars is present but not a positive safe integer (fail loud on misconfiguration).
35
- */
36
- export declare function resolvePrefixCache(opts?: PrefixCacheOptions): {
37
- mode: PlannerPrefixCacheMode;
38
- maxChars: number;
39
- };
40
- /**
41
- * True when any assistant message in this session reported cache-read
42
- * tokens — evidence the provider can serve a warm prefix.
43
- *
44
- * @param events - session log rows, newest last.
45
- * @returns whether cache evidence exists (false on an empty/unreadable log).
46
- */
47
- export declare function hasCacheEvidence(events: readonly unknown[]): boolean;
48
- /**
49
- * Pick the planning input route for one call.
50
- *
51
- * @param events - session log rows, newest last.
52
- * @param mode - routing mode (default "auto").
53
- * @returns "A" when the session prefix should carry the context, else "B".
54
- */
55
- export declare function detectPlannerRoute(events: readonly unknown[], mode?: PlannerPrefixCacheMode): PlannerRoute;
56
- /**
57
- * Rebuild a tail-biased session prefix from the event log.
58
- *
59
- * Inclusion mirrors the trajectory grounding (only direct human `user`
60
- * speech plus the model's own assistant text), so injected plugin context
61
- * and tool results never echo back into the planning input. Whole messages
62
- * are dropped oldest-first over budget; a single over-budget message keeps
63
- * its tail.
64
- *
65
- * @param events - session log rows, newest last.
66
- * @param opts - provider/model stamps the assistant messages (the calling
67
- * agent's own route); maxChars caps the prefix (default {@link DEFAULT_PREFIX_MAX_CHARS}).
68
- * @returns the prefix messages, oldest first ([] when nothing qualifies —
69
- * callers fall back to Route B).
70
- * @throws When assistant text exists but provider/model is missing (an
71
- * assistant message cannot be honestly stamped without its route).
72
- */
73
- export declare function buildPrefixMessages(events: readonly unknown[], opts: {
74
- provider?: string;
75
- model?: string;
76
- maxChars?: number;
77
- }): (UserMessage | AssistantMessage)[];
78
- //# sourceMappingURL=prefix-cache.d.ts.map
@@ -1,168 +0,0 @@
1
- /**
2
- * Prefix-cache-aware planning input routing (Route A/B).
3
- *
4
- * The planner and the review gate re-send session context on every call as
5
- * flat text. On providers with prompt caching that context is re-billed at
6
- * full price each time. Route A instead prepends session-derived messages —
7
- * the same evidence in structured form, eligible for cache-read pricing —
8
- * and drops the redundant flat trajectory block. Route B keeps the legacy
9
- * flat-text input byte-for-byte.
10
- *
11
- * Adapted from dsh-continual-harness (Route A/B + cache-detect): this port
12
- * deliberately does NOT reproduce the host loop's byte-identical cache key
13
- * (no deriveMessages/tools/sessionId plumbing on this seam), so a hit needs
14
- * a provider that keys on the message-prefix content. Sources are stamped
15
- * honestly and every reduction is documented below.
16
- */
17
- import { createAssistantMessage, createUserMessage } from "@deepseek-ai/dsh-llm";
18
- /** Default session-prefix budget for one Route A input, in characters. */
19
- export const DEFAULT_PREFIX_MAX_CHARS = 12000;
20
- /**
21
- * Resolve per-call overrides against the defaults.
22
- *
23
- * @param opts - sparse per-call overrides.
24
- * @returns the effective mode (default "auto") and char budget.
25
- * @throws When maxChars is present but not a positive safe integer (fail loud on misconfiguration).
26
- */
27
- export function resolvePrefixCache(opts) {
28
- const mode = opts?.mode ?? "auto";
29
- const maxChars = opts?.maxChars ?? DEFAULT_PREFIX_MAX_CHARS;
30
- if (!Number.isSafeInteger(maxChars) || maxChars < 1) {
31
- throw new Error(`evolve: prefix-cache maxChars must be a positive integer, got ${String(opts?.maxChars)}`);
32
- }
33
- return { mode, maxChars };
34
- }
35
- /**
36
- * True when any assistant message in this session reported cache-read
37
- * tokens — evidence the provider can serve a warm prefix.
38
- *
39
- * @param events - session log rows, newest last.
40
- * @returns whether cache evidence exists (false on an empty/unreadable log).
41
- */
42
- export function hasCacheEvidence(events) {
43
- for (const raw of events) {
44
- if (typeof raw !== "object" || raw === null)
45
- continue;
46
- const event = raw;
47
- if (event.type !== "assistant/message")
48
- continue;
49
- const read = event.data?.usage?.cacheReadTokens;
50
- if (typeof read === "number" && read > 0)
51
- return true;
52
- }
53
- return false;
54
- }
55
- /**
56
- * Pick the planning input route for one call.
57
- *
58
- * @param events - session log rows, newest last.
59
- * @param mode - routing mode (default "auto").
60
- * @returns "A" when the session prefix should carry the context, else "B".
61
- */
62
- export function detectPlannerRoute(events, mode) {
63
- const resolved = mode ?? "auto";
64
- if (resolved === "session")
65
- return "A";
66
- if (resolved === "off")
67
- return "B";
68
- return hasCacheEvidence(events) ? "A" : "B";
69
- }
70
- /**
71
- * Extract one message's text without depending on the dsh-llm ContentBlock
72
- * type: string blocks pass through, object blocks contribute their `text`
73
- * field when present. Non-text blocks (tool calls, images) are dropped.
74
- */
75
- function blockText(content) {
76
- if (typeof content === "string")
77
- return content;
78
- if (!Array.isArray(content))
79
- return "";
80
- const parts = [];
81
- for (const block of content) {
82
- if (typeof block === "string") {
83
- parts.push(block);
84
- }
85
- else if (block !== null && typeof block === "object" && "text" in block && typeof block.text === "string") {
86
- parts.push(block.text);
87
- }
88
- }
89
- return parts.filter((text) => text.length > 0).join(" ");
90
- }
91
- /**
92
- * Rebuild a tail-biased session prefix from the event log.
93
- *
94
- * Inclusion mirrors the trajectory grounding (only direct human `user`
95
- * speech plus the model's own assistant text), so injected plugin context
96
- * and tool results never echo back into the planning input. Whole messages
97
- * are dropped oldest-first over budget; a single over-budget message keeps
98
- * its tail.
99
- *
100
- * @param events - session log rows, newest last.
101
- * @param opts - provider/model stamps the assistant messages (the calling
102
- * agent's own route); maxChars caps the prefix (default {@link DEFAULT_PREFIX_MAX_CHARS}).
103
- * @returns the prefix messages, oldest first ([] when nothing qualifies —
104
- * callers fall back to Route B).
105
- * @throws When assistant text exists but provider/model is missing (an
106
- * assistant message cannot be honestly stamped without its route).
107
- */
108
- export function buildPrefixMessages(events, opts) {
109
- const maxChars = opts.maxChars ?? DEFAULT_PREFIX_MAX_CHARS;
110
- if (!Number.isSafeInteger(maxChars) || maxChars < 1) {
111
- throw new Error(`evolve: prefix-cache maxChars must be a positive integer, got ${String(opts.maxChars)}`);
112
- }
113
- const pairs = [];
114
- for (let i = events.length - 1; i >= 0; i -= 1) {
115
- const raw = events[i];
116
- if (typeof raw !== "object" || raw === null)
117
- continue;
118
- const event = raw;
119
- const isUser = event.type === "user/message";
120
- const isAssistant = event.type === "assistant/message";
121
- if (!isUser && !isAssistant)
122
- continue;
123
- if (isUser) {
124
- const kind = event.data?.source?.kind;
125
- if (kind !== undefined && kind !== "user")
126
- continue;
127
- }
128
- const text = blockText(event.data?.content).trim();
129
- if (text.length === 0)
130
- continue;
131
- pairs.push({ role: isUser ? "user" : "assistant", text });
132
- }
133
- if (pairs.length === 0)
134
- return [];
135
- // Collected newest-first; restore conversation order before budgeting.
136
- pairs.reverse();
137
- // Tail-biased budget: drop oldest whole messages over budget; a lone
138
- // over-budget message keeps its tail.
139
- let total = pairs.reduce((sum, pair) => sum + pair.text.length, 0);
140
- while (pairs.length > 1 && total > maxChars) {
141
- const dropped = pairs.shift();
142
- total -= dropped?.text.length ?? 0;
143
- }
144
- const oldest = pairs[0];
145
- if (oldest !== undefined && total > maxChars) {
146
- oldest.text = oldest.text.slice(-maxChars);
147
- }
148
- const messages = [];
149
- for (const pair of pairs) {
150
- if (pair.role === "user") {
151
- messages.push(createUserMessage({
152
- source: { kind: "user" },
153
- content: [{ type: "text", text: pair.text }],
154
- }));
155
- }
156
- else {
157
- if (!opts.provider || !opts.model) {
158
- throw new Error("evolve: prefix-cache needs the agent provider/model to stamp assistant messages");
159
- }
160
- messages.push(createAssistantMessage({
161
- source: { provider: opts.provider, model: opts.model },
162
- content: [{ type: "text", text: pair.text }],
163
- }));
164
- }
165
- }
166
- return messages;
167
- }
168
- //# sourceMappingURL=prefix-cache.js.map
package/lib/project.d.ts DELETED
@@ -1,28 +0,0 @@
1
- /**
2
- * Stable project key for a workspace. `workspaceIdentity` (when set)
3
- * replaces the path as the hash source so renamed checkouts of the same
4
- * logical project keep one store.
5
- */
6
- export declare function resolveProjectKey(workspacePath: string, workspaceIdentity?: string): string;
7
- /** Defense for externally supplied keys (explicit opts): single safe segment. */
8
- export declare function sanitizeProjectKey(key: string): string;
9
- /** Minimal agent shape the project-key derivation needs (duck-typed). */
10
- export interface ProjectKeyAgentLike {
11
- session?: {
12
- header?: {
13
- cwd?: unknown;
14
- meta?: {
15
- cwd?: unknown;
16
- };
17
- };
18
- };
19
- }
20
- /**
21
- * Derive the project key from an assembling agent's session cwd. Returns
22
- * undefined when no validated absolute cwd is present — the caller then
23
- * skips the project layer rather than failing the whole assembly.
24
- */
25
- export declare function projectKeyOf(agent: unknown, opts?: {
26
- workspaceIdentity?: string;
27
- }): string | undefined;
28
- //# sourceMappingURL=project.d.ts.map
package/lib/project.js DELETED
@@ -1,66 +0,0 @@
1
- /**
2
- * Project scope identity: the third harness layer between session-local
3
- * staging and the cross-project global store.
4
- *
5
- * A project is identified by the session's validated absolute `cwd`
6
- * (stable slug + sha256, mirroring ZCode's `project-root.ts`), so every
7
- * session opened in the same workspace shares one project store while
8
- * different checkouts never collide. All helpers are pure and never throw —
9
- * a missing cwd simply means "no project layer" (the caller degrades to
10
- * global+local, the same shape as an empty project store).
11
- */
12
- import { createHash } from "node:crypto";
13
- import { basename, isAbsolute, resolve } from "node:path";
14
- /**
15
- * Stable project key for a workspace. `workspaceIdentity` (when set)
16
- * replaces the path as the hash source so renamed checkouts of the same
17
- * logical project keep one store.
18
- */
19
- export function resolveProjectKey(workspacePath, workspaceIdentity) {
20
- const identity = workspaceIdentity?.trim();
21
- const normalized = resolve(workspacePath);
22
- const keySource = identity && identity.length > 0 ? identity : process.platform === "win32" ? normalized.toLowerCase() : normalized;
23
- const hash = createHash("sha256").update(keySource).digest("hex").slice(0, 16);
24
- const slug = identity && identity.length > 0 ? "project" : sanitizeSlug(basename(normalized) || "project");
25
- return `${slug}-${hash}`;
26
- }
27
- function sanitizeSlug(value) {
28
- const slug = value
29
- .toLowerCase()
30
- .replace(/[^a-z0-9._-]+/g, "-")
31
- .replace(/^-+|-+$/g, "")
32
- .slice(0, 48);
33
- return slug.length > 0 ? slug : "project";
34
- }
35
- /** Defense for externally supplied keys (explicit opts): single safe segment. */
36
- export function sanitizeProjectKey(key) {
37
- const cleaned = key
38
- .trim()
39
- .replace(/[^a-zA-Z0-9._-]+/g, "-")
40
- .replace(/^-+|-+$/g, "")
41
- .slice(0, 64);
42
- return cleaned.length > 0 ? cleaned : "project";
43
- }
44
- /**
45
- * Derive the project key from an assembling agent's session cwd. Returns
46
- * undefined when no validated absolute cwd is present — the caller then
47
- * skips the project layer rather than failing the whole assembly.
48
- */
49
- export function projectKeyOf(agent, opts) {
50
- try {
51
- const header = agent?.session?.header;
52
- const raw = header?.cwd ?? header?.meta?.cwd;
53
- if (typeof raw !== "string" || raw.trim().length === 0) {
54
- return undefined;
55
- }
56
- const cwd = raw.trim();
57
- if (!isAbsolute(cwd)) {
58
- return undefined;
59
- }
60
- return resolveProjectKey(cwd, opts?.workspaceIdentity);
61
- }
62
- catch {
63
- return undefined;
64
- }
65
- }
66
- //# sourceMappingURL=project.js.map
@@ -1,22 +0,0 @@
1
- import type { HarnessState } from "./types.js";
2
- /** Index file name at the store root. */
3
- export declare const MEMORY_INDEX_FILE = "MEMORY.md";
4
- /** Per-fact file directory inside the store. */
5
- export declare const MEMORY_FACTS_DIR = "memory";
6
- /** Map a memory id to a flat filename (`:` is legal on disk but hostile to tools). */
7
- export declare function memoryFactFilename(id: string): string;
8
- /**
9
- * Rewrite the whole projection from the post-apply state: index, one file
10
- * per memory entry (including archived ones, flagged), and deletion of
11
- * files whose ids no longer exist. Deterministic — the same state always
12
- * produces the same files.
13
- *
14
- * @param stateDir The store directory holding harness_state.json.
15
- * @param state The freshly persisted state (post-apply, post-rollback).
16
- */
17
- export declare function materializeMemoryProjection(stateDir: string, state: HarnessState): void;
18
- /** Read back one projected fact file (round-trip helper for tests and tools). */
19
- export declare function readMemoryFact(factsDir: string, filename: string): string;
20
- /** True when the store directory carries a projection from a previous apply. */
21
- export declare function hasMemoryProjection(stateDir: string): boolean;
22
- //# sourceMappingURL=projection.d.ts.map
package/lib/projection.js DELETED
@@ -1,112 +0,0 @@
1
- /**
2
- * Readable Markdown projection (P1): a ZCode-style `MEMORY.md` index plus
3
- * one file per memory fact, materialized from the JSON store.
4
- *
5
- * The JSON harness state stays the single source of truth (versions,
6
- * rollback, audit). The projection is a derived, human-trustable view:
7
- * it is rewritten from the post-apply state inside the engine, so the
8
- * model can never write it directly and rollback/archive stay in sync.
9
- *
10
- * Layout per store directory:
11
- *
12
- * <stateDir>/MEMORY.md one line per memory entry
13
- * <stateDir>/memory/<file>.md one fact per file with frontmatter
14
- */
15
- import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
16
- import { join } from "node:path";
17
- import { isArchived, isMemoryType, MEMORY_TYPE_KEY } from "./types.js";
18
- /** Index file name at the store root. */
19
- export const MEMORY_INDEX_FILE = "MEMORY.md";
20
- /** Per-fact file directory inside the store. */
21
- export const MEMORY_FACTS_DIR = "memory";
22
- /** Map a memory id to a flat filename (`:` is legal on disk but hostile to tools). */
23
- export function memoryFactFilename(id) {
24
- return `${id.replace(/:/g, "_")}.md`;
25
- }
26
- /**
27
- * Rewrite the whole projection from the post-apply state: index, one file
28
- * per memory entry (including archived ones, flagged), and deletion of
29
- * files whose ids no longer exist. Deterministic — the same state always
30
- * produces the same files.
31
- *
32
- * @param stateDir The store directory holding harness_state.json.
33
- * @param state The freshly persisted state (post-apply, post-rollback).
34
- */
35
- export function materializeMemoryProjection(stateDir, state) {
36
- const entries = Object.values(state.entries.memory).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
37
- const factsDir = join(stateDir, MEMORY_FACTS_DIR);
38
- mkdirSync(factsDir, { recursive: true });
39
- const expected = new Map();
40
- for (const entry of entries) {
41
- let filename = memoryFactFilename(entry.id);
42
- let suffix = 1;
43
- while (expected.has(filename)) {
44
- suffix += 1;
45
- filename = `${memoryFactFilename(entry.id).replace(/\.md$/, "")}~${suffix}.md`;
46
- }
47
- expected.set(filename, entry);
48
- }
49
- for (const [filename, entry] of expected) {
50
- writeFileSync(join(factsDir, filename), renderMemoryFact(entry), { encoding: "utf8", mode: 0o600 });
51
- }
52
- // Sweep facts whose entries vanished (delete); archived entries keep
53
- // their file with archived:true frontmatter.
54
- let existing;
55
- try {
56
- existing = readdirSync(factsDir).filter((file) => file.endsWith(".md"));
57
- }
58
- catch {
59
- existing = [];
60
- }
61
- for (const file of existing) {
62
- if (!expected.has(file)) {
63
- try {
64
- unlinkSync(join(factsDir, file));
65
- }
66
- catch {
67
- // A stale fact file must not break the apply path; the next
68
- // materialization retries the sweep.
69
- }
70
- }
71
- }
72
- writeFileSync(join(stateDir, MEMORY_INDEX_FILE), renderMemoryIndex([...expected]), { encoding: "utf8", mode: 0o600 });
73
- }
74
- /** Read back one projected fact file (round-trip helper for tests and tools). */
75
- export function readMemoryFact(factsDir, filename) {
76
- return readFileSync(join(factsDir, filename), "utf8");
77
- }
78
- /** True when the store directory carries a projection from a previous apply. */
79
- export function hasMemoryProjection(stateDir) {
80
- return existsSync(join(stateDir, MEMORY_INDEX_FILE)) && existsSync(join(stateDir, MEMORY_FACTS_DIR));
81
- }
82
- function renderMemoryFact(entry) {
83
- const type = isMemoryType(entry.metadata[MEMORY_TYPE_KEY]) ? String(entry.metadata[MEMORY_TYPE_KEY]) : "untyped";
84
- const frontmatter = [
85
- "---",
86
- `id: ${entry.id}`,
87
- `scope: ${entry.scope}`,
88
- `kind: memory`,
89
- `memoryType: ${type}`,
90
- `version: ${entry.version}`,
91
- `path: ${entry.path}`,
92
- `archived: ${isArchived(entry)}`,
93
- `created_at: ${entry.created_at}`,
94
- `updated_at: ${entry.updated_at}`,
95
- "---",
96
- ].join("\n");
97
- return `${frontmatter}\n\n# ${entry.title}\n\n${entry.content}\n`;
98
- }
99
- function renderMemoryIndex(files) {
100
- const lines = ["# Memory Index", "", "Derived from the JSON harness state — do not edit by hand; it is rewritten on every memory apply.", ""];
101
- if (files.length === 0) {
102
- lines.push("(no memory entries yet)");
103
- return `${lines.join("\n")}\n`;
104
- }
105
- for (const [file, entry] of files) {
106
- const type = isMemoryType(entry.metadata[MEMORY_TYPE_KEY]) ? String(entry.metadata[MEMORY_TYPE_KEY]) : "untyped";
107
- const archived = isArchived(entry) ? ", archived" : "";
108
- lines.push(`- [${entry.scope}:${entry.id}] ${entry.title} (type=${type}, v${entry.version}${archived}) → ${MEMORY_FACTS_DIR}/${file}`);
109
- }
110
- return `${lines.join("\n")}\n`;
111
- }
112
- //# sourceMappingURL=projection.js.map