@haaaiawd/second-nature 0.1.38 → 0.1.40
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/agent-inner-guide.md +18 -0
- package/index.js +10 -2
- package/openclaw.plugin.json +2 -2
- package/package.json +1 -1
- package/runtime/cli/commands/connector-init.js +11 -4
- package/runtime/cli/index.js +6 -1
- package/runtime/cli/ops/heartbeat-surface.d.ts +15 -0
- package/runtime/cli/ops/heartbeat-surface.js +16 -2
- package/runtime/cli/ops/ops-router.js +229 -83
- package/runtime/cli/ops/workspace-heartbeat-runner.js +49 -4
- package/runtime/connectors/services/connector-executor-adapter.js +192 -41
- package/runtime/core/second-nature/guidance/apply-guidance.d.ts +2 -0
- package/runtime/core/second-nature/guidance/apply-guidance.js +6 -1
- package/runtime/core/second-nature/guidance/user-reply-continuity.d.ts +1 -1
- package/runtime/core/second-nature/guidance/user-reply-continuity.js +14 -5
- package/runtime/core/second-nature/orchestrator/intent-planner.js +15 -0
- package/runtime/core/second-nature/runtime/service-entry.d.ts +3 -0
- package/runtime/core/second-nature/runtime/service-entry.js +1 -2
- package/runtime/dream/dream-engine.d.ts +14 -0
- package/runtime/dream/dream-engine.js +306 -0
- package/runtime/dream/dream-input-loader.d.ts +37 -0
- package/runtime/dream/dream-input-loader.js +155 -0
- package/runtime/dream/dream-scheduler.d.ts +75 -0
- package/runtime/dream/dream-scheduler.js +131 -0
- package/runtime/dream/index.d.ts +16 -0
- package/runtime/dream/index.js +14 -0
- package/runtime/dream/insight-extractor.d.ts +32 -0
- package/runtime/dream/insight-extractor.js +135 -0
- package/runtime/dream/memory-consolidator.d.ts +45 -0
- package/runtime/dream/memory-consolidator.js +140 -0
- package/runtime/dream/narrative-update-proposal.d.ts +34 -0
- package/runtime/dream/narrative-update-proposal.js +83 -0
- package/runtime/dream/output-validator.d.ts +20 -0
- package/runtime/dream/output-validator.js +110 -0
- package/runtime/dream/redaction-gate.d.ts +31 -0
- package/runtime/dream/redaction-gate.js +109 -0
- package/runtime/dream/relationship-update-proposal.d.ts +27 -0
- package/runtime/dream/relationship-update-proposal.js +119 -0
- package/runtime/dream/sampler.d.ts +30 -0
- package/runtime/dream/sampler.js +65 -0
- package/runtime/dream/types.d.ts +187 -0
- package/runtime/dream/types.js +11 -0
- package/runtime/guidance/fallback.js +6 -3
- package/runtime/guidance/guidance-assembler.js +5 -3
- package/runtime/guidance/output-guard.d.ts +4 -1
- package/runtime/guidance/output-guard.js +24 -0
- package/runtime/guidance/template-registry.d.ts +5 -1
- package/runtime/guidance/template-registry.js +71 -30
- package/runtime/guidance/types.d.ts +14 -0
- package/runtime/observability/projections/guidance-audit.js +4 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dream Scheduler
|
|
3
|
+
*
|
|
4
|
+
* Core logic: trigger Dream runs via cron schedule, evidence threshold, or manual
|
|
5
|
+
* request. Uses DreamRunLock to prevent concurrent runs on the same workspace/input
|
|
6
|
+
* window. Operator timeout is enforced via options in the engine call.
|
|
7
|
+
*
|
|
8
|
+
* - Cron: simplified — checks if a scheduled window is due (last run + interval).
|
|
9
|
+
* - Evidence threshold: triggers when evidence count exceeds threshold since last run.
|
|
10
|
+
* - Manual: always allowed if no active lock.
|
|
11
|
+
* - Lock: in-memory or port-backed; released after run completes or times out.
|
|
12
|
+
* Test coverage: tests/integration/dream/t7-1-2-dream-scheduler.test.ts
|
|
13
|
+
*/
|
|
14
|
+
import { runDream } from "./dream-engine.js";
|
|
15
|
+
const DEFAULT_LOCK_TTL_MS = 35 * 60 * 1000; // 35 min (covers 30 min operator timeout + buffer)
|
|
16
|
+
// In-memory lock fallback when no lockPort is provided
|
|
17
|
+
export function memoryLockPort() {
|
|
18
|
+
const locks = new Map();
|
|
19
|
+
return {
|
|
20
|
+
async acquireLock(input) {
|
|
21
|
+
const key = input.windowKey;
|
|
22
|
+
const existing = locks.get(key);
|
|
23
|
+
if (existing && existing.expiresAt > Date.now()) {
|
|
24
|
+
return { acquired: false, existingRunId: existing.runId };
|
|
25
|
+
}
|
|
26
|
+
locks.set(key, {
|
|
27
|
+
runId: input.runId,
|
|
28
|
+
expiresAt: Date.now() + input.ttlMs,
|
|
29
|
+
});
|
|
30
|
+
return { acquired: true };
|
|
31
|
+
},
|
|
32
|
+
async releaseLock(input) {
|
|
33
|
+
const key = input.windowKey;
|
|
34
|
+
const existing = locks.get(key);
|
|
35
|
+
if (existing && existing.runId === input.runId) {
|
|
36
|
+
locks.delete(key);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export async function scheduleDream(input) {
|
|
42
|
+
const lock = input.lockPort ?? memoryLockPort();
|
|
43
|
+
const windowKey = input.windowKey ?? "dream_lock:default";
|
|
44
|
+
// Acquire lock
|
|
45
|
+
const lockResult = await lock.acquireLock({
|
|
46
|
+
runId: input.runId,
|
|
47
|
+
windowKey,
|
|
48
|
+
ttlMs: DEFAULT_LOCK_TTL_MS,
|
|
49
|
+
});
|
|
50
|
+
if (!lockResult.acquired) {
|
|
51
|
+
const reason = input.triggerKind === "quiet_completion"
|
|
52
|
+
? "skip:lock_held"
|
|
53
|
+
: `lock_held_by:${lockResult.existingRunId ?? "unknown"}`;
|
|
54
|
+
return {
|
|
55
|
+
runId: input.runId,
|
|
56
|
+
status: "skipped",
|
|
57
|
+
reason,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// Run Dream asynchronously; do not await so heartbeat is not blocked
|
|
61
|
+
runDream({
|
|
62
|
+
runId: input.runId,
|
|
63
|
+
traceId: input.traceId,
|
|
64
|
+
triggerKind: input.triggerKind,
|
|
65
|
+
statePort: input.statePort,
|
|
66
|
+
modelPort: input.modelPort,
|
|
67
|
+
modelAssistPort: input.modelAssistPort,
|
|
68
|
+
tracePort: input.tracePort,
|
|
69
|
+
budgetPort: input.budgetPort,
|
|
70
|
+
options: input.options,
|
|
71
|
+
})
|
|
72
|
+
.then(async (result) => {
|
|
73
|
+
await lock.releaseLock({ runId: input.runId, windowKey });
|
|
74
|
+
return result;
|
|
75
|
+
})
|
|
76
|
+
.catch(async (err) => {
|
|
77
|
+
console.error("[dream-scheduler] runDream failed:", err);
|
|
78
|
+
await lock.releaseLock({ runId: input.runId, windowKey });
|
|
79
|
+
});
|
|
80
|
+
return {
|
|
81
|
+
runId: input.runId,
|
|
82
|
+
status: "started",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function isHourInWindow(hour, start, end) {
|
|
86
|
+
if (start < end) {
|
|
87
|
+
return hour >= start && hour < end;
|
|
88
|
+
}
|
|
89
|
+
return hour >= start || hour < end;
|
|
90
|
+
}
|
|
91
|
+
export function shouldTrigger(policy) {
|
|
92
|
+
switch (policy.type) {
|
|
93
|
+
case "cron": {
|
|
94
|
+
if (!policy.lastRunAt) {
|
|
95
|
+
return { shouldRun: true, reason: "first_run" };
|
|
96
|
+
}
|
|
97
|
+
const last = new Date(policy.lastRunAt).getTime();
|
|
98
|
+
const next = last + policy.intervalHours * 60 * 60 * 1000;
|
|
99
|
+
if (Date.now() >= next) {
|
|
100
|
+
return { shouldRun: true, reason: "cron_due" };
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
shouldRun: false,
|
|
104
|
+
reason: `next_run_at:${new Date(next).toISOString()}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
case "evidence_threshold": {
|
|
108
|
+
const delta = policy.currentEvidenceCount - policy.lastRunEvidenceCount;
|
|
109
|
+
if (delta >= policy.threshold) {
|
|
110
|
+
return {
|
|
111
|
+
shouldRun: true,
|
|
112
|
+
reason: `threshold_reached:${delta}/${policy.threshold}`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
shouldRun: false,
|
|
117
|
+
reason: `below_threshold:${delta}/${policy.threshold}`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
case "manual": {
|
|
121
|
+
return { shouldRun: true, reason: "manual_trigger" };
|
|
122
|
+
}
|
|
123
|
+
case "quiet_completion": {
|
|
124
|
+
const hour = new Date(policy.quietCompletedAt).getUTCHours();
|
|
125
|
+
if (isHourInWindow(hour, policy.windowStartHour, policy.windowEndHour)) {
|
|
126
|
+
return { shouldRun: true, reason: "quiet_completion_in_window" };
|
|
127
|
+
}
|
|
128
|
+
return { shouldRun: false, reason: "skip:out_of_window" };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dream System public API.
|
|
3
|
+
*/
|
|
4
|
+
export * from "./types.js";
|
|
5
|
+
export { consolidateMemory } from "./memory-consolidator.js";
|
|
6
|
+
export { sampleDreamInput } from "./sampler.js";
|
|
7
|
+
export { redactDreamInput, redactBundle } from "./redaction-gate.js";
|
|
8
|
+
export { validateDreamOutput } from "./output-validator.js";
|
|
9
|
+
export { runDream } from "./dream-engine.js";
|
|
10
|
+
export { scheduleDream, shouldTrigger, memoryLockPort } from "./dream-scheduler.js";
|
|
11
|
+
export type { SchedulerInput, DreamRunLockPort, ScheduleResult, CronPolicy, EvidenceThresholdPolicy, ManualPolicy, QuietCompletionPolicy, TriggerPolicy } from "./dream-scheduler.js";
|
|
12
|
+
export { extractInsights } from "./insight-extractor.js";
|
|
13
|
+
export { draftNarrativeFromDream } from "./narrative-update-proposal.js";
|
|
14
|
+
export { draftRelationshipFromDream } from "./relationship-update-proposal.js";
|
|
15
|
+
export { createDreamInputLoader } from "./dream-input-loader.js";
|
|
16
|
+
export type { DreamInputLoaderOptions } from "./dream-input-loader.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dream System public API.
|
|
3
|
+
*/
|
|
4
|
+
export * from "./types.js";
|
|
5
|
+
export { consolidateMemory } from "./memory-consolidator.js";
|
|
6
|
+
export { sampleDreamInput } from "./sampler.js";
|
|
7
|
+
export { redactDreamInput, redactBundle } from "./redaction-gate.js";
|
|
8
|
+
export { validateDreamOutput } from "./output-validator.js";
|
|
9
|
+
export { runDream } from "./dream-engine.js";
|
|
10
|
+
export { scheduleDream, shouldTrigger, memoryLockPort } from "./dream-scheduler.js";
|
|
11
|
+
export { extractInsights } from "./insight-extractor.js";
|
|
12
|
+
export { draftNarrativeFromDream } from "./narrative-update-proposal.js";
|
|
13
|
+
export { draftRelationshipFromDream } from "./relationship-update-proposal.js";
|
|
14
|
+
export { createDreamInputLoader } from "./dream-input-loader.js";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Insight Extractor
|
|
3
|
+
*
|
|
4
|
+
* Core logic: extract source-grounded insight candidates from sampled evidence.
|
|
5
|
+
* Rules-based (no LLM required) using keyword patterns, frequency counts, and
|
|
6
|
+
* temporal clustering. Each insight carries type, summary, sourceRefs, and confidence.
|
|
7
|
+
*
|
|
8
|
+
* - Pattern: recurring themes across multiple evidence entries.
|
|
9
|
+
* - Learning: new skills or behaviors observed over time.
|
|
10
|
+
* - Observation: one-off notable events.
|
|
11
|
+
* - Conflict: contradictory claims or repeated failures.
|
|
12
|
+
* Test coverage: tests/unit/dream/t7-1-3-insight-extraction.test.ts
|
|
13
|
+
*/
|
|
14
|
+
import type { DreamInsight } from "./types.js";
|
|
15
|
+
export interface ExtractInsightsInput {
|
|
16
|
+
evidenceSummaries: Array<{
|
|
17
|
+
id: string;
|
|
18
|
+
summary: string;
|
|
19
|
+
createdAt: string;
|
|
20
|
+
kind?: string;
|
|
21
|
+
}>;
|
|
22
|
+
chronicleSummaries: Array<{
|
|
23
|
+
id: string;
|
|
24
|
+
summary: string;
|
|
25
|
+
createdAt: string;
|
|
26
|
+
}>;
|
|
27
|
+
redacted?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare function extractInsights(input: ExtractInsightsInput): {
|
|
30
|
+
insights: DreamInsight[];
|
|
31
|
+
unsupportedClaims: string[];
|
|
32
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Insight Extractor
|
|
3
|
+
*
|
|
4
|
+
* Core logic: extract source-grounded insight candidates from sampled evidence.
|
|
5
|
+
* Rules-based (no LLM required) using keyword patterns, frequency counts, and
|
|
6
|
+
* temporal clustering. Each insight carries type, summary, sourceRefs, and confidence.
|
|
7
|
+
*
|
|
8
|
+
* - Pattern: recurring themes across multiple evidence entries.
|
|
9
|
+
* - Learning: new skills or behaviors observed over time.
|
|
10
|
+
* - Observation: one-off notable events.
|
|
11
|
+
* - Conflict: contradictory claims or repeated failures.
|
|
12
|
+
* Test coverage: tests/unit/dream/t7-1-3-insight-extraction.test.ts
|
|
13
|
+
*/
|
|
14
|
+
// Keywords that indicate learning/development
|
|
15
|
+
const LEARNING_KEYWORDS = [
|
|
16
|
+
"learned", "learn", "figured out", "discovered", "understood",
|
|
17
|
+
"new approach", "different way", "better method", "improved",
|
|
18
|
+
];
|
|
19
|
+
// Keywords that indicate recurring patterns
|
|
20
|
+
const PATTERN_KEYWORDS = [
|
|
21
|
+
"again", "regularly", "routine", "habit", "consistently",
|
|
22
|
+
"every", "frequently", "pattern", "trend",
|
|
23
|
+
];
|
|
24
|
+
// Keywords that indicate conflict or failure
|
|
25
|
+
const CONFLICT_KEYWORDS = [
|
|
26
|
+
"failed", "failure", "error", "bug", "broke", "broken",
|
|
27
|
+
"conflict", "contradict", "disagree", "mismatch", "unexpected",
|
|
28
|
+
"not working", "does not work", "issue", "problem",
|
|
29
|
+
];
|
|
30
|
+
function countKeywordMatches(text, keywords) {
|
|
31
|
+
const lower = text.toLowerCase();
|
|
32
|
+
return keywords.reduce((count, kw) => {
|
|
33
|
+
const regex = new RegExp(kw.replace(/\s+/g, "\\s+"), "g");
|
|
34
|
+
const matches = lower.match(regex);
|
|
35
|
+
return count + (matches ? matches.length : 0);
|
|
36
|
+
}, 0);
|
|
37
|
+
}
|
|
38
|
+
function groupByDay(items) {
|
|
39
|
+
const groups = new Map();
|
|
40
|
+
for (const item of items) {
|
|
41
|
+
const day = item.createdAt.slice(0, 10);
|
|
42
|
+
const existing = groups.get(day) ?? [];
|
|
43
|
+
existing.push(item.summary);
|
|
44
|
+
groups.set(day, existing);
|
|
45
|
+
}
|
|
46
|
+
return groups;
|
|
47
|
+
}
|
|
48
|
+
function findRecurringThemes(items) {
|
|
49
|
+
const themeCounts = new Map();
|
|
50
|
+
for (const item of items) {
|
|
51
|
+
const words = item.summary.toLowerCase().split(/\s+/).filter((w) => w.length > 4);
|
|
52
|
+
for (const word of words) {
|
|
53
|
+
const entry = themeCounts.get(word) ?? { count: 0, sourceIds: [] };
|
|
54
|
+
entry.count++;
|
|
55
|
+
if (!entry.sourceIds.includes(item.id)) {
|
|
56
|
+
entry.sourceIds.push(item.id);
|
|
57
|
+
}
|
|
58
|
+
themeCounts.set(word, entry);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return Array.from(themeCounts.entries())
|
|
62
|
+
.filter(([, v]) => v.count >= 3 && v.sourceIds.length >= 2)
|
|
63
|
+
.map(([theme, v]) => ({ theme, count: v.count, sourceIds: v.sourceIds }))
|
|
64
|
+
.sort((a, b) => b.count - a.count)
|
|
65
|
+
.slice(0, 5);
|
|
66
|
+
}
|
|
67
|
+
export function extractInsights(input) {
|
|
68
|
+
const allItems = [
|
|
69
|
+
...input.evidenceSummaries.map((e) => ({ ...e, origin: "evidence" })),
|
|
70
|
+
...input.chronicleSummaries.map((c) => ({ ...c, origin: "chronicle" })),
|
|
71
|
+
];
|
|
72
|
+
const insights = [];
|
|
73
|
+
const unsupportedClaims = [];
|
|
74
|
+
if (allItems.length === 0) {
|
|
75
|
+
return { insights: [], unsupportedClaims: ["no_evidence_for_insight"] };
|
|
76
|
+
}
|
|
77
|
+
// 1. Pattern insights — recurring themes
|
|
78
|
+
const themes = findRecurringThemes(allItems);
|
|
79
|
+
for (const theme of themes) {
|
|
80
|
+
insights.push({
|
|
81
|
+
id: `insight:pattern:${theme.theme}:${crypto.randomUUID()}`,
|
|
82
|
+
type: "pattern",
|
|
83
|
+
summary: `Recurring theme "${theme.theme}" observed across ${theme.sourceIds.length} entries`,
|
|
84
|
+
sourceRefs: theme.sourceIds.slice(0, 10),
|
|
85
|
+
confidence: Math.min(0.95, 0.5 + theme.count * 0.05),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
// 2. Learning insights — entries with learning keywords
|
|
89
|
+
const learningItems = allItems.filter((item) => countKeywordMatches(item.summary, LEARNING_KEYWORDS) > 0);
|
|
90
|
+
if (learningItems.length > 0) {
|
|
91
|
+
insights.push({
|
|
92
|
+
id: `insight:learning:${crypto.randomUUID()}`,
|
|
93
|
+
type: "learning",
|
|
94
|
+
summary: `Learning observed in ${learningItems.length} entries: ${learningItems[0].summary.slice(0, 60)}...`,
|
|
95
|
+
sourceRefs: learningItems.map((i) => i.id).slice(0, 10),
|
|
96
|
+
confidence: Math.min(0.9, 0.4 + learningItems.length * 0.1),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
// 3. Conflict insights — entries with conflict/failure keywords
|
|
100
|
+
const conflictItems = allItems.filter((item) => countKeywordMatches(item.summary, CONFLICT_KEYWORDS) > 0);
|
|
101
|
+
if (conflictItems.length >= 2) {
|
|
102
|
+
insights.push({
|
|
103
|
+
id: `insight:conflict:${crypto.randomUUID()}`,
|
|
104
|
+
type: "conflict",
|
|
105
|
+
summary: `Repeated issues observed in ${conflictItems.length} entries`,
|
|
106
|
+
sourceRefs: conflictItems.map((i) => i.id).slice(0, 10),
|
|
107
|
+
confidence: Math.min(0.85, 0.5 + conflictItems.length * 0.05),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
// 4. Observation insights — notable one-off events (high-activity days)
|
|
111
|
+
const byDay = groupByDay(allItems);
|
|
112
|
+
let maxDay = "";
|
|
113
|
+
let maxCount = 0;
|
|
114
|
+
for (const [day, summaries] of byDay) {
|
|
115
|
+
if (summaries.length > maxCount) {
|
|
116
|
+
maxCount = summaries.length;
|
|
117
|
+
maxDay = day;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (maxCount >= 3 && maxDay) {
|
|
121
|
+
const dayItems = allItems.filter((i) => i.createdAt.startsWith(maxDay));
|
|
122
|
+
insights.push({
|
|
123
|
+
id: `insight:observation:${crypto.randomUUID()}`,
|
|
124
|
+
type: "observation",
|
|
125
|
+
summary: `High activity day (${maxDay}): ${maxCount} notable events`,
|
|
126
|
+
sourceRefs: dayItems.map((i) => i.id).slice(0, 10),
|
|
127
|
+
confidence: Math.min(0.8, 0.4 + maxCount * 0.05),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
// If no insights extracted, record unsupported claim
|
|
131
|
+
if (insights.length === 0) {
|
|
132
|
+
unsupportedClaims.push("no_insight_patterns_detected");
|
|
133
|
+
}
|
|
134
|
+
return { insights, unsupportedClaims };
|
|
135
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rules-based memory consolidation.
|
|
3
|
+
*
|
|
4
|
+
* Core logic: dedupe, merge, stale cleanup, and conflict marking on evidence,
|
|
5
|
+
* chronicle, and existing memory entries. No LLM required.
|
|
6
|
+
*
|
|
7
|
+
* - Deduplicate by sourceRef id + kind; keep the most recent.
|
|
8
|
+
* - Merge entries with same kind + similar summary (naive prefix match).
|
|
9
|
+
* - Mark entries older than 90 days as stale (retain but flag).
|
|
10
|
+
* - Mark entries with conflicting sourceRefs as conflict.
|
|
11
|
+
* Test coverage: tests/integration/dream/t7-1-1-dream-pipeline.test.ts
|
|
12
|
+
*/
|
|
13
|
+
import type { CanonicalMemoryEntry, SourceRef } from "../storage/memory-store/memory-store-lifecycle.js";
|
|
14
|
+
export interface ConsolidationInput {
|
|
15
|
+
evidenceSummaries: Array<{
|
|
16
|
+
id: string;
|
|
17
|
+
summary: string;
|
|
18
|
+
sourceRefs: SourceRef[];
|
|
19
|
+
createdAt: string;
|
|
20
|
+
sensitivity?: string;
|
|
21
|
+
}>;
|
|
22
|
+
chronicleSummaries: Array<{
|
|
23
|
+
id: string;
|
|
24
|
+
summary: string;
|
|
25
|
+
sourceRefs: SourceRef[];
|
|
26
|
+
createdAt: string;
|
|
27
|
+
}>;
|
|
28
|
+
toolExperienceSummaries?: Array<{
|
|
29
|
+
id: string;
|
|
30
|
+
summary: string;
|
|
31
|
+
sourceRefs: SourceRef[];
|
|
32
|
+
createdAt: string;
|
|
33
|
+
}>;
|
|
34
|
+
existingEntries: CanonicalMemoryEntry[];
|
|
35
|
+
}
|
|
36
|
+
export interface ConsolidationResult {
|
|
37
|
+
entries: CanonicalMemoryEntry[];
|
|
38
|
+
conflicts: Array<{
|
|
39
|
+
entryId: string;
|
|
40
|
+
reason: string;
|
|
41
|
+
}>;
|
|
42
|
+
staleCount: number;
|
|
43
|
+
dedupeCount: number;
|
|
44
|
+
}
|
|
45
|
+
export declare function consolidateMemory(input: ConsolidationInput): ConsolidationResult;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rules-based memory consolidation.
|
|
3
|
+
*
|
|
4
|
+
* Core logic: dedupe, merge, stale cleanup, and conflict marking on evidence,
|
|
5
|
+
* chronicle, and existing memory entries. No LLM required.
|
|
6
|
+
*
|
|
7
|
+
* - Deduplicate by sourceRef id + kind; keep the most recent.
|
|
8
|
+
* - Merge entries with same kind + similar summary (naive prefix match).
|
|
9
|
+
* - Mark entries older than 90 days as stale (retain but flag).
|
|
10
|
+
* - Mark entries with conflicting sourceRefs as conflict.
|
|
11
|
+
* Test coverage: tests/integration/dream/t7-1-1-dream-pipeline.test.ts
|
|
12
|
+
*/
|
|
13
|
+
const STALE_DAYS = 90;
|
|
14
|
+
function isStale(createdAt) {
|
|
15
|
+
const then = new Date(createdAt).getTime();
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
return now - then > STALE_DAYS * 24 * 60 * 60 * 1000;
|
|
18
|
+
}
|
|
19
|
+
function keyForSourceRefs(refs) {
|
|
20
|
+
return refs
|
|
21
|
+
.map((r) => `${r.sourceId}:${r.kind}`)
|
|
22
|
+
.sort()
|
|
23
|
+
.join("|");
|
|
24
|
+
}
|
|
25
|
+
function summariesSimilar(a, b) {
|
|
26
|
+
const norm = (s) => s
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.replace(/[^a-z0-9\s]/g, "")
|
|
29
|
+
.split(/\s+/)
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.slice(0, 5)
|
|
32
|
+
.join(" ");
|
|
33
|
+
return norm(a) === norm(b);
|
|
34
|
+
}
|
|
35
|
+
export function consolidateMemory(input) {
|
|
36
|
+
const allRaw = [
|
|
37
|
+
...input.evidenceSummaries.map((e) => ({
|
|
38
|
+
...e,
|
|
39
|
+
origin: "evidence",
|
|
40
|
+
})),
|
|
41
|
+
...input.chronicleSummaries.map((c) => ({
|
|
42
|
+
...c,
|
|
43
|
+
origin: "chronicle",
|
|
44
|
+
})),
|
|
45
|
+
...(input.toolExperienceSummaries ?? []).map((t) => ({
|
|
46
|
+
...t,
|
|
47
|
+
origin: "tool_experience",
|
|
48
|
+
})),
|
|
49
|
+
...input.existingEntries.map((e) => ({
|
|
50
|
+
id: e.entryId,
|
|
51
|
+
summary: e.summary,
|
|
52
|
+
sourceRefs: e.sourceRefs,
|
|
53
|
+
createdAt: e.createdAt,
|
|
54
|
+
origin: "memory",
|
|
55
|
+
})),
|
|
56
|
+
];
|
|
57
|
+
// 1. Deduplicate by sourceRef key, keep most recent
|
|
58
|
+
const bySourceKey = new Map();
|
|
59
|
+
for (const item of allRaw) {
|
|
60
|
+
const key = keyForSourceRefs(item.sourceRefs);
|
|
61
|
+
const existing = bySourceKey.get(key);
|
|
62
|
+
if (!existing || item.createdAt > existing.createdAt) {
|
|
63
|
+
bySourceKey.set(key, item);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const deduped = Array.from(bySourceKey.values());
|
|
67
|
+
const dedupeCount = allRaw.length - deduped.length;
|
|
68
|
+
// 2. Merge similar summaries
|
|
69
|
+
const merged = [];
|
|
70
|
+
const used = new Set();
|
|
71
|
+
for (const item of deduped) {
|
|
72
|
+
if (used.has(item.id))
|
|
73
|
+
continue;
|
|
74
|
+
const group = [item];
|
|
75
|
+
for (const other of deduped) {
|
|
76
|
+
if (other.id === item.id || used.has(other.id))
|
|
77
|
+
continue;
|
|
78
|
+
if (summariesSimilar(item.summary, other.summary)) {
|
|
79
|
+
group.push(other);
|
|
80
|
+
used.add(other.id);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
used.add(item.id);
|
|
84
|
+
const mergedRefs = [];
|
|
85
|
+
const seenRefKeys = new Set();
|
|
86
|
+
for (const g of group) {
|
|
87
|
+
for (const ref of g.sourceRefs) {
|
|
88
|
+
const rk = `${ref.sourceId}:${ref.kind}`;
|
|
89
|
+
if (!seenRefKeys.has(rk)) {
|
|
90
|
+
seenRefKeys.add(rk);
|
|
91
|
+
mergedRefs.push(ref);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const latestCreatedAt = group
|
|
96
|
+
.map((g) => g.createdAt)
|
|
97
|
+
.sort()
|
|
98
|
+
.at(-1);
|
|
99
|
+
merged.push({
|
|
100
|
+
entryId: `consolidated:${crypto.randomUUID()}`,
|
|
101
|
+
kind: group[0].origin,
|
|
102
|
+
summary: group[0].summary,
|
|
103
|
+
sourceRefs: mergedRefs,
|
|
104
|
+
createdAt: latestCreatedAt,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// 3. Stale marking
|
|
108
|
+
let staleCount = 0;
|
|
109
|
+
for (const entry of merged) {
|
|
110
|
+
if (isStale(entry.createdAt)) {
|
|
111
|
+
staleCount++;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// 4. Conflict detection: entries with overlapping sourceRefs but different summaries
|
|
115
|
+
const conflicts = [];
|
|
116
|
+
for (let i = 0; i < merged.length; i++) {
|
|
117
|
+
for (let j = i + 1; j < merged.length; j++) {
|
|
118
|
+
const a = merged[i];
|
|
119
|
+
const b = merged[j];
|
|
120
|
+
const aKeys = new Set(a.sourceRefs.map((r) => `${r.sourceId}:${r.kind}`));
|
|
121
|
+
const overlap = b.sourceRefs.some((r) => aKeys.has(`${r.sourceId}:${r.kind}`));
|
|
122
|
+
if (overlap && !summariesSimilar(a.summary, b.summary)) {
|
|
123
|
+
conflicts.push({
|
|
124
|
+
entryId: a.entryId,
|
|
125
|
+
reason: `conflict_with:${b.entryId}`,
|
|
126
|
+
});
|
|
127
|
+
conflicts.push({
|
|
128
|
+
entryId: b.entryId,
|
|
129
|
+
reason: `conflict_with:${a.entryId}`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
entries: merged,
|
|
136
|
+
conflicts,
|
|
137
|
+
staleCount,
|
|
138
|
+
dedupeCount,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrative Update Proposal
|
|
3
|
+
*
|
|
4
|
+
* Core logic: generate a narrative update proposal based on evidence and
|
|
5
|
+
* extracted insights. Claims must be source-backed; unsupported claims are
|
|
6
|
+
* flagged and the proposal is degraded or blocked.
|
|
7
|
+
*
|
|
8
|
+
* - Focus: most prominent theme from insights or evidence.
|
|
9
|
+
* - Progress: learning and observation insights mapped to progress entries.
|
|
10
|
+
* - NextIntent: inferred from unresolved conflicts or high-priority patterns.
|
|
11
|
+
* Test coverage: tests/unit/dream/t7-1-4-narrative-update.test.ts
|
|
12
|
+
*/
|
|
13
|
+
import type { DreamNarrativeUpdate } from "./types.js";
|
|
14
|
+
export interface NarrativeProposalInput {
|
|
15
|
+
evidenceSummaries: Array<{
|
|
16
|
+
id: string;
|
|
17
|
+
summary: string;
|
|
18
|
+
createdAt: string;
|
|
19
|
+
}>;
|
|
20
|
+
insights: Array<{
|
|
21
|
+
id: string;
|
|
22
|
+
type: "pattern" | "learning" | "observation" | "conflict";
|
|
23
|
+
summary: string;
|
|
24
|
+
sourceRefs: string[];
|
|
25
|
+
confidence: number;
|
|
26
|
+
}>;
|
|
27
|
+
priorFocus?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface NarrativeProposalResult {
|
|
30
|
+
proposal?: DreamNarrativeUpdate;
|
|
31
|
+
unsupportedClaims: string[];
|
|
32
|
+
blocked: boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare function draftNarrativeFromDream(input: NarrativeProposalInput): NarrativeProposalResult;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrative Update Proposal
|
|
3
|
+
*
|
|
4
|
+
* Core logic: generate a narrative update proposal based on evidence and
|
|
5
|
+
* extracted insights. Claims must be source-backed; unsupported claims are
|
|
6
|
+
* flagged and the proposal is degraded or blocked.
|
|
7
|
+
*
|
|
8
|
+
* - Focus: most prominent theme from insights or evidence.
|
|
9
|
+
* - Progress: learning and observation insights mapped to progress entries.
|
|
10
|
+
* - NextIntent: inferred from unresolved conflicts or high-priority patterns.
|
|
11
|
+
* Test coverage: tests/unit/dream/t7-1-4-narrative-update.test.ts
|
|
12
|
+
*/
|
|
13
|
+
export function draftNarrativeFromDream(input) {
|
|
14
|
+
const unsupportedClaims = [];
|
|
15
|
+
if (input.evidenceSummaries.length === 0 && input.insights.length === 0) {
|
|
16
|
+
return {
|
|
17
|
+
unsupportedClaims: ["no_evidence_for_narrative"],
|
|
18
|
+
blocked: true,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
// Focus: highest-confidence insight summary, or first evidence if no insights
|
|
22
|
+
let focus = input.priorFocus ?? "continue exploration";
|
|
23
|
+
const highConfidenceInsights = input.insights.filter((i) => i.confidence >= 0.6);
|
|
24
|
+
if (highConfidenceInsights.length > 0) {
|
|
25
|
+
// Pick the insight with highest confidence
|
|
26
|
+
const top = highConfidenceInsights.sort((a, b) => b.confidence - a.confidence)[0];
|
|
27
|
+
focus = top.summary.slice(0, 120);
|
|
28
|
+
}
|
|
29
|
+
else if (input.evidenceSummaries.length > 0) {
|
|
30
|
+
focus = input.evidenceSummaries[0].summary.slice(0, 120);
|
|
31
|
+
}
|
|
32
|
+
// Progress: learning + observation insights become progress entries
|
|
33
|
+
const progressAdditions = [];
|
|
34
|
+
for (const insight of input.insights) {
|
|
35
|
+
if (insight.type === "learning" || insight.type === "observation") {
|
|
36
|
+
progressAdditions.push(insight.summary.slice(0, 200));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (progressAdditions.length === 0 && input.evidenceSummaries.length > 0) {
|
|
40
|
+
// Fallback: use most recent evidence as progress
|
|
41
|
+
const recent = [...input.evidenceSummaries].sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0];
|
|
42
|
+
if (recent) {
|
|
43
|
+
progressAdditions.push(recent.summary.slice(0, 200));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// NextIntent: if conflicts exist, intent is to resolve; otherwise continue
|
|
47
|
+
const hasConflict = input.insights.some((i) => i.type === "conflict");
|
|
48
|
+
const nextIntent = hasConflict
|
|
49
|
+
? "resolve_conflicts_and_validate"
|
|
50
|
+
: "continue_current_focus";
|
|
51
|
+
// Source refs: collect all insight source refs + evidence ids
|
|
52
|
+
const sourceRefs = new Set();
|
|
53
|
+
for (const insight of input.insights) {
|
|
54
|
+
for (const ref of insight.sourceRefs) {
|
|
55
|
+
sourceRefs.add(ref);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const ev of input.evidenceSummaries) {
|
|
59
|
+
sourceRefs.add(ev.id);
|
|
60
|
+
}
|
|
61
|
+
// Confidence: average of insight confidences, or 0.5 fallback
|
|
62
|
+
const avgConfidence = input.insights.length > 0
|
|
63
|
+
? input.insights.reduce((sum, i) => sum + i.confidence, 0) /
|
|
64
|
+
input.insights.length
|
|
65
|
+
: 0.5;
|
|
66
|
+
// Degrade if confidence too low
|
|
67
|
+
if (avgConfidence < 0.3) {
|
|
68
|
+
unsupportedClaims.push("low_average_confidence_for_narrative");
|
|
69
|
+
}
|
|
70
|
+
const blocked = unsupportedClaims.length > 0 && avgConfidence < 0.2;
|
|
71
|
+
return {
|
|
72
|
+
proposal: {
|
|
73
|
+
focus,
|
|
74
|
+
progressAdditions: progressAdditions.slice(0, 20),
|
|
75
|
+
nextIntent,
|
|
76
|
+
confidenceDelta: Number(avgConfidence.toFixed(2)),
|
|
77
|
+
sourceRefs: Array.from(sourceRefs).slice(0, 50),
|
|
78
|
+
unsupportedClaims,
|
|
79
|
+
},
|
|
80
|
+
unsupportedClaims,
|
|
81
|
+
blocked,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dream output validator.
|
|
3
|
+
*
|
|
4
|
+
* Core logic: schema, source grounding, sensitivity, and unsupported claim
|
|
5
|
+
* checks on candidate DreamOutput. Decides accepted eligibility or archive reason.
|
|
6
|
+
* Test coverage: tests/integration/dream/t7-1-1-dream-pipeline.test.ts
|
|
7
|
+
*/
|
|
8
|
+
import type { DreamOutput, DreamOutputValidation } from "./types.js";
|
|
9
|
+
export interface ValidationInput {
|
|
10
|
+
output: DreamOutput;
|
|
11
|
+
inputEvidenceIds: string[];
|
|
12
|
+
inputChronicleIds: string[];
|
|
13
|
+
inputToolExperienceIds?: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface ValidationResult {
|
|
16
|
+
eligible: boolean;
|
|
17
|
+
validation: DreamOutputValidation;
|
|
18
|
+
archiveReasons: string[];
|
|
19
|
+
}
|
|
20
|
+
export declare function validateDreamOutput(input: ValidationInput): ValidationResult;
|