@zhijiewang/openharness 2.3.0 → 2.4.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/dist/commands/index.js +67 -0
- package/dist/harness/config.d.ts +7 -0
- package/dist/harness/memory.d.ts +2 -1
- package/dist/harness/memory.js +19 -4
- package/dist/main.js +16 -0
- package/dist/providers/fallback.d.ts +27 -0
- package/dist/providers/fallback.js +115 -0
- package/dist/query/index.js +50 -3
- package/dist/query/types.d.ts +2 -0
- package/dist/repl.js +16 -5
- package/dist/services/SkillExtractor.js +21 -1
- package/dist/tools/AgentTool/index.js +2 -36
- package/dist/tools/CronTool/index.d.ts +2 -2
- package/dist/tools/MemoryTool/index.d.ts +6 -6
- package/dist/tools/PipelineTool/index.d.ts +4 -4
- package/dist/tools/TaskUpdateTool/index.d.ts +2 -2
- package/package.json +1 -1
package/dist/commands/index.js
CHANGED
|
@@ -993,6 +993,73 @@ register("rebuild-sessions", "Rebuild session search index", () => {
|
|
|
993
993
|
});
|
|
994
994
|
return { output: "Rebuilding session search index...", handled: true };
|
|
995
995
|
});
|
|
996
|
+
// ── Skill Management ──
|
|
997
|
+
register("skill-create", "Create a new skill file", (args) => {
|
|
998
|
+
const name = args.trim();
|
|
999
|
+
if (!name)
|
|
1000
|
+
return { output: "Usage: /skill-create <name>", handled: true };
|
|
1001
|
+
if (name.includes("..") || name.includes("/") || name.includes("\\")) {
|
|
1002
|
+
return { output: "Error: Invalid skill name.", handled: true };
|
|
1003
|
+
}
|
|
1004
|
+
const dir = join(process.cwd(), ".oh", "skills");
|
|
1005
|
+
mkdirSync(dir, { recursive: true });
|
|
1006
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
1007
|
+
const filePath = join(dir, `${slug}.md`);
|
|
1008
|
+
if (existsSync(filePath)) {
|
|
1009
|
+
return { output: `Skill "${slug}" already exists at ${filePath}`, handled: true };
|
|
1010
|
+
}
|
|
1011
|
+
const template = `---
|
|
1012
|
+
name: ${slug}
|
|
1013
|
+
description: TODO — describe what this skill does
|
|
1014
|
+
trigger: ${slug}
|
|
1015
|
+
---
|
|
1016
|
+
|
|
1017
|
+
# ${name}
|
|
1018
|
+
|
|
1019
|
+
## When to Use
|
|
1020
|
+
Describe when this skill should be triggered.
|
|
1021
|
+
|
|
1022
|
+
## Procedure
|
|
1023
|
+
1. Step one
|
|
1024
|
+
2. Step two
|
|
1025
|
+
3. Step three
|
|
1026
|
+
|
|
1027
|
+
## Pitfalls
|
|
1028
|
+
- Common mistakes to avoid
|
|
1029
|
+
|
|
1030
|
+
## Verification
|
|
1031
|
+
How to confirm the skill worked correctly.
|
|
1032
|
+
`;
|
|
1033
|
+
writeFileSync(filePath, template);
|
|
1034
|
+
return { output: `Created skill: ${filePath}\nEdit the file to customize it.`, handled: true };
|
|
1035
|
+
});
|
|
1036
|
+
register("skill-delete", "Delete a skill file", (args) => {
|
|
1037
|
+
const name = args.trim();
|
|
1038
|
+
if (!name)
|
|
1039
|
+
return { output: "Usage: /skill-delete <name>", handled: true };
|
|
1040
|
+
const { findSkill } = require("../harness/plugins.js");
|
|
1041
|
+
const skill = findSkill(name);
|
|
1042
|
+
if (!skill)
|
|
1043
|
+
return { output: `Skill "${name}" not found.`, handled: true };
|
|
1044
|
+
try {
|
|
1045
|
+
const { unlinkSync } = require("node:fs");
|
|
1046
|
+
unlinkSync(skill.filePath);
|
|
1047
|
+
return { output: `Deleted skill: ${skill.filePath}`, handled: true };
|
|
1048
|
+
}
|
|
1049
|
+
catch (err) {
|
|
1050
|
+
return { output: `Error deleting skill: ${err.message}`, handled: true };
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
register("skill-edit", "Show skill file path for editing", (args) => {
|
|
1054
|
+
const name = args.trim();
|
|
1055
|
+
if (!name)
|
|
1056
|
+
return { output: "Usage: /skill-edit <name>", handled: true };
|
|
1057
|
+
const { findSkill } = require("../harness/plugins.js");
|
|
1058
|
+
const skill = findSkill(name);
|
|
1059
|
+
if (!skill)
|
|
1060
|
+
return { output: `Skill "${name}" not found.`, handled: true };
|
|
1061
|
+
return { output: `Skill file: ${skill.filePath}\nEdit this file to update the skill.`, handled: true };
|
|
1062
|
+
});
|
|
996
1063
|
// ── Command Parser ──
|
|
997
1064
|
/**
|
|
998
1065
|
* Check if input is a slash command. If so, execute it.
|
package/dist/harness/config.d.ts
CHANGED
|
@@ -68,6 +68,13 @@ export type OhConfig = {
|
|
|
68
68
|
balanced?: string;
|
|
69
69
|
powerful?: string;
|
|
70
70
|
};
|
|
71
|
+
/** Fallback providers — tried in order when primary fails */
|
|
72
|
+
fallbackProviders?: Array<{
|
|
73
|
+
provider: string;
|
|
74
|
+
model?: string;
|
|
75
|
+
apiKey?: string;
|
|
76
|
+
baseUrl?: string;
|
|
77
|
+
}>;
|
|
71
78
|
/** Effort level for LLM reasoning depth */
|
|
72
79
|
effortLevel?: "low" | "medium" | "high" | "max";
|
|
73
80
|
/** Opt-in telemetry (default: off) */
|
package/dist/harness/memory.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { Provider } from "../providers/base.js";
|
|
11
11
|
import type { Message } from "../types/message.js";
|
|
12
|
+
export declare function memoryVersion(): number;
|
|
12
13
|
/**
|
|
13
14
|
* Memory types — supports both legacy and Claude Code-compatible names.
|
|
14
15
|
* Legacy: convention, preference, project, debugging
|
|
@@ -28,7 +29,7 @@ export type MemoryEntry = {
|
|
|
28
29
|
};
|
|
29
30
|
/** Load all memories from project and global dirs */
|
|
30
31
|
export declare function loadMemories(): MemoryEntry[];
|
|
31
|
-
/** Build a system prompt section from loaded memories */
|
|
32
|
+
/** Build a system prompt section from loaded memories (capped at MEMORY_PROMPT_MAX_CHARS) */
|
|
32
33
|
export declare function memoriesToPrompt(memories: MemoryEntry[]): string;
|
|
33
34
|
/** Save a memory entry to the project memory directory */
|
|
34
35
|
export declare function saveMemory(name: string, type: MemoryType, description: string, content: string, global?: boolean): string;
|
package/dist/harness/memory.js
CHANGED
|
@@ -13,6 +13,11 @@ import { join, resolve, sep } from "node:path";
|
|
|
13
13
|
import { createUserMessage } from "../types/message.js";
|
|
14
14
|
const PROJECT_MEMORY_DIR = join(".oh", "memory");
|
|
15
15
|
const GLOBAL_MEMORY_DIR = join(homedir(), ".oh", "memory");
|
|
16
|
+
// Version counter — incremented on every save, used by query loop for live injection
|
|
17
|
+
let _memoryVersion = 0;
|
|
18
|
+
export function memoryVersion() {
|
|
19
|
+
return _memoryVersion;
|
|
20
|
+
}
|
|
16
21
|
/** Load all memories from project and global dirs */
|
|
17
22
|
export function loadMemories() {
|
|
18
23
|
const entries = [];
|
|
@@ -60,12 +65,19 @@ function parseMemory(raw, filePath) {
|
|
|
60
65
|
accessCount: accessCountMatch ? parseInt(accessCountMatch[1], 10) : 0,
|
|
61
66
|
};
|
|
62
67
|
}
|
|
63
|
-
/** Build a system prompt section from loaded memories */
|
|
68
|
+
/** Build a system prompt section from loaded memories (capped at MEMORY_PROMPT_MAX_CHARS) */
|
|
64
69
|
export function memoriesToPrompt(memories) {
|
|
65
70
|
if (memories.length === 0)
|
|
66
71
|
return "";
|
|
67
|
-
const
|
|
68
|
-
|
|
72
|
+
const header = "# Remembered Context\n";
|
|
73
|
+
let result = header;
|
|
74
|
+
for (const m of memories) {
|
|
75
|
+
const line = `- **${m.name}** (${m.type}): ${m.content.slice(0, 200)}\n`;
|
|
76
|
+
if (result.length + line.length > MEMORY_PROMPT_MAX_CHARS)
|
|
77
|
+
break;
|
|
78
|
+
result += line;
|
|
79
|
+
}
|
|
80
|
+
return result.trimEnd();
|
|
69
81
|
}
|
|
70
82
|
/** Save a memory entry to the project memory directory */
|
|
71
83
|
export function saveMemory(name, type, description, content, global = false) {
|
|
@@ -90,6 +102,7 @@ accessCount: 0
|
|
|
90
102
|
${content}
|
|
91
103
|
`;
|
|
92
104
|
writeFileSync(filePath, md);
|
|
105
|
+
_memoryVersion++;
|
|
93
106
|
updateMemoryIndex(dir);
|
|
94
107
|
return filePath;
|
|
95
108
|
}
|
|
@@ -274,7 +287,8 @@ export function consolidateMemories() {
|
|
|
274
287
|
}
|
|
275
288
|
// ── User Profile ──
|
|
276
289
|
const USER_PROFILE_FILE = "USER.md";
|
|
277
|
-
const USER_PROFILE_MAX_CHARS =
|
|
290
|
+
const USER_PROFILE_MAX_CHARS = 1375; // Matches Hermes USER.md limit
|
|
291
|
+
const MEMORY_PROMPT_MAX_CHARS = 2200; // Matches Hermes MEMORY.md limit
|
|
278
292
|
/** Load the user profile from .oh/memory/USER.md */
|
|
279
293
|
export function loadUserProfile() {
|
|
280
294
|
const filePath = join(PROJECT_MEMORY_DIR, USER_PROFILE_FILE);
|
|
@@ -307,6 +321,7 @@ updatedAt: ${Date.now()}
|
|
|
307
321
|
${truncated}
|
|
308
322
|
`;
|
|
309
323
|
writeFileSync(join(PROJECT_MEMORY_DIR, USER_PROFILE_FILE), md);
|
|
324
|
+
_memoryVersion++;
|
|
310
325
|
}
|
|
311
326
|
/** Format user profile for system prompt injection */
|
|
312
327
|
export function userProfileToPrompt() {
|
package/dist/main.js
CHANGED
|
@@ -19,6 +19,8 @@ import { readOhConfig } from "./harness/config.js";
|
|
|
19
19
|
import { emitHook } from "./harness/hooks.js";
|
|
20
20
|
import { detectProject, projectContextToPrompt } from "./harness/onboarding.js";
|
|
21
21
|
import { createRulesFile, loadRules, loadRulesAsPrompt } from "./harness/rules.js";
|
|
22
|
+
import { loadActiveMemories, memoriesToPrompt, userProfileToPrompt } from "./harness/memory.js";
|
|
23
|
+
import { discoverSkills, skillsToPrompt } from "./harness/plugins.js";
|
|
22
24
|
import { listSessions } from "./harness/session.js";
|
|
23
25
|
import { connectedMcpServers, disconnectMcpClients, getMcpInstructions, loadMcpTools } from "./mcp/loader.js";
|
|
24
26
|
import { getAllTools } from "./tools.js";
|
|
@@ -78,6 +80,20 @@ function buildSystemPrompt(model) {
|
|
|
78
80
|
const rulesPrompt = loadRulesAsPrompt();
|
|
79
81
|
if (rulesPrompt)
|
|
80
82
|
parts.push(rulesPrompt);
|
|
83
|
+
// User profile (highest priority personal context)
|
|
84
|
+
const userProfile = userProfileToPrompt();
|
|
85
|
+
if (userProfile)
|
|
86
|
+
parts.push(userProfile);
|
|
87
|
+
// Remembered context from past sessions
|
|
88
|
+
const memories = loadActiveMemories();
|
|
89
|
+
const memoriesPrompt = memoriesToPrompt(memories);
|
|
90
|
+
if (memoriesPrompt)
|
|
91
|
+
parts.push(memoriesPrompt);
|
|
92
|
+
// Available skills (Level 0 — names + descriptions only)
|
|
93
|
+
const skills = discoverSkills();
|
|
94
|
+
const skillsPrompt = skillsToPrompt(skills);
|
|
95
|
+
if (skillsPrompt)
|
|
96
|
+
parts.push(skillsPrompt);
|
|
81
97
|
// MCP server instructions (sandboxed — treat as untrusted)
|
|
82
98
|
const mcpInstructions = getMcpInstructions();
|
|
83
99
|
if (mcpInstructions.length > 0) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FallbackProvider — wraps a primary provider with fallback chain.
|
|
3
|
+
*
|
|
4
|
+
* When the primary provider fails (rate limit, 5xx, network), transparently
|
|
5
|
+
* tries the next provider in the chain. Matches Hermes Agent pattern.
|
|
6
|
+
*
|
|
7
|
+
* Design notes:
|
|
8
|
+
* - Streaming fallback only activates if primary fails BEFORE yielding events.
|
|
9
|
+
* Once events are streaming, partial output can't be un-sent, so we don't
|
|
10
|
+
* catch mid-stream errors (they propagate to the caller for retry).
|
|
11
|
+
* - 401/403 are NOT retriable (they're permanent auth failures). Different
|
|
12
|
+
* providers have different keys, so this is handled at the config level.
|
|
13
|
+
*/
|
|
14
|
+
import type { Provider } from "./base.js";
|
|
15
|
+
export type FallbackConfig = {
|
|
16
|
+
provider: Provider;
|
|
17
|
+
model?: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Create a provider that falls back to alternatives on failure.
|
|
21
|
+
* The primary provider is tried first. If it fails with a retriable error
|
|
22
|
+
* BEFORE streaming begins, each fallback is tried in order.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createFallbackProvider(primary: Provider, fallbacks: FallbackConfig[]): Provider & {
|
|
25
|
+
readonly activeFallback: string | null;
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=fallback.d.ts.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FallbackProvider — wraps a primary provider with fallback chain.
|
|
3
|
+
*
|
|
4
|
+
* When the primary provider fails (rate limit, 5xx, network), transparently
|
|
5
|
+
* tries the next provider in the chain. Matches Hermes Agent pattern.
|
|
6
|
+
*
|
|
7
|
+
* Design notes:
|
|
8
|
+
* - Streaming fallback only activates if primary fails BEFORE yielding events.
|
|
9
|
+
* Once events are streaming, partial output can't be un-sent, so we don't
|
|
10
|
+
* catch mid-stream errors (they propagate to the caller for retry).
|
|
11
|
+
* - 401/403 are NOT retriable (they're permanent auth failures). Different
|
|
12
|
+
* providers have different keys, so this is handled at the config level.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Create a provider that falls back to alternatives on failure.
|
|
16
|
+
* The primary provider is tried first. If it fails with a retriable error
|
|
17
|
+
* BEFORE streaming begins, each fallback is tried in order.
|
|
18
|
+
*/
|
|
19
|
+
export function createFallbackProvider(primary, fallbacks) {
|
|
20
|
+
let _activeFallback = null;
|
|
21
|
+
const obj = {
|
|
22
|
+
name: primary.name,
|
|
23
|
+
get activeFallback() {
|
|
24
|
+
return _activeFallback;
|
|
25
|
+
},
|
|
26
|
+
async *stream(messages, systemPrompt, tools, model) {
|
|
27
|
+
// Collect first event to detect early failure vs mid-stream failure.
|
|
28
|
+
// If the provider fails before ANY event, try fallback.
|
|
29
|
+
// If it fails mid-stream, propagate the error (partial output already sent).
|
|
30
|
+
const providers = [
|
|
31
|
+
{ provider: primary, model },
|
|
32
|
+
...fallbacks.map((fb) => ({ provider: fb.provider, model: fb.model ?? model })),
|
|
33
|
+
];
|
|
34
|
+
for (let i = 0; i < providers.length; i++) {
|
|
35
|
+
const p = providers[i];
|
|
36
|
+
try {
|
|
37
|
+
let hasYielded = false;
|
|
38
|
+
for await (const event of p.provider.stream(messages, systemPrompt, tools, p.model)) {
|
|
39
|
+
hasYielded = true;
|
|
40
|
+
yield event;
|
|
41
|
+
}
|
|
42
|
+
_activeFallback = i === 0 ? null : p.provider.name;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
// Mid-stream failure: can't un-send events, propagate error
|
|
47
|
+
if (i > 0 || !isRetriableError(err))
|
|
48
|
+
throw err;
|
|
49
|
+
// Pre-stream failure on primary: try next provider
|
|
50
|
+
_activeFallback = null;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
_activeFallback = null;
|
|
55
|
+
throw new Error("All providers failed (primary + fallbacks)");
|
|
56
|
+
},
|
|
57
|
+
async complete(messages, systemPrompt, tools, model) {
|
|
58
|
+
// complete() is atomic — safe to retry with any provider
|
|
59
|
+
const providers = [
|
|
60
|
+
{ provider: primary, model },
|
|
61
|
+
...fallbacks.map((fb) => ({ provider: fb.provider, model: fb.model ?? model })),
|
|
62
|
+
];
|
|
63
|
+
for (let i = 0; i < providers.length; i++) {
|
|
64
|
+
const p = providers[i];
|
|
65
|
+
try {
|
|
66
|
+
const result = await p.provider.complete(messages, systemPrompt, tools, p.model);
|
|
67
|
+
_activeFallback = i === 0 ? null : p.provider.name;
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
if (!isRetriableError(err))
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
_activeFallback = null;
|
|
76
|
+
throw new Error("All providers failed (primary + fallbacks)");
|
|
77
|
+
},
|
|
78
|
+
listModels() {
|
|
79
|
+
return primary.listModels();
|
|
80
|
+
},
|
|
81
|
+
async healthCheck() {
|
|
82
|
+
if (await primary.healthCheck())
|
|
83
|
+
return true;
|
|
84
|
+
for (const fb of fallbacks) {
|
|
85
|
+
if (await fb.provider.healthCheck())
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
},
|
|
90
|
+
estimateTokens: primary.estimateTokens?.bind(primary),
|
|
91
|
+
getModelInfo: primary.getModelInfo?.bind(primary),
|
|
92
|
+
};
|
|
93
|
+
return obj;
|
|
94
|
+
}
|
|
95
|
+
/** Check if an error is worth retrying with a different provider */
|
|
96
|
+
function isRetriableError(err) {
|
|
97
|
+
if (!(err instanceof Error))
|
|
98
|
+
return false;
|
|
99
|
+
const msg = err.message.toLowerCase();
|
|
100
|
+
return (msg.includes("rate limit") ||
|
|
101
|
+
msg.includes("429") ||
|
|
102
|
+
msg.includes("too many requests") ||
|
|
103
|
+
msg.includes("overloaded") ||
|
|
104
|
+
msg.includes("503") ||
|
|
105
|
+
msg.includes("529") ||
|
|
106
|
+
msg.includes("service unavailable") ||
|
|
107
|
+
msg.includes("econnrefused") ||
|
|
108
|
+
msg.includes("network") ||
|
|
109
|
+
msg.includes("timeout")
|
|
110
|
+
// Note: 401/403 are NOT retriable — they're permanent auth failures.
|
|
111
|
+
// Different providers use different API keys, so auth issues don't
|
|
112
|
+
// benefit from fallback. The user should fix their API key.
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=fallback.js.map
|
package/dist/query/index.js
CHANGED
|
@@ -21,7 +21,7 @@ const DEFAULT_MAX_TURNS = 50;
|
|
|
21
21
|
export async function* query(userMessage, config, existingMessages = []) {
|
|
22
22
|
const maxTurns = config.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
23
23
|
const toolContext = {
|
|
24
|
-
workingDir: process.cwd(),
|
|
24
|
+
workingDir: config.workingDir ?? process.cwd(),
|
|
25
25
|
abortSignal: config.abortSignal,
|
|
26
26
|
provider: config.provider,
|
|
27
27
|
model: config.model,
|
|
@@ -44,9 +44,26 @@ export async function* query(userMessage, config, existingMessages = []) {
|
|
|
44
44
|
toolPrompts += `\n\n[${deferredCount} additional tools available — use ToolSearch to discover them]`;
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
let fullSystemPrompt = toolPrompts
|
|
48
48
|
? `${config.systemPrompt}\n\n# Available Tools\n\n${toolPrompts}`
|
|
49
49
|
: config.systemPrompt;
|
|
50
|
+
// Auto-trigger skills matching user message
|
|
51
|
+
try {
|
|
52
|
+
const { findTriggeredSkills } = await import("../harness/plugins.js");
|
|
53
|
+
const triggered = findTriggeredSkills(userMessage);
|
|
54
|
+
if (triggered.length > 0) {
|
|
55
|
+
const hints = triggered.map((s) => `- **${s.name}**: ${s.description}`).join("\n");
|
|
56
|
+
fullSystemPrompt += `\n\n# Suggested Skills\nThese skills match your request. Use Skill tool to load them:\n${hints}`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch { /* skills optional */ }
|
|
60
|
+
// Track memory version for live injection
|
|
61
|
+
let lastMemoryVer = 0;
|
|
62
|
+
try {
|
|
63
|
+
const { memoryVersion } = await import("../harness/memory.js");
|
|
64
|
+
lastMemoryVer = memoryVersion();
|
|
65
|
+
}
|
|
66
|
+
catch { /* ignore */ }
|
|
50
67
|
const state = {
|
|
51
68
|
messages: [...existingMessages, createUserMessage(userMessage)],
|
|
52
69
|
turn: 0,
|
|
@@ -83,13 +100,43 @@ export async function* query(userMessage, config, existingMessages = []) {
|
|
|
83
100
|
}
|
|
84
101
|
}
|
|
85
102
|
}
|
|
103
|
+
// ── Dynamic prompt: refresh memories if changed, inject warnings ──
|
|
104
|
+
try {
|
|
105
|
+
const { memoryVersion, loadActiveMemories, memoriesToPrompt } = await import("../harness/memory.js");
|
|
106
|
+
const currentVer = memoryVersion();
|
|
107
|
+
if (currentVer > lastMemoryVer) {
|
|
108
|
+
const fresh = memoriesToPrompt(loadActiveMemories());
|
|
109
|
+
// Replace or append memory section in fullSystemPrompt
|
|
110
|
+
if (fullSystemPrompt.includes("# Remembered Context")) {
|
|
111
|
+
fullSystemPrompt = fullSystemPrompt.replace(/# Remembered Context[\s\S]*?(?=\n# |$)/, fresh);
|
|
112
|
+
}
|
|
113
|
+
else if (fresh) {
|
|
114
|
+
fullSystemPrompt += `\n\n${fresh}`;
|
|
115
|
+
}
|
|
116
|
+
lastMemoryVer = currentVer;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch { /* memory refresh optional */ }
|
|
120
|
+
let turnPrompt = fullSystemPrompt;
|
|
121
|
+
if (config.maxCost && config.maxCost > 0) {
|
|
122
|
+
const pct = state.totalCost / config.maxCost;
|
|
123
|
+
if (pct >= 0.9) {
|
|
124
|
+
turnPrompt += `\n\n⚠️ BUDGET CRITICAL: Only $${(config.maxCost - state.totalCost).toFixed(4)} remaining. Provide final response NOW.`;
|
|
125
|
+
}
|
|
126
|
+
else if (pct >= 0.7) {
|
|
127
|
+
turnPrompt += `\n\n⚠️ BUDGET WARNING: ${Math.round((1 - pct) * 100)}% budget remaining. Start consolidating.`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (state.turn >= maxTurns * 0.9 && maxTurns > 1) {
|
|
131
|
+
turnPrompt += `\n\n⚠️ TURN LIMIT: ${maxTurns - state.turn} turn(s) remaining. Wrap up.`;
|
|
132
|
+
}
|
|
86
133
|
// ── LLM call with streaming ──
|
|
87
134
|
let assistantContent = "";
|
|
88
135
|
const toolCalls = [];
|
|
89
136
|
let streamError = null;
|
|
90
137
|
const streamingExecutor = new StreamingToolExecutor(config.tools, toolContext, config.permissionMode, config.askUser, config.abortSignal);
|
|
91
138
|
try {
|
|
92
|
-
for await (const event of config.provider.stream(state.messages,
|
|
139
|
+
for await (const event of config.provider.stream(state.messages, turnPrompt, apiTools, config.model)) {
|
|
93
140
|
if (config.abortSignal?.aborted)
|
|
94
141
|
break;
|
|
95
142
|
switch (event.type) {
|
package/dist/query/types.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type QueryConfig = {
|
|
|
16
16
|
maxCost?: number;
|
|
17
17
|
model?: string;
|
|
18
18
|
abortSignal?: AbortSignal;
|
|
19
|
+
/** Working directory for tool execution (defaults to process.cwd()) */
|
|
20
|
+
workingDir?: string;
|
|
19
21
|
};
|
|
20
22
|
export type TransitionReason = "next_turn" | "retry_network" | "retry_prompt_too_long" | "retry_max_output_tokens";
|
|
21
23
|
export type QueryLoopState = {
|
package/dist/repl.js
CHANGED
|
@@ -674,17 +674,28 @@ export async function startREPL(config) {
|
|
|
674
674
|
if (extracted.length > 0) {
|
|
675
675
|
console.log(`[learn] Extracted ${extracted.length} skill(s) from this session.`);
|
|
676
676
|
}
|
|
677
|
-
// User profile update
|
|
677
|
+
// User profile update with LLM consolidation
|
|
678
678
|
if (messages.length >= 6) {
|
|
679
679
|
const detected = await detectMemories(config.provider, messages, currentModel);
|
|
680
680
|
const profileUpdates = detected.filter((d) => d.type === "user");
|
|
681
681
|
if (profileUpdates.length > 0) {
|
|
682
682
|
const currentProfile = loadUserProfile();
|
|
683
683
|
const newObservations = profileUpdates.map((d) => d.content).join("\n");
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
:
|
|
687
|
-
|
|
684
|
+
if (currentProfile) {
|
|
685
|
+
// LLM-assisted merge: consolidate instead of blind append
|
|
686
|
+
const { createUserMessage: makeMsg } = await import("./types/message.js");
|
|
687
|
+
try {
|
|
688
|
+
const consolidated = await config.provider.complete([makeMsg(`Merge this user profile with new observations into a single cohesive profile. Keep the most important and recent information. Remove duplicates. Stay under 2000 characters. Return ONLY the merged profile text.\n\nCurrent profile:\n${currentProfile}\n\nNew observations:\n${newObservations}`)], "You are a profile curator. Return ONLY the merged profile, no commentary.", undefined, currentModel);
|
|
689
|
+
updateUserProfile(consolidated.content);
|
|
690
|
+
}
|
|
691
|
+
catch {
|
|
692
|
+
// Fallback to simple append if LLM fails
|
|
693
|
+
updateUserProfile(`${currentProfile}\n\n${newObservations}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
updateUserProfile(newObservations);
|
|
698
|
+
}
|
|
688
699
|
}
|
|
689
700
|
}
|
|
690
701
|
}
|
|
@@ -113,6 +113,23 @@ ${candidate.verification}
|
|
|
113
113
|
writeFileSync(filePath, content, "utf-8");
|
|
114
114
|
return filePath;
|
|
115
115
|
}
|
|
116
|
+
/** Quick LLM quality check — is this skill worth keeping? */
|
|
117
|
+
async function isSkillWorthy(provider, candidate, model) {
|
|
118
|
+
try {
|
|
119
|
+
const prompt = `Is this extracted skill worth saving for future reuse? Answer YES or NO (one word only).
|
|
120
|
+
|
|
121
|
+
Name: ${candidate.name}
|
|
122
|
+
Description: ${candidate.description}
|
|
123
|
+
Procedure: ${candidate.procedure}
|
|
124
|
+
|
|
125
|
+
Criteria: Is it reusable (not a one-off)? Is the procedure clear and complete? Would it save time in future sessions?`;
|
|
126
|
+
const response = await provider.complete([createUserMessage(prompt)], "Answer YES or NO only.", undefined, model);
|
|
127
|
+
return response.content.trim().toUpperCase().startsWith("YES");
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return true; // On error, allow the skill through
|
|
131
|
+
}
|
|
132
|
+
}
|
|
116
133
|
/**
|
|
117
134
|
* Orchestrate the full extraction pipeline:
|
|
118
135
|
* 1. Check if extraction is warranted
|
|
@@ -132,9 +149,12 @@ export async function runExtraction(provider, messages, sessionId, model) {
|
|
|
132
149
|
const written = [];
|
|
133
150
|
for (const candidate of candidates) {
|
|
134
151
|
const similar = findSimilarSkill(candidate.name, candidate.description, existingSkills);
|
|
135
|
-
// Skip if a very similar skill already exists (avoid duplicates)
|
|
136
152
|
if (similar)
|
|
137
153
|
continue;
|
|
154
|
+
// Quality gate: quick LLM check before persisting
|
|
155
|
+
const worthy = await isSkillWorthy(provider, candidate, model);
|
|
156
|
+
if (!worthy)
|
|
157
|
+
continue;
|
|
138
158
|
const filePath = persistSkill(candidate, sessionId);
|
|
139
159
|
written.push(filePath);
|
|
140
160
|
}
|
|
@@ -87,6 +87,7 @@ export const AgentTool = {
|
|
|
87
87
|
model: agentModel,
|
|
88
88
|
maxTurns: 20,
|
|
89
89
|
abortSignal: context.abortSignal,
|
|
90
|
+
workingDir: agentWorkingDir,
|
|
90
91
|
};
|
|
91
92
|
const agentId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
92
93
|
emitHook("subagentStart", { agentId, toolName: input.subagent_type ?? "general" });
|
|
@@ -97,30 +98,13 @@ export const AgentTool = {
|
|
|
97
98
|
bus.registerBackgroundAgent(bgId, input.subagent_type ?? "general");
|
|
98
99
|
const runAgent = async () => {
|
|
99
100
|
let finalText = "";
|
|
100
|
-
const originalCwd = process.cwd();
|
|
101
101
|
try {
|
|
102
|
-
if (worktreePath) {
|
|
103
|
-
try {
|
|
104
|
-
process.chdir(agentWorkingDir);
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
/* ignore */
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
102
|
for await (const event of query(input.prompt, config)) {
|
|
111
103
|
if (event.type === "text_delta")
|
|
112
104
|
finalText += event.content;
|
|
113
105
|
}
|
|
114
106
|
}
|
|
115
107
|
finally {
|
|
116
|
-
if (worktreePath) {
|
|
117
|
-
try {
|
|
118
|
-
process.chdir(originalCwd);
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
/* ignore */
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
108
|
// Clean up worktree only if no changes were made
|
|
125
109
|
if (worktreePath) {
|
|
126
110
|
const hasChanges = hasWorktreeChanges(worktreePath);
|
|
@@ -152,16 +136,6 @@ export const AgentTool = {
|
|
|
152
136
|
const outputChunks = [];
|
|
153
137
|
let finalText = "";
|
|
154
138
|
try {
|
|
155
|
-
// Override process.cwd for the sub-agent by setting workingDir in tool context
|
|
156
|
-
const originalCwd = process.cwd();
|
|
157
|
-
if (worktreePath) {
|
|
158
|
-
try {
|
|
159
|
-
process.chdir(agentWorkingDir);
|
|
160
|
-
}
|
|
161
|
-
catch {
|
|
162
|
-
/* ignore */
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
139
|
try {
|
|
166
140
|
for await (const event of query(input.prompt, config)) {
|
|
167
141
|
if (event.type === "text_delta") {
|
|
@@ -184,15 +158,7 @@ export const AgentTool = {
|
|
|
184
158
|
}
|
|
185
159
|
}
|
|
186
160
|
finally {
|
|
187
|
-
|
|
188
|
-
if (worktreePath) {
|
|
189
|
-
try {
|
|
190
|
-
process.chdir(originalCwd);
|
|
191
|
-
}
|
|
192
|
-
catch {
|
|
193
|
-
/* ignore */
|
|
194
|
-
}
|
|
195
|
-
}
|
|
161
|
+
/* workingDir passed via config — no process.chdir cleanup needed */
|
|
196
162
|
}
|
|
197
163
|
}
|
|
198
164
|
catch (err) {
|
|
@@ -7,13 +7,13 @@ declare const createSchema: z.ZodObject<{
|
|
|
7
7
|
prompt: z.ZodString;
|
|
8
8
|
}, "strip", z.ZodTypeAny, {
|
|
9
9
|
action: "create";
|
|
10
|
-
prompt: string;
|
|
11
10
|
name: string;
|
|
11
|
+
prompt: string;
|
|
12
12
|
schedule: string;
|
|
13
13
|
}, {
|
|
14
14
|
action: "create";
|
|
15
|
-
prompt: string;
|
|
16
15
|
name: string;
|
|
16
|
+
prompt: string;
|
|
17
17
|
schedule: string;
|
|
18
18
|
}>;
|
|
19
19
|
declare const deleteSchema: z.ZodObject<{
|
|
@@ -12,18 +12,18 @@ declare const inputSchema: z.ZodObject<{
|
|
|
12
12
|
action: "search" | "list" | "save";
|
|
13
13
|
content?: string | undefined;
|
|
14
14
|
type?: "user" | "convention" | "preference" | "project" | "debugging" | "feedback" | "reference" | undefined;
|
|
15
|
-
query?: string | undefined;
|
|
16
|
-
description?: string | undefined;
|
|
17
|
-
name?: string | undefined;
|
|
18
15
|
global?: boolean | undefined;
|
|
16
|
+
name?: string | undefined;
|
|
17
|
+
description?: string | undefined;
|
|
18
|
+
query?: string | undefined;
|
|
19
19
|
}, {
|
|
20
20
|
action: "search" | "list" | "save";
|
|
21
21
|
content?: string | undefined;
|
|
22
22
|
type?: "user" | "convention" | "preference" | "project" | "debugging" | "feedback" | "reference" | undefined;
|
|
23
|
-
query?: string | undefined;
|
|
24
|
-
description?: string | undefined;
|
|
25
|
-
name?: string | undefined;
|
|
26
23
|
global?: boolean | undefined;
|
|
24
|
+
name?: string | undefined;
|
|
25
|
+
description?: string | undefined;
|
|
26
|
+
query?: string | undefined;
|
|
27
27
|
}>;
|
|
28
28
|
export declare const MemoryTool: Tool<typeof inputSchema>;
|
|
29
29
|
export {};
|
|
@@ -8,29 +8,29 @@ declare const inputSchema: z.ZodObject<{
|
|
|
8
8
|
dependsOn: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
9
9
|
}, "strip", z.ZodTypeAny, {
|
|
10
10
|
tool: string;
|
|
11
|
-
id: string;
|
|
12
11
|
args: Record<string, unknown>;
|
|
12
|
+
id: string;
|
|
13
13
|
dependsOn?: string[] | undefined;
|
|
14
14
|
}, {
|
|
15
15
|
tool: string;
|
|
16
|
-
id: string;
|
|
17
16
|
args: Record<string, unknown>;
|
|
17
|
+
id: string;
|
|
18
18
|
dependsOn?: string[] | undefined;
|
|
19
19
|
}>, "many">;
|
|
20
20
|
description: z.ZodOptional<z.ZodString>;
|
|
21
21
|
}, "strip", z.ZodTypeAny, {
|
|
22
22
|
steps: {
|
|
23
23
|
tool: string;
|
|
24
|
-
id: string;
|
|
25
24
|
args: Record<string, unknown>;
|
|
25
|
+
id: string;
|
|
26
26
|
dependsOn?: string[] | undefined;
|
|
27
27
|
}[];
|
|
28
28
|
description?: string | undefined;
|
|
29
29
|
}, {
|
|
30
30
|
steps: {
|
|
31
31
|
tool: string;
|
|
32
|
-
id: string;
|
|
33
32
|
args: Record<string, unknown>;
|
|
33
|
+
id: string;
|
|
34
34
|
dependsOn?: string[] | undefined;
|
|
35
35
|
}[];
|
|
36
36
|
description?: string | undefined;
|
|
@@ -13,8 +13,8 @@ declare const inputSchema: z.ZodObject<{
|
|
|
13
13
|
}, "strip", z.ZodTypeAny, {
|
|
14
14
|
taskId: number;
|
|
15
15
|
status?: "completed" | "pending" | "cancelled" | "in_progress" | "deleted" | undefined;
|
|
16
|
-
metadata?: Record<string, unknown> | undefined;
|
|
17
16
|
description?: string | undefined;
|
|
17
|
+
metadata?: Record<string, unknown> | undefined;
|
|
18
18
|
subject?: string | undefined;
|
|
19
19
|
activeForm?: string | undefined;
|
|
20
20
|
owner?: string | undefined;
|
|
@@ -23,8 +23,8 @@ declare const inputSchema: z.ZodObject<{
|
|
|
23
23
|
}, {
|
|
24
24
|
taskId: number;
|
|
25
25
|
status?: "completed" | "pending" | "cancelled" | "in_progress" | "deleted" | undefined;
|
|
26
|
-
metadata?: Record<string, unknown> | undefined;
|
|
27
26
|
description?: string | undefined;
|
|
27
|
+
metadata?: Record<string, unknown> | undefined;
|
|
28
28
|
subject?: string | undefined;
|
|
29
29
|
activeForm?: string | undefined;
|
|
30
30
|
owner?: string | undefined;
|