mcp-council-server 4.0.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/README.md +645 -0
- package/package.json +54 -0
- package/src/analysis/parser.js +92 -0
- package/src/config.js +26 -0
- package/src/db/database.js +12 -0
- package/src/db/json-store.js +122 -0
- package/src/db/migrations.js +16 -0
- package/src/index.js +28 -0
- package/src/memory/decision.js +37 -0
- package/src/memory/phase.js +82 -0
- package/src/memory/recall.js +108 -0
- package/src/memory/session.js +80 -0
- package/src/prinsip/adaptive.js +139 -0
- package/src/prinsip/agentic.js +138 -0
- package/src/prinsip/transparency.js +87 -0
- package/src/score/agentic.js +28 -0
- package/src/score/completeness.js +76 -0
- package/src/score/consistency.js +15 -0
- package/src/score/index.js +101 -0
- package/src/score/transparency.js +51 -0
- package/src/tools.js +556 -0
- package/src/verify/contradiction.js +125 -0
- package/src/verify/grounding.js +30 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const STOPWORDS = new Set([
|
|
2
|
+
'dan','di','ke','dari','yang','ini','itu','dengan','untuk','pada',
|
|
3
|
+
'adalah','akan','dapat','telah','sudah','bisa','juga','atau','tidak',
|
|
4
|
+
'the','and','for','with','that','this','from','are','have','has','had',
|
|
5
|
+
'but','not','you','all','can','was','were','been','being','does','done',
|
|
6
|
+
'get','got','may','say','said','make','made','like','just','more','some',
|
|
7
|
+
'there','their','they','what','when','where','which','who','whom','why',
|
|
8
|
+
'how','much','many','very','too','so','if','then','than','about','into',
|
|
9
|
+
'over','after','before','between','under','again','further','once','here',
|
|
10
|
+
'there','each','few','both','same','own','saya','kami','kita','dia',
|
|
11
|
+
'mereka','saja','pun','masih','belum','pernah','sedang','telah',
|
|
12
|
+
'bakal','hendak','mau','ingin','harus','boleh','mungkin','akan',
|
|
13
|
+
'selalu','sering','jarang','baru','lalu','kemudian','sekarang'
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
export function parseTaskStructure(task) {
|
|
17
|
+
const lines = task.split('\n');
|
|
18
|
+
const sentences = task.split(/[.!?]+/).filter(s => s.trim().length > 0);
|
|
19
|
+
|
|
20
|
+
const structure = {
|
|
21
|
+
sentenceCount: sentences.length,
|
|
22
|
+
bulletPoints: 0,
|
|
23
|
+
numberedPoints: 0,
|
|
24
|
+
explicitQuestions: [],
|
|
25
|
+
codeBlocks: 0,
|
|
26
|
+
structuralSections: [],
|
|
27
|
+
keywords: []
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
const trimmed = line.trim();
|
|
32
|
+
|
|
33
|
+
if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
|
|
34
|
+
structure.bulletPoints++;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (/^\d+[\.\)]\s/.test(trimmed)) {
|
|
38
|
+
structure.numberedPoints++;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const headerMatch = trimmed.match(/^#{1,6}\s(.+)/);
|
|
42
|
+
if (headerMatch) {
|
|
43
|
+
structure.structuralSections.push(headerMatch[1]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const sentence of sentences) {
|
|
48
|
+
if (sentence.includes('?')) {
|
|
49
|
+
structure.explicitQuestions.push(sentence.trim());
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const codeMatches = task.match(/```/g);
|
|
54
|
+
structure.codeBlocks = codeMatches ? Math.floor(codeMatches.length / 2) : 0;
|
|
55
|
+
|
|
56
|
+
const words = task.toLowerCase()
|
|
57
|
+
.split(/[\s,.;:!?()\[\]{}<>"'\/\\|=+`~@#$%^&*]+/)
|
|
58
|
+
.filter(w => w.length > 3 && !STOPWORDS.has(w));
|
|
59
|
+
|
|
60
|
+
const freq = {};
|
|
61
|
+
for (const w of words) {
|
|
62
|
+
freq[w] = (freq[w] || 0) + 1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
structure.keywords = Object.entries(freq)
|
|
66
|
+
.sort((a, b) => b[1] - a[1])
|
|
67
|
+
.slice(0, 15)
|
|
68
|
+
.map(([word, count]) => `${word}(${count})`);
|
|
69
|
+
|
|
70
|
+
return structure;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function extractDeliverables(output) {
|
|
74
|
+
if (!output || typeof output !== 'object') return ['output kosong'];
|
|
75
|
+
const keys = Object.keys(output);
|
|
76
|
+
return keys.filter(k => {
|
|
77
|
+
const v = output[k];
|
|
78
|
+
return Array.isArray(v) ? v.length > 0 : Boolean(v);
|
|
79
|
+
}).map(k => `${k}: ${formatPreview(output[k])}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function formatPreview(value) {
|
|
83
|
+
if (Array.isArray(value)) return `${value.length} item`;
|
|
84
|
+
if (typeof value === 'string') return value.length > 60 ? value.slice(0, 60) + '...' : value;
|
|
85
|
+
if (typeof value === 'object') return JSON.stringify(value).slice(0, 60);
|
|
86
|
+
return String(value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function extractUnresolved(output) {
|
|
90
|
+
if (!output?.knowledgeBoundary?.whatIDontKnow) return [];
|
|
91
|
+
return output.knowledgeBoundary.whatIDontKnow;
|
|
92
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
export const CONFIG = {
|
|
5
|
+
DB_PATH: process.env.COUNCIL_DB_PATH || './data/council.db',
|
|
6
|
+
SESSION_TTL_HOURS: parseInt(process.env.COUNCIL_SESSION_TTL_HOURS || '24'),
|
|
7
|
+
|
|
8
|
+
MAX_TASK_LENGTH: parseInt(process.env.COUNCIL_MAX_TASK_LENGTH || '10000'),
|
|
9
|
+
MAX_CONTEXT_LENGTH: parseInt(process.env.COUNCIL_MAX_CONTEXT_LENGTH || '5000'),
|
|
10
|
+
|
|
11
|
+
CONFIDENCE_THRESHOLD: parseFloat(process.env.COUNCIL_CONFIDENCE_THRESHOLD || '0.7'),
|
|
12
|
+
SCORE_PASS_THRESHOLD: parseInt(process.env.COUNCIL_SCORE_PASS_THRESHOLD || '70'),
|
|
13
|
+
MAX_CRITIQUE_LOOPS: parseInt(process.env.COUNCIL_MAX_CRITIQUE_LOOPS || '3'),
|
|
14
|
+
|
|
15
|
+
SCORE_WEIGHTS: {
|
|
16
|
+
completeness: 0.25,
|
|
17
|
+
consistency: 0.25,
|
|
18
|
+
decisionQuality: 0.15,
|
|
19
|
+
structure: 0.10,
|
|
20
|
+
transparency: 0.15,
|
|
21
|
+
agentic: 0.10
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
uuidv4: randomUUID,
|
|
25
|
+
VERSION: '4.0.0'
|
|
26
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { CONFIG } from '../config.js';
|
|
4
|
+
|
|
5
|
+
const cache = {};
|
|
6
|
+
const DATA_DIR = dirname(CONFIG.DB_PATH);
|
|
7
|
+
|
|
8
|
+
function ensureDir() {
|
|
9
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function filePath(collection) {
|
|
13
|
+
return join(DATA_DIR, `${collection}.json`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function initStore(collection) {
|
|
17
|
+
// Skip if already loaded and has data
|
|
18
|
+
if (cache[collection]) return;
|
|
19
|
+
|
|
20
|
+
ensureDir();
|
|
21
|
+
const path = filePath(collection);
|
|
22
|
+
|
|
23
|
+
if (existsSync(path)) {
|
|
24
|
+
try {
|
|
25
|
+
const raw = readFileSync(path, 'utf-8');
|
|
26
|
+
cache[collection] = JSON.parse(raw);
|
|
27
|
+
return;
|
|
28
|
+
} catch {
|
|
29
|
+
// Corrupted file — reset
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
cache[collection] = [];
|
|
34
|
+
flushStore(collection);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function flushStore(collection) {
|
|
38
|
+
ensureDir();
|
|
39
|
+
const path = filePath(collection);
|
|
40
|
+
writeFileSync(path, JSON.stringify(cache[collection] || [], null, 2), 'utf-8');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getCol(collection) {
|
|
44
|
+
if (!cache[collection]) initStore(collection);
|
|
45
|
+
return cache[collection];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getAll(collection) {
|
|
49
|
+
return [...getCol(collection)];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getById(collection, id) {
|
|
53
|
+
return getCol(collection).find(item => item.id === id) || null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function findMany(collection, predicate) {
|
|
57
|
+
return getCol(collection).filter(predicate);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function findOne(collection, predicate) {
|
|
61
|
+
return getCol(collection).find(predicate) || null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function insert(collection, doc) {
|
|
65
|
+
const col = getCol(collection);
|
|
66
|
+
col.push({ ...doc });
|
|
67
|
+
flushStore(collection);
|
|
68
|
+
return doc;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function updateWhere(collection, predicate, updates) {
|
|
72
|
+
const col = getCol(collection);
|
|
73
|
+
let count = 0;
|
|
74
|
+
|
|
75
|
+
for (let i = 0; i < col.length; i++) {
|
|
76
|
+
if (predicate(col[i])) {
|
|
77
|
+
col[i] = { ...col[i], ...updates };
|
|
78
|
+
count++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (count > 0) flushStore(collection);
|
|
83
|
+
return count;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function upsert(collection, predicate, doc) {
|
|
87
|
+
const col = getCol(collection);
|
|
88
|
+
const idx = col.findIndex(predicate);
|
|
89
|
+
|
|
90
|
+
if (idx >= 0) {
|
|
91
|
+
col[idx] = { ...col[idx], ...doc };
|
|
92
|
+
flushStore(collection);
|
|
93
|
+
return col[idx];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
col.push({ ...doc });
|
|
97
|
+
flushStore(collection);
|
|
98
|
+
return doc;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function deleteWhere(collection, predicate) {
|
|
102
|
+
const col = getCol(collection);
|
|
103
|
+
const before = col.length;
|
|
104
|
+
cache[collection] = col.filter(item => !predicate(item));
|
|
105
|
+
|
|
106
|
+
if (cache[collection].length !== before) {
|
|
107
|
+
flushStore(collection);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return before - cache[collection].length;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function count(collection, predicate) {
|
|
114
|
+
if (!predicate) return getCol(collection).length;
|
|
115
|
+
return getCol(collection).filter(predicate).length;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function getNextOrder(collection, sessionId) {
|
|
119
|
+
const col = getCol(collection);
|
|
120
|
+
const sessionItems = col.filter(item => item.session_id === sessionId);
|
|
121
|
+
return sessionItems.length > 0 ? Math.max(...sessionItems.map(i => i.order_num || 0)) + 1 : 1;
|
|
122
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { initStore } from './json-store.js';
|
|
2
|
+
|
|
3
|
+
const COLLECTIONS = [
|
|
4
|
+
'sessions',
|
|
5
|
+
'phases',
|
|
6
|
+
'decisions',
|
|
7
|
+
'claims',
|
|
8
|
+
'transparency_log',
|
|
9
|
+
'phase_handoffs'
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export function runMigrations() {
|
|
13
|
+
for (const col of COLLECTIONS) {
|
|
14
|
+
initStore(col);
|
|
15
|
+
}
|
|
16
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { getDb } from './db/database.js';
|
|
4
|
+
import { CONFIG } from './config.js';
|
|
5
|
+
import { registerCouncilTools } from './tools.js';
|
|
6
|
+
|
|
7
|
+
getDb();
|
|
8
|
+
|
|
9
|
+
const server = new McpServer({
|
|
10
|
+
name: 'mcp-council-server',
|
|
11
|
+
version: CONFIG.VERSION,
|
|
12
|
+
description: 'Council MCP — Memory, State & Scaffolding untuk AI Utama. ' +
|
|
13
|
+
'Berbasis 3 prinsip: ADAPTIF | TRANSPARANSI | AGENTIC'
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
registerCouncilTools(server);
|
|
17
|
+
|
|
18
|
+
async function main() {
|
|
19
|
+
try {
|
|
20
|
+
const transport = new StdioServerTransport();
|
|
21
|
+
await server.connect(transport);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
console.error('FATAL:', err);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
main();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { CONFIG } from '../config.js';
|
|
2
|
+
import { insert, findMany } from '../db/json-store.js';
|
|
3
|
+
|
|
4
|
+
function parseDecision(row) {
|
|
5
|
+
return {
|
|
6
|
+
...row,
|
|
7
|
+
alternatives: typeof row.alternatives === 'string' ? JSON.parse(row.alternatives) : (row.alternatives || []),
|
|
8
|
+
rejected_reasons: typeof row.rejected_reasons === 'string' ? JSON.parse(row.rejected_reasons) : (row.rejected_reasons || {})
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function saveDecision(sessionId, phaseName, description, rationale, alternatives, rejectedReasons) {
|
|
13
|
+
const id = CONFIG.uuidv4();
|
|
14
|
+
insert('decisions', {
|
|
15
|
+
id,
|
|
16
|
+
session_id: sessionId,
|
|
17
|
+
phase_name: phaseName,
|
|
18
|
+
description,
|
|
19
|
+
rationale: rationale || '',
|
|
20
|
+
alternatives: JSON.stringify(alternatives || []),
|
|
21
|
+
rejected_reasons: JSON.stringify(rejectedReasons || {}),
|
|
22
|
+
created_at: new Date().toISOString()
|
|
23
|
+
});
|
|
24
|
+
return id;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getDecisions(sessionId) {
|
|
28
|
+
return findMany('decisions', d => d.session_id === sessionId)
|
|
29
|
+
.sort((a, b) => new Date(a.created_at) - new Date(b.created_at))
|
|
30
|
+
.map(parseDecision);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getDecisionsByPhase(sessionId, phaseName) {
|
|
34
|
+
return findMany('decisions', d => d.session_id === sessionId && d.phase_name === phaseName)
|
|
35
|
+
.sort((a, b) => new Date(a.created_at) - new Date(b.created_at))
|
|
36
|
+
.map(parseDecision);
|
|
37
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { CONFIG } from '../config.js';
|
|
2
|
+
import { findOne, findMany, updateWhere } from '../db/json-store.js';
|
|
3
|
+
|
|
4
|
+
function parsePhase(row) {
|
|
5
|
+
if (!row) return null;
|
|
6
|
+
return {
|
|
7
|
+
...row,
|
|
8
|
+
output: row.output ? (typeof row.output === 'string' ? JSON.parse(row.output) : row.output) : null,
|
|
9
|
+
knowledge_boundary: typeof row.knowledge_boundary === 'string' ? JSON.parse(row.knowledge_boundary) : (row.knowledge_boundary || {}),
|
|
10
|
+
certainty_summary: typeof row.certainty_summary === 'string' ? JSON.parse(row.certainty_summary) : (row.certainty_summary || {}),
|
|
11
|
+
authority_warnings: typeof row.authority_warnings === 'string' ? JSON.parse(row.authority_warnings) : (row.authority_warnings || [])
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getActivePhase(sessionId) {
|
|
16
|
+
const row = findOne('phases', p => p.session_id === sessionId && p.status === 'active');
|
|
17
|
+
return parsePhase(row);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getPhase(sessionId, name) {
|
|
21
|
+
const row = findOne('phases', p => p.session_id === sessionId && p.name === name);
|
|
22
|
+
return parsePhase(row);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getAllPhases(sessionId) {
|
|
26
|
+
const rows = findMany('phases', p => p.session_id === sessionId)
|
|
27
|
+
.sort((a, b) => a.order_num - b.order_num);
|
|
28
|
+
return rows.map(parsePhase);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function savePhaseOutput(sessionId, name, output, confidence, kb, certainty, warnings) {
|
|
32
|
+
updateWhere('phases',
|
|
33
|
+
p => p.session_id === sessionId && p.name === name,
|
|
34
|
+
{
|
|
35
|
+
status: 'done',
|
|
36
|
+
output: JSON.stringify(output),
|
|
37
|
+
confidence: confidence ?? null,
|
|
38
|
+
knowledge_boundary: JSON.stringify(kb || {}),
|
|
39
|
+
certainty_summary: JSON.stringify(certainty || {}),
|
|
40
|
+
authority_warnings: JSON.stringify(warnings || []),
|
|
41
|
+
completed_at: new Date().toISOString()
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
return getPhase(sessionId, name);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function validatePhaseTransition(sessionId, currentPhaseName, targetPhaseName) {
|
|
48
|
+
if (currentPhaseName !== targetPhaseName) {
|
|
49
|
+
return { valid: false, error: `Fase '${targetPhaseName}' tidak aktif. Fase saat ini: '${currentPhaseName}'. Selesaikan fase '${currentPhaseName}' terlebih dahulu.` };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const phase = getPhase(sessionId, targetPhaseName);
|
|
53
|
+
if (!phase) {
|
|
54
|
+
return { valid: false, error: `Fase '${targetPhaseName}' tidak ditemukan.` };
|
|
55
|
+
}
|
|
56
|
+
if (phase.status === 'done') {
|
|
57
|
+
return { valid: false, error: `Fase '${targetPhaseName}' sudah selesai. Tidak bisa overwrite.` };
|
|
58
|
+
}
|
|
59
|
+
if (phase.status !== 'active') {
|
|
60
|
+
return { valid: false, error: `Fase '${targetPhaseName}' belum aktif. Selesaikan fase sebelumnya.` };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { valid: true };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function unlockNextPhase(sessionId, currentPhaseName) {
|
|
67
|
+
const current = getPhase(sessionId, currentPhaseName);
|
|
68
|
+
if (!current) return null;
|
|
69
|
+
|
|
70
|
+
const next = findOne('phases',
|
|
71
|
+
p => p.session_id === sessionId && p.order_num === (current.order_num + 1) && p.status === 'pending'
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (!next) return null;
|
|
75
|
+
|
|
76
|
+
updateWhere('phases',
|
|
77
|
+
p => p.id === next.id,
|
|
78
|
+
{ status: 'active', started_at: new Date().toISOString() }
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return getPhase(sessionId, next.name);
|
|
82
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { getSession } from './session.js';
|
|
2
|
+
import { getAllPhases, getActivePhase } from './phase.js';
|
|
3
|
+
import { getDecisions } from './decision.js';
|
|
4
|
+
import { detectContradictions } from '../verify/contradiction.js';
|
|
5
|
+
import { getPersona } from '../prinsip/agentic.js';
|
|
6
|
+
import { getPhaseInstructions } from '../prinsip/adaptive.js';
|
|
7
|
+
|
|
8
|
+
export function composeRecallContext(sessionId, focus, format) {
|
|
9
|
+
const session = getSession(sessionId);
|
|
10
|
+
if (!session) return null;
|
|
11
|
+
|
|
12
|
+
const allPhases = getAllPhases(sessionId);
|
|
13
|
+
const decisions = getDecisions(sessionId);
|
|
14
|
+
const completedPhases = allPhases.filter(p => p.status === 'done');
|
|
15
|
+
const activePhase = getActivePhase(sessionId);
|
|
16
|
+
|
|
17
|
+
let filteredPhases = allPhases;
|
|
18
|
+
if (focus && focus.length > 0) {
|
|
19
|
+
filteredPhases = allPhases.filter(p => focus.includes(p.name));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (format === 'summary') {
|
|
23
|
+
return {
|
|
24
|
+
session: {
|
|
25
|
+
id: session.id,
|
|
26
|
+
task: session.task.length > 200 ? session.task.slice(0, 200) + '...' : session.task,
|
|
27
|
+
status: session.status,
|
|
28
|
+
currentPhase: session.current_phase,
|
|
29
|
+
complexity: session.complexity
|
|
30
|
+
},
|
|
31
|
+
phases: completedPhases.map(p => ({
|
|
32
|
+
name: p.name,
|
|
33
|
+
status: p.status,
|
|
34
|
+
confidence: p.confidence,
|
|
35
|
+
persona: p.persona
|
|
36
|
+
})),
|
|
37
|
+
progress: `${completedPhases.length}/${allPhases.length}`,
|
|
38
|
+
activePersona: activePhase ? getPersona(activePhase.name).name : null
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (format === 'decisions') {
|
|
43
|
+
return {
|
|
44
|
+
session: { id: session.id },
|
|
45
|
+
decisions: decisions.map(d => ({
|
|
46
|
+
phase: d.phase_name,
|
|
47
|
+
description: d.description,
|
|
48
|
+
rationale: d.rationale,
|
|
49
|
+
alternatives: d.alternatives,
|
|
50
|
+
rejectedReasons: d.rejected_reasons
|
|
51
|
+
}))
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// full
|
|
56
|
+
const baseContext = {
|
|
57
|
+
session: {
|
|
58
|
+
id: session.id,
|
|
59
|
+
task: session.task,
|
|
60
|
+
context: session.context,
|
|
61
|
+
status: session.status,
|
|
62
|
+
currentPhase: session.current_phase,
|
|
63
|
+
complexity: session.complexity,
|
|
64
|
+
adaptationPlan: session.adaptation_plan,
|
|
65
|
+
createdAt: session.created_at
|
|
66
|
+
},
|
|
67
|
+
phases: filteredPhases.filter(p => p.status === 'done').map(p => ({
|
|
68
|
+
name: p.name,
|
|
69
|
+
persona: p.persona,
|
|
70
|
+
depth: p.depth,
|
|
71
|
+
status: p.status,
|
|
72
|
+
output: p.output,
|
|
73
|
+
confidence: p.confidence,
|
|
74
|
+
knowledgeBoundary: p.knowledge_boundary,
|
|
75
|
+
completedAt: p.completed_at
|
|
76
|
+
})),
|
|
77
|
+
decisions,
|
|
78
|
+
completedCount: completedPhases.length,
|
|
79
|
+
totalCount: allPhases.length
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if (activePhase) {
|
|
83
|
+
const persona = getPersona(activePhase.name);
|
|
84
|
+
baseContext.currentPhase = {
|
|
85
|
+
name: activePhase.name,
|
|
86
|
+
persona: persona.name,
|
|
87
|
+
personaIdentity: persona.identity,
|
|
88
|
+
instructions: getPhaseInstructions(activePhase.name, activePhase.depth)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const warnings = [];
|
|
93
|
+
for (let i = 1; i < completedPhases.length; i++) {
|
|
94
|
+
const curr = completedPhases[i];
|
|
95
|
+
const prevs = completedPhases.slice(0, i);
|
|
96
|
+
if (curr.output) {
|
|
97
|
+
const contradictions = detectContradictions(curr.output, prevs);
|
|
98
|
+
if (contradictions.length > 0) {
|
|
99
|
+
warnings.push({ from: curr.name, issues: contradictions.slice(0, 3) });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (warnings.length > 0) {
|
|
104
|
+
baseContext.warnings = warnings;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return baseContext;
|
|
108
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { CONFIG } from '../config.js';
|
|
2
|
+
import { insert, getById, updateWhere, findOne, deleteWhere } from '../db/json-store.js';
|
|
3
|
+
import { insert as insertPhase } from '../db/json-store.js';
|
|
4
|
+
|
|
5
|
+
export function createSession(task, context, title, complexity, adaptationPlan) {
|
|
6
|
+
const id = CONFIG.uuidv4();
|
|
7
|
+
const now = new Date().toISOString();
|
|
8
|
+
|
|
9
|
+
const session = {
|
|
10
|
+
id,
|
|
11
|
+
title: title || '',
|
|
12
|
+
task,
|
|
13
|
+
context: context || '',
|
|
14
|
+
status: 'active',
|
|
15
|
+
current_phase: adaptationPlan[0]?.name || 'decompile',
|
|
16
|
+
complexity: JSON.stringify(complexity),
|
|
17
|
+
adaptation_plan: JSON.stringify(adaptationPlan),
|
|
18
|
+
created_at: now,
|
|
19
|
+
updated_at: now
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
insert('sessions', session);
|
|
23
|
+
|
|
24
|
+
// Create phase records
|
|
25
|
+
adaptationPlan.forEach((p, i) => {
|
|
26
|
+
insertPhase('phases', {
|
|
27
|
+
id: CONFIG.uuidv4(),
|
|
28
|
+
session_id: id,
|
|
29
|
+
name: p.name,
|
|
30
|
+
order_num: i + 1,
|
|
31
|
+
status: i === 0 ? 'active' : 'pending',
|
|
32
|
+
persona: p.persona || '',
|
|
33
|
+
depth: p.depth || 'standard',
|
|
34
|
+
output: null,
|
|
35
|
+
confidence: null,
|
|
36
|
+
knowledge_boundary: '{}',
|
|
37
|
+
certainty_summary: '{}',
|
|
38
|
+
authority_warnings: '[]',
|
|
39
|
+
started_at: i === 0 ? now : null,
|
|
40
|
+
completed_at: null
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return id;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getSession(sessionId) {
|
|
48
|
+
const row = getById('sessions', sessionId);
|
|
49
|
+
if (!row) return null;
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
...row,
|
|
53
|
+
complexity: JSON.parse(row.complexity || '{}'),
|
|
54
|
+
adaptation_plan: JSON.parse(row.adaptation_plan || '[]')
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function updateSessionStatus(sessionId, status) {
|
|
59
|
+
updateWhere('sessions', s => s.id === sessionId, {
|
|
60
|
+
status,
|
|
61
|
+
updated_at: new Date().toISOString()
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function updateSessionPhase(sessionId, phaseName) {
|
|
66
|
+
updateWhere('sessions', s => s.id === sessionId, {
|
|
67
|
+
current_phase: phaseName,
|
|
68
|
+
updated_at: new Date().toISOString()
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function deleteOldSessions() {
|
|
73
|
+
const cutoff = new Date();
|
|
74
|
+
cutoff.setHours(cutoff.getHours() - CONFIG.SESSION_TTL_HOURS);
|
|
75
|
+
|
|
76
|
+
const deleted = deleteWhere('sessions', s =>
|
|
77
|
+
new Date(s.created_at) < cutoff
|
|
78
|
+
);
|
|
79
|
+
return deleted;
|
|
80
|
+
}
|