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
package/src/tools.js
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { CONFIG } from './config.js';
|
|
3
|
+
import { createSession, getSession, updateSessionStatus, updateSessionPhase } from './memory/session.js';
|
|
4
|
+
import { getActivePhase, getAllPhases, savePhaseOutput, validatePhaseTransition, unlockNextPhase } from './memory/phase.js';
|
|
5
|
+
import { saveDecision } from './memory/decision.js';
|
|
6
|
+
import { composeRecallContext } from './memory/recall.js';
|
|
7
|
+
import { parseTaskStructure, extractDeliverables, extractUnresolved } from './analysis/parser.js';
|
|
8
|
+
import { assessComplexity, generatePipeline, getPhaseInstructions } from './prinsip/adaptive.js';
|
|
9
|
+
import { validateCertainty, extractKnowledgeBoundary, createTransparencyLogEntry } from './prinsip/transparency.js';
|
|
10
|
+
import { getPersona, validateAuthority, buildHandshake } from './prinsip/agentic.js';
|
|
11
|
+
import { detectContradictions } from './verify/contradiction.js';
|
|
12
|
+
import { checkGrounding } from './verify/grounding.js';
|
|
13
|
+
import { calculatePhaseScore } from './score/index.js';
|
|
14
|
+
import { scoreCompleteness } from './score/completeness.js';
|
|
15
|
+
import { scoreConsistency } from './score/consistency.js';
|
|
16
|
+
import { scoreTransparency } from './score/transparency.js';
|
|
17
|
+
import { scoreAgentic } from './score/agentic.js';
|
|
18
|
+
import { insert } from './db/json-store.js';
|
|
19
|
+
|
|
20
|
+
function sanitize(str) {
|
|
21
|
+
if (typeof str !== 'string') return '';
|
|
22
|
+
return str.replace(/\0/g, '').normalize('NFKC').trim();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buildError(message) {
|
|
26
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: message }) }], isError: true };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildResponse(data) {
|
|
30
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const InitSchema = z.object({
|
|
34
|
+
task: z.string().min(10).max(CONFIG.MAX_TASK_LENGTH),
|
|
35
|
+
context: z.string().max(CONFIG.MAX_CONTEXT_LENGTH).optional().default(''),
|
|
36
|
+
title: z.string().max(200).optional().default('')
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const SaveSchema = z.object({
|
|
40
|
+
sessionId: z.string().uuid(),
|
|
41
|
+
phase: z.enum(['decompile', 'design', 'critique', 'synthesis', 'verify']),
|
|
42
|
+
output: z.object({
|
|
43
|
+
summary: z.string().min(1).max(20000),
|
|
44
|
+
decisions: z.array(z.object({
|
|
45
|
+
description: z.string(),
|
|
46
|
+
rationale: z.string().optional().default(''),
|
|
47
|
+
alternatives: z.array(z.string()).optional().default([]),
|
|
48
|
+
rejectedReasons: z.record(z.string()).optional().default({})
|
|
49
|
+
})).optional().default([]),
|
|
50
|
+
constraints: z.array(z.string()).optional().default([]),
|
|
51
|
+
confidence: z.number().min(0).max(1),
|
|
52
|
+
claims: z.array(z.object({
|
|
53
|
+
text: z.string(),
|
|
54
|
+
certainty: z.enum(['confirmed', 'likely', 'uncertain', 'unknown', 'assumption']).optional().default('unknown'),
|
|
55
|
+
source: z.string().optional().default('assumption')
|
|
56
|
+
})).optional().default([]),
|
|
57
|
+
knowledgeBoundary: z.object({
|
|
58
|
+
whatIKnow: z.array(z.string()).optional().default([]),
|
|
59
|
+
whatIAssume: z.array(z.string()).optional().default([]),
|
|
60
|
+
whatIDontKnow: z.array(z.string()).optional().default([]),
|
|
61
|
+
whatNeedsVerification: z.array(z.string()).optional().default([])
|
|
62
|
+
}).optional().default({}),
|
|
63
|
+
approaches: z.array(z.any()).optional(),
|
|
64
|
+
justification: z.string().optional(),
|
|
65
|
+
tradeoffs: z.array(z.any()).optional(),
|
|
66
|
+
recommended: z.string().optional(),
|
|
67
|
+
vulnerabilities: z.array(z.any()).optional(),
|
|
68
|
+
riskLevel: z.string().optional(),
|
|
69
|
+
mitigation: z.array(z.any()).optional(),
|
|
70
|
+
implementation: z.array(z.any()).optional(),
|
|
71
|
+
testing: z.array(z.any()).optional(),
|
|
72
|
+
warnings: z.array(z.string()).optional().default([]),
|
|
73
|
+
checks: z.array(z.any()).optional(),
|
|
74
|
+
passedCount: z.number().optional(),
|
|
75
|
+
failedCount: z.number().optional(),
|
|
76
|
+
finalConfidence: z.number().optional(),
|
|
77
|
+
criticalQuestions: z.array(z.string()).optional().default([])
|
|
78
|
+
})
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const RecallSchema = z.object({
|
|
82
|
+
sessionId: z.string().uuid(),
|
|
83
|
+
focus: z.array(z.enum(['decompile', 'design', 'critique', 'synthesis', 'verify'])).optional(),
|
|
84
|
+
format: z.enum(['full', 'summary', 'decisions']).optional().default('full')
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const VerifySchema = z.object({
|
|
88
|
+
sessionId: z.string().uuid(),
|
|
89
|
+
claims: z.array(z.object({
|
|
90
|
+
text: z.string(),
|
|
91
|
+
source: z.string().optional().default('assumption'),
|
|
92
|
+
type: z.string().optional().default('')
|
|
93
|
+
})),
|
|
94
|
+
mode: z.enum(['contradiction', 'grounding', 'all']).optional().default('all')
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const ContinueSchema = z.object({
|
|
98
|
+
sessionId: z.string().uuid()
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const ScoreSchema = z.object({
|
|
102
|
+
sessionId: z.string().uuid()
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
export function registerCouncilTools(server) {
|
|
106
|
+
// ─── Tool 1: council_init ───────────────────────────────────────
|
|
107
|
+
server.tool(
|
|
108
|
+
'council_init',
|
|
109
|
+
'Memulai sesi reasoning council. Panggil pertama kali saat menerima prompt kompleks dari user. ' +
|
|
110
|
+
'Mengembalikan adaptive pipeline, persona, phase plan, dan memory slots. ' +
|
|
111
|
+
'Pipeline menyesuaikan kompleksitas task (light/standard/full).',
|
|
112
|
+
{
|
|
113
|
+
task: z.string().min(10).max(CONFIG.MAX_TASK_LENGTH).describe('Prompt kompleks dari user'),
|
|
114
|
+
context: z.string().max(CONFIG.MAX_CONTEXT_LENGTH).optional().describe('Konteks tambahan'),
|
|
115
|
+
title: z.string().max(200).optional().describe('Nama sesi untuk identifikasi')
|
|
116
|
+
},
|
|
117
|
+
async ({ task, context, title }) => {
|
|
118
|
+
try {
|
|
119
|
+
const cleanTask = sanitize(task);
|
|
120
|
+
const cleanContext = sanitize(context || '');
|
|
121
|
+
|
|
122
|
+
const structure = parseTaskStructure(cleanTask);
|
|
123
|
+
const complexity = assessComplexity(cleanTask, structure);
|
|
124
|
+
const pipeline = generatePipeline(complexity);
|
|
125
|
+
const sessionId = createSession(cleanTask, cleanContext, title, complexity, pipeline);
|
|
126
|
+
const activePhase = getActivePhase(sessionId);
|
|
127
|
+
const persona = getPersona(activePhase.name);
|
|
128
|
+
|
|
129
|
+
return buildResponse({
|
|
130
|
+
type: 'council_init',
|
|
131
|
+
sessionId,
|
|
132
|
+
adaptation: {
|
|
133
|
+
complexity,
|
|
134
|
+
pipeline: pipeline.map((p, i) => ({ ...p, order: i + 1 })),
|
|
135
|
+
adaptiveReason: `Complexity score ${complexity.normalized}/100 (${complexity.level}). Pipeline: ${complexity.pipelineDepth}.`
|
|
136
|
+
},
|
|
137
|
+
personas: { current: { name: persona.name, identity: persona.identity, authority: persona.authority, notAuthority: persona.notAuthority } },
|
|
138
|
+
phase: activePhase.name,
|
|
139
|
+
phasePlan: pipeline.map((p, i) => ({
|
|
140
|
+
name: p.name, persona: p.persona, order: i + 1, status: i === 0 ? 'active' : 'locked'
|
|
141
|
+
})),
|
|
142
|
+
instructions: getPhaseInstructions(activePhase.name, activePhase.depth),
|
|
143
|
+
memorySlots: {
|
|
144
|
+
originalTask: cleanTask,
|
|
145
|
+
decompositions: [],
|
|
146
|
+
decisions: [],
|
|
147
|
+
constraints: [],
|
|
148
|
+
criticalQuestions: []
|
|
149
|
+
},
|
|
150
|
+
transparencyLog: { entries: [], count: 0 }
|
|
151
|
+
});
|
|
152
|
+
} catch (err) {
|
|
153
|
+
return buildError(`Gagal inisialisasi: ${err.message}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// ─── Tool 2: council_save ─────────────────────────────────────────
|
|
159
|
+
server.tool(
|
|
160
|
+
'council_save',
|
|
161
|
+
'Simpan hasil fase ke memory. Fase harus sesuai urutan (tidak bisa skip). ' +
|
|
162
|
+
'Setelah save: server menghitung scoring, deteksi kontradiksi, validasi authority, ' +
|
|
163
|
+
'dan menyusun handoff ke fase berikutnya. Jika score < 70, AI disarankan refine fase ini.',
|
|
164
|
+
{
|
|
165
|
+
sessionId: z.string().uuid().describe('ID sesi dari council_init'),
|
|
166
|
+
phase: z.enum(['decompile', 'design', 'critique', 'synthesis', 'verify']).describe('Nama fase yang akan disimpan'),
|
|
167
|
+
output: z.any().describe('Output fase — object dengan summary, decisions, claims, dll')
|
|
168
|
+
},
|
|
169
|
+
async ({ sessionId, phase, output }) => {
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
// Validasi session
|
|
173
|
+
const session = getSession(sessionId);
|
|
174
|
+
if (!session) return buildError('Session tidak ditemukan. Gunakan council_init untuk memulai sesi baru.');
|
|
175
|
+
|
|
176
|
+
// Validasi phase transition
|
|
177
|
+
const transition = validatePhaseTransition(sessionId, session.current_phase, phase);
|
|
178
|
+
if (!transition.valid) return buildError(transition.error);
|
|
179
|
+
|
|
180
|
+
// Authority validation
|
|
181
|
+
const persona = getPersona(phase);
|
|
182
|
+
const authorityWarnings = validateAuthority(phase, output);
|
|
183
|
+
|
|
184
|
+
// Get previous phases for scoring
|
|
185
|
+
const allPhases = getAllPhases(sessionId);
|
|
186
|
+
const previousPhases = allPhases.filter(p => p.status === 'done');
|
|
187
|
+
|
|
188
|
+
// Scoring
|
|
189
|
+
const score = calculatePhaseScore(phase, output, previousPhases);
|
|
190
|
+
|
|
191
|
+
// Transparency validation
|
|
192
|
+
const certaintyResult = validateCertainty(output.claims || []);
|
|
193
|
+
const kb = extractKnowledgeBoundary(output);
|
|
194
|
+
|
|
195
|
+
// Contradiction detection
|
|
196
|
+
const conflicts = detectContradictions(output, previousPhases);
|
|
197
|
+
|
|
198
|
+
// Save to DB
|
|
199
|
+
savePhaseOutput(sessionId, phase, output, output.confidence, kb, certaintyResult.stats, authorityWarnings);
|
|
200
|
+
|
|
201
|
+
// Save decisions
|
|
202
|
+
if (output.decisions) {
|
|
203
|
+
for (const d of output.decisions) {
|
|
204
|
+
saveDecision(sessionId, phase, d.description, d.rationale, d.alternatives, d.rejectedReasons);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Save claims to store
|
|
209
|
+
if (output.claims) {
|
|
210
|
+
for (const c of output.claims) {
|
|
211
|
+
insert('claims', {
|
|
212
|
+
id: CONFIG.uuidv4(),
|
|
213
|
+
session_id: sessionId,
|
|
214
|
+
phase_name: phase,
|
|
215
|
+
text: c.text,
|
|
216
|
+
certainty: c.certainty || 'unknown',
|
|
217
|
+
source: c.source || 'assumption',
|
|
218
|
+
status: 'unverified',
|
|
219
|
+
created_at: new Date().toISOString()
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Transparency log for issues
|
|
225
|
+
if (!certaintyResult.valid) {
|
|
226
|
+
createTransparencyLogEntry(sessionId, phase, 'uncertainty',
|
|
227
|
+
`${certaintyResult.unlabeled.length} claims tanpa certainty`, certaintyResult.unlabeled, 'unknown');
|
|
228
|
+
}
|
|
229
|
+
if (authorityWarnings.length > 0) {
|
|
230
|
+
createTransparencyLogEntry(sessionId, phase, 'boundary',
|
|
231
|
+
`${authorityWarnings.length} authority violations`, authorityWarnings, 'unknown');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Handoff
|
|
235
|
+
const nextPhase = unlockNextPhase(sessionId, phase);
|
|
236
|
+
if (nextPhase) {
|
|
237
|
+
updateSessionPhase(sessionId, nextPhase.name);
|
|
238
|
+
} else {
|
|
239
|
+
updateSessionPhase(sessionId, 'done');
|
|
240
|
+
updateSessionStatus(sessionId, 'done');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const deliverables = extractDeliverables(output);
|
|
244
|
+
const unresolved = extractUnresolved(output);
|
|
245
|
+
const handshake = buildHandshake(phase, nextPhase?.name || 'done', deliverables, authorityWarnings, unresolved, conflicts);
|
|
246
|
+
|
|
247
|
+
// Context for next phase
|
|
248
|
+
let contextForNext = null;
|
|
249
|
+
if (nextPhase && score.canProceed) {
|
|
250
|
+
const nextPersona = getPersona(nextPhase.name);
|
|
251
|
+
contextForNext = {
|
|
252
|
+
currentPersona: {
|
|
253
|
+
name: nextPersona.name,
|
|
254
|
+
identity: nextPersona.identity,
|
|
255
|
+
authority: nextPersona.authority,
|
|
256
|
+
notAuthority: nextPersona.notAuthority
|
|
257
|
+
},
|
|
258
|
+
originalTask: session.task,
|
|
259
|
+
previousResults: allPhases.filter(p => p.status === 'done').map(p => ({
|
|
260
|
+
phase: p.name,
|
|
261
|
+
summary: p.output?.summary || '',
|
|
262
|
+
confidence: p.confidence
|
|
263
|
+
})),
|
|
264
|
+
phaseInstructions: getPhaseInstructions(nextPhase.name, nextPhase.depth)
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return buildResponse({
|
|
269
|
+
type: 'council_save',
|
|
270
|
+
status: 'saved',
|
|
271
|
+
phase,
|
|
272
|
+
scoring: {
|
|
273
|
+
finalScore: score.finalScore,
|
|
274
|
+
verdict: score.verdict,
|
|
275
|
+
dimensions: score.dimensions,
|
|
276
|
+
suggestions: score.suggestions
|
|
277
|
+
},
|
|
278
|
+
canProceed: score.canProceed,
|
|
279
|
+
handoff: handshake,
|
|
280
|
+
nextPhase: nextPhase?.name || null,
|
|
281
|
+
phasePlan: allPhases.map(p => ({
|
|
282
|
+
name: p.name,
|
|
283
|
+
status: p.status === phase ? 'done' : p.name === nextPhase?.name ? 'active' : p.status === 'done' ? 'done' : 'locked'
|
|
284
|
+
})),
|
|
285
|
+
contextForNext,
|
|
286
|
+
sessionComplete: !nextPhase
|
|
287
|
+
});
|
|
288
|
+
} catch (err) {
|
|
289
|
+
return buildError(`Gagal menyimpan fase: ${err.message}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
// ─── Tool 3: council_recall ───────────────────────────────────────
|
|
295
|
+
server.tool(
|
|
296
|
+
'council_recall',
|
|
297
|
+
'Ambil memory kapan saja. Berguna saat AI lupa konteks, mau review keputusan, ' +
|
|
298
|
+
'atau resume session. Tersedia format: full (default), summary, atau decisions-only.',
|
|
299
|
+
{
|
|
300
|
+
sessionId: z.string().uuid().describe('ID sesi'),
|
|
301
|
+
focus: z.array(z.enum(['decompile', 'design', 'critique', 'synthesis', 'verify'])).optional()
|
|
302
|
+
.describe('Fase spesifik yang ingin dilihat'),
|
|
303
|
+
format: z.enum(['full', 'summary', 'decisions']).optional().default('full')
|
|
304
|
+
.describe('Format output: full (default), summary, atau decisions-only')
|
|
305
|
+
},
|
|
306
|
+
async ({ sessionId, focus, format }) => {
|
|
307
|
+
try {
|
|
308
|
+
const context = composeRecallContext(sessionId, focus, format);
|
|
309
|
+
if (!context) return buildError('Session tidak ditemukan.');
|
|
310
|
+
return buildResponse({ type: 'council_recall', ...context });
|
|
311
|
+
} catch (err) {
|
|
312
|
+
return buildError(`Gagal recall: ${err.message}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
// ─── Tool 4: council_verify ───────────────────────────────────────
|
|
318
|
+
server.tool(
|
|
319
|
+
'council_verify',
|
|
320
|
+
'Verifikasi klaim terhadap memory sesi. Deteksi kontradiksi numerik, polaritas, ' +
|
|
321
|
+
'dan grounding source. Mode: contradiction, grounding, atau all (default).',
|
|
322
|
+
{
|
|
323
|
+
sessionId: z.string().uuid().describe('ID sesi'),
|
|
324
|
+
claims: z.any().describe('Daftar klaim yang akan diverifikasi — array of {text, source?, type?}'),
|
|
325
|
+
mode: z.enum(['contradiction', 'grounding', 'all']).optional().default('all')
|
|
326
|
+
.describe('Mode verifikasi')
|
|
327
|
+
},
|
|
328
|
+
async ({ sessionId, claims, mode }) => {
|
|
329
|
+
try {
|
|
330
|
+
const session = getSession(sessionId);
|
|
331
|
+
if (!session) return buildError('Session tidak ditemukan.');
|
|
332
|
+
|
|
333
|
+
const allPhases = getAllPhases(sessionId);
|
|
334
|
+
const completedPhases = allPhases.filter(p => p.status === 'done');
|
|
335
|
+
|
|
336
|
+
const results = [];
|
|
337
|
+
let contradictionResults = [];
|
|
338
|
+
let groundingResults = null;
|
|
339
|
+
|
|
340
|
+
if (mode === 'contradiction' || mode === 'all') {
|
|
341
|
+
const claimsObj = { claims };
|
|
342
|
+
contradictionResults = detectContradictions(claimsObj, completedPhases);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (mode === 'grounding' || mode === 'all') {
|
|
346
|
+
groundingResults = checkGrounding(claims);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
let total = 0, passed = 0, warning = 0, contradicted = 0;
|
|
350
|
+
|
|
351
|
+
if (contradictionResults.length > 0) {
|
|
352
|
+
contradicted = contradictionResults.length;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (groundingResults) {
|
|
356
|
+
passed = groundingResults.verified;
|
|
357
|
+
warning = groundingResults.unverified;
|
|
358
|
+
total = groundingResults.total;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return buildResponse({
|
|
362
|
+
type: 'council_verify',
|
|
363
|
+
results: {
|
|
364
|
+
contradictions: contradictionResults,
|
|
365
|
+
grounding: groundingResults
|
|
366
|
+
},
|
|
367
|
+
summary: {
|
|
368
|
+
total: Math.max(total, contradictionResults.length),
|
|
369
|
+
passed,
|
|
370
|
+
warning,
|
|
371
|
+
contradicted
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
} catch (err) {
|
|
375
|
+
return buildError(`Gagal verifikasi: ${err.message}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
// ─── Tool 5: council_continue ─────────────────────────────────────
|
|
381
|
+
server.tool(
|
|
382
|
+
'council_continue',
|
|
383
|
+
'Resume session yang terhenti (karena restart atau interupsi). ' +
|
|
384
|
+
'Mengembalikan full context + fase terakhir yang aktif + instruksi resume.',
|
|
385
|
+
{
|
|
386
|
+
sessionId: z.string().uuid().describe('ID sesi yang akan dilanjutkan')
|
|
387
|
+
},
|
|
388
|
+
async ({ sessionId }) => {
|
|
389
|
+
try {
|
|
390
|
+
const session = getSession(sessionId);
|
|
391
|
+
if (!session) return buildError('Session tidak ditemukan.');
|
|
392
|
+
|
|
393
|
+
if (session.status === 'done') {
|
|
394
|
+
return buildError('Session sudah selesai. Gunakan council_recall untuk review.');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const activePhase = getActivePhase(sessionId);
|
|
398
|
+
if (!activePhase) return buildError('Tidak ada fase aktif. Session mungkin sudah selesai.');
|
|
399
|
+
|
|
400
|
+
const allPhases = getAllPhases(sessionId);
|
|
401
|
+
const lastDone = allPhases.filter(p => p.status === 'done').pop();
|
|
402
|
+
const persona = getPersona(activePhase.name);
|
|
403
|
+
const context = composeRecallContext(sessionId);
|
|
404
|
+
|
|
405
|
+
return buildResponse({
|
|
406
|
+
type: 'council_continue',
|
|
407
|
+
session: {
|
|
408
|
+
id: session.id,
|
|
409
|
+
task: session.task,
|
|
410
|
+
status: session.status,
|
|
411
|
+
currentPhase: session.current_phase,
|
|
412
|
+
progress: `${allPhases.filter(p => p.status === 'done').length}/${allPhases.length}`
|
|
413
|
+
},
|
|
414
|
+
currentPhase: {
|
|
415
|
+
name: activePhase.name,
|
|
416
|
+
persona: persona.name,
|
|
417
|
+
personaIdentity: persona.identity,
|
|
418
|
+
personaAuthority: persona.authority,
|
|
419
|
+
personaNotAuthority: persona.notAuthority,
|
|
420
|
+
instructions: getPhaseInstructions(activePhase.name, activePhase.depth)
|
|
421
|
+
},
|
|
422
|
+
lastCompletedPhase: lastDone ? { name: lastDone.name, summary: lastDone.output?.summary || '' } : null,
|
|
423
|
+
context,
|
|
424
|
+
resumeInstructions: `Lanjutkan dari fase ${activePhase.name} sebagai ${persona.name}. ${getPhaseInstructions(activePhase.name, activePhase.depth)}`
|
|
425
|
+
});
|
|
426
|
+
} catch (err) {
|
|
427
|
+
return buildError(`Gagal melanjutkan session: ${err.message}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
// ─── Tool 6: council_score ────────────────────────────────────────
|
|
433
|
+
server.tool(
|
|
434
|
+
'council_score',
|
|
435
|
+
'Nilai kualitas seluruh sesi council. Menganalisis completeness, consistency, ' +
|
|
436
|
+
'transparency, dan agentic alignment dari SEMUA fase. Memberi overall score + rekomendasi.',
|
|
437
|
+
{
|
|
438
|
+
sessionId: z.string().uuid().describe('ID sesi yang akan dinilai')
|
|
439
|
+
},
|
|
440
|
+
async ({ sessionId }) => {
|
|
441
|
+
try {
|
|
442
|
+
const session = getSession(sessionId);
|
|
443
|
+
if (!session) return buildError('Session tidak ditemukan.');
|
|
444
|
+
|
|
445
|
+
const allPhases = getAllPhases(sessionId);
|
|
446
|
+
const completedPhases = allPhases.filter(p => p.status === 'done');
|
|
447
|
+
|
|
448
|
+
if (completedPhases.length === 0) {
|
|
449
|
+
return buildError('Belum ada fase yang selesai. Selesaikan minimal 1 fase terlebih dahulu.');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const perPhase = [];
|
|
453
|
+
let totalQuality = 0;
|
|
454
|
+
let totalTransparency = 0;
|
|
455
|
+
let totalAgentic = 0;
|
|
456
|
+
let totalAdaptability = 0;
|
|
457
|
+
|
|
458
|
+
for (let i = 0; i < completedPhases.length; i++) {
|
|
459
|
+
const phase = completedPhases[i];
|
|
460
|
+
const prevPhases = completedPhases.slice(0, i);
|
|
461
|
+
const persona = getPersona(phase.name);
|
|
462
|
+
const output = phase.output || {};
|
|
463
|
+
|
|
464
|
+
const compScore = scoreCompleteness(phase.name, output);
|
|
465
|
+
const consScore = scoreConsistency(phase.name, output, prevPhases);
|
|
466
|
+
const transScore = scoreTransparency(phase.name, output);
|
|
467
|
+
const agScore = scoreAgentic(phase.name, output, persona);
|
|
468
|
+
|
|
469
|
+
const phaseFinal = Math.round(
|
|
470
|
+
compScore.score * 0.25 + consScore.score * 0.25 +
|
|
471
|
+
(output.decisions?.length > 0 ? 80 : 0) * 0.15 +
|
|
472
|
+
transScore.score * 0.15 + agScore.score * 0.10 + 100 * 0.10
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
totalQuality += phaseFinal;
|
|
476
|
+
totalTransparency += transScore.score;
|
|
477
|
+
totalAgentic += agScore.score;
|
|
478
|
+
|
|
479
|
+
perPhase.push({
|
|
480
|
+
phase: phase.name,
|
|
481
|
+
persona: persona.name,
|
|
482
|
+
finalScore: phaseFinal,
|
|
483
|
+
verdict: phaseFinal >= 90 ? 'excellent' : phaseFinal >= 70 ? 'good' : phaseFinal >= 50 ? 'needs_improvement' : 'insufficient',
|
|
484
|
+
dimensions: {
|
|
485
|
+
completeness: compScore,
|
|
486
|
+
consistency: { score: consScore.score, contradictions: consScore.contradictions },
|
|
487
|
+
transparency: transScore,
|
|
488
|
+
agentic: agScore
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Cross-phase contradictions
|
|
494
|
+
const crossPhaseIssues = [];
|
|
495
|
+
for (let i = 1; i < completedPhases.length; i++) {
|
|
496
|
+
const curr = completedPhases[i];
|
|
497
|
+
const prevs = completedPhases.slice(0, i);
|
|
498
|
+
if (curr.output) {
|
|
499
|
+
const contradictions = detectContradictions(curr.output, prevs);
|
|
500
|
+
if (contradictions.length > 0) {
|
|
501
|
+
crossPhaseIssues.push({
|
|
502
|
+
phase: curr.name,
|
|
503
|
+
contradictions: contradictions.slice(0, 5)
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const avgQuality = Math.round(totalQuality / completedPhases.length);
|
|
510
|
+
const avgTransparency = Math.round(totalTransparency / completedPhases.length);
|
|
511
|
+
const avgAgentic = Math.round(totalAgentic / completedPhases.length);
|
|
512
|
+
const adaptabilityScore = session.adaptation_plan?.length > 0 ? 85 : 70;
|
|
513
|
+
|
|
514
|
+
const overall = Math.round(avgQuality * 0.4 + adaptabilityScore * 0.2 + avgTransparency * 0.2 + avgAgentic * 0.2);
|
|
515
|
+
const finalVerdict = overall >= 90 ? 'excellent' : overall >= 70 ? 'good' : overall >= 50 ? 'needs_improvement' : 'insufficient';
|
|
516
|
+
|
|
517
|
+
// Suggestions
|
|
518
|
+
const suggestions = [];
|
|
519
|
+
if (crossPhaseIssues.length > 0) {
|
|
520
|
+
suggestions.push(`Resolve ${crossPhaseIssues.length} cross-phase contradiction(s)`);
|
|
521
|
+
}
|
|
522
|
+
const lowScorePhases = perPhase.filter(p => p.finalScore < 70);
|
|
523
|
+
if (lowScorePhases.length > 0) {
|
|
524
|
+
suggestions.push(`Perbaiki fase: ${lowScorePhases.map(p => `${p.phase}(${p.finalScore})`).join(', ')}`);
|
|
525
|
+
}
|
|
526
|
+
if (avgTransparency < 70) {
|
|
527
|
+
suggestions.push('Tingkatkan transparansi: tambahkan certainty level dan knowledge boundary');
|
|
528
|
+
}
|
|
529
|
+
if (avgAgentic < 70) {
|
|
530
|
+
suggestions.push('Tingkatkan agentic alignment: ikuti batasan authority persona');
|
|
531
|
+
}
|
|
532
|
+
if (suggestions.length === 0) {
|
|
533
|
+
suggestions.push('Semua fase berkualitas baik. Tidak ada rekomendasi perbaikan.');
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return buildResponse({
|
|
537
|
+
type: 'council_score',
|
|
538
|
+
sessionId,
|
|
539
|
+
overall: {
|
|
540
|
+
finalScore: overall,
|
|
541
|
+
verdict: finalVerdict,
|
|
542
|
+
qualityScore: avgQuality,
|
|
543
|
+
adaptabilityScore,
|
|
544
|
+
transparencyScore: avgTransparency,
|
|
545
|
+
agenticScore: avgAgentic
|
|
546
|
+
},
|
|
547
|
+
perPhase,
|
|
548
|
+
crossPhaseIssues,
|
|
549
|
+
improvementSuggestions: suggestions
|
|
550
|
+
});
|
|
551
|
+
} catch (err) {
|
|
552
|
+
return buildError(`Gagal menghitung score: ${err.message}`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
);
|
|
556
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const POLARITY_PAIRS = [
|
|
2
|
+
['increase', 'decrease'], ['use', 'avoid'], ['sync', 'async'],
|
|
3
|
+
['monolith', 'microservice'], ['sql', 'nosql'],
|
|
4
|
+
['paid', 'free'], ['fast', 'slow'],
|
|
5
|
+
['secure', 'insecure'], ['include', 'exclude'],
|
|
6
|
+
['start', 'stop'], ['enable', 'disable'],
|
|
7
|
+
['accept', 'reject'], ['allow', 'deny'],
|
|
8
|
+
['optimistic', 'pessimistic'], ['vertical', 'horizontal'],
|
|
9
|
+
['centralized', 'decentralized'], ['synchronous', 'asynchronous'],
|
|
10
|
+
['blocking', 'non-blocking'], ['stateful', 'stateless']
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export function detectContradictions(newOutput, previousPhases) {
|
|
14
|
+
if (!previousPhases || previousPhases.length === 0) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const contradictions = [];
|
|
19
|
+
const newText = JSON.stringify(newOutput).toLowerCase();
|
|
20
|
+
|
|
21
|
+
// 1. Numerical contradiction detection
|
|
22
|
+
const numberPattern = /\b(\d+\.?\d*)\s*(ms|s|gb|mb|kb|%|usd|rb|rps|qps|tps|menit|jam|hari|gbps|mbps)?\b/g;
|
|
23
|
+
|
|
24
|
+
const currentNumbers = [];
|
|
25
|
+
let match;
|
|
26
|
+
while ((match = numberPattern.exec(newText)) !== null) {
|
|
27
|
+
currentNumbers.push({ value: parseFloat(match[1]), unit: (match[2] || '').toLowerCase(), raw: match[0] });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const prev of previousPhases) {
|
|
31
|
+
if (!prev.output) continue;
|
|
32
|
+
const prevText = JSON.stringify(prev.output).toLowerCase();
|
|
33
|
+
|
|
34
|
+
const prevNumbers = [];
|
|
35
|
+
while ((match = numberPattern.exec(prevText)) !== null) {
|
|
36
|
+
prevNumbers.push({ value: parseFloat(match[1]), unit: (match[2] || '').toLowerCase(), raw: match[0] });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const curr of currentNumbers) {
|
|
40
|
+
if (isNaN(curr.value) || curr.value < 1) continue;
|
|
41
|
+
|
|
42
|
+
for (const prevN of prevNumbers) {
|
|
43
|
+
if (isNaN(prevN.value) || prevN.value < 1) continue;
|
|
44
|
+
if (curr.unit !== prevN.unit) continue;
|
|
45
|
+
if (!curr.unit || !prevN.unit) continue; // require explicit units
|
|
46
|
+
|
|
47
|
+
const deviation = Math.abs(curr.value - prevN.value) / Math.max(Math.abs(prevN.value), 0.001);
|
|
48
|
+
if (deviation > 0.2) {
|
|
49
|
+
const key = `${curr.raw}-vs-${prevN.raw}-${prev.name}`;
|
|
50
|
+
if (!contradictions.some(c => c.key === key)) {
|
|
51
|
+
contradictions.push({
|
|
52
|
+
key,
|
|
53
|
+
type: 'numerical',
|
|
54
|
+
detail: `Angka "${curr.raw}" (fase saat ini) vs "${prevN.raw}" (${prev.name}) — deviasi ${Math.round(deviation * 100)}%`,
|
|
55
|
+
between: [prev.name, 'current']
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 2. Polarity contradiction detection
|
|
64
|
+
for (const [pos, neg] of POLARITY_PAIRS) {
|
|
65
|
+
const currentHasPos = newText.includes(pos);
|
|
66
|
+
const currentHasNeg = newText.includes(neg);
|
|
67
|
+
|
|
68
|
+
for (const prev of previousPhases) {
|
|
69
|
+
if (!prev.output) continue;
|
|
70
|
+
const prevText = JSON.stringify(prev.output).toLowerCase();
|
|
71
|
+
const prevHasPos = prevText.includes(pos);
|
|
72
|
+
const prevHasNeg = prevText.includes(neg);
|
|
73
|
+
|
|
74
|
+
if (currentHasPos && prevHasNeg) {
|
|
75
|
+
const key = `polarity-${pos}-${neg}-${prev.name}`;
|
|
76
|
+
if (!contradictions.some(c => c.key === key)) {
|
|
77
|
+
contradictions.push({
|
|
78
|
+
key,
|
|
79
|
+
type: 'polarity',
|
|
80
|
+
detail: `Fase ini bilang "${pos}" tapi ${prev.name} bilang "${neg}"`,
|
|
81
|
+
between: [prev.name, 'current']
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (currentHasNeg && prevHasPos) {
|
|
87
|
+
const key = `polarity-${neg}-${pos}-${prev.name}`;
|
|
88
|
+
if (!contradictions.some(c => c.key === key)) {
|
|
89
|
+
contradictions.push({
|
|
90
|
+
key,
|
|
91
|
+
type: 'polarity',
|
|
92
|
+
detail: `Fase ini bilang "${neg}" tapi ${prev.name} bilang "${pos}"`,
|
|
93
|
+
between: [prev.name, 'current']
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 3. Constraint compliance check (soft warning)
|
|
101
|
+
for (const prev of previousPhases) {
|
|
102
|
+
if (!prev.output?.constraints) continue;
|
|
103
|
+
for (const constraint of prev.output.constraints) {
|
|
104
|
+
const keywords = String(constraint).toLowerCase().split(/[\s_]+/).filter(k => k.length > 3);
|
|
105
|
+
const found = keywords.some(k => newText.includes(k));
|
|
106
|
+
if (!found && keywords.length > 0) {
|
|
107
|
+
contradictions.push({
|
|
108
|
+
type: 'constraint_not_referenced',
|
|
109
|
+
detail: `Constraint "${constraint}" dari fase ${prev.name} tidak direferensi di fase ini`,
|
|
110
|
+
between: [prev.name, 'current'],
|
|
111
|
+
severity: 'warning'
|
|
112
|
+
});
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Remove duplicates and clean up
|
|
119
|
+
return contradictions.map(c => ({
|
|
120
|
+
type: c.type,
|
|
121
|
+
detail: c.detail,
|
|
122
|
+
between: c.between,
|
|
123
|
+
severity: c.severity || 'contradiction'
|
|
124
|
+
}));
|
|
125
|
+
}
|