cuelo 0.10.21 → 0.10.23
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/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +5 -5
- package/.next/build-manifest.json +2 -2
- package/.next/prerender-manifest.json +3 -3
- package/.next/required-server-files.js +1 -1
- package/.next/required-server-files.json +1 -1
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +2 -2
- package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/api/updates/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +4 -4
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/index.segments/_full.segment.rsc +4 -4
- package/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/office/page_client-reference-manifest.js +1 -1
- package/.next/server/app/office-preview/page_client-reference-manifest.js +1 -1
- package/.next/server/app/office-preview.html +1 -1
- package/.next/server/app/office-preview.rsc +2 -2
- package/.next/server/app/office-preview.segments/_full.segment.rsc +2 -2
- package/.next/server/app/office-preview.segments/_head.segment.rsc +1 -1
- package/.next/server/app/office-preview.segments/_index.segment.rsc +2 -2
- package/.next/server/app/office-preview.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/office-preview.segments/office-preview/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/office-preview.segments/office-preview.segment.rsc +1 -1
- package/.next/server/app/office.html +1 -1
- package/.next/server/app/office.rsc +4 -4
- package/.next/server/app/office.segments/_full.segment.rsc +4 -4
- package/.next/server/app/office.segments/_head.segment.rsc +1 -1
- package/.next/server/app/office.segments/_index.segment.rsc +2 -2
- package/.next/server/app/office.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/office.segments/office/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/office.segments/office.segment.rsc +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/recover/page_client-reference-manifest.js +1 -1
- package/.next/server/app/recover.html +1 -1
- package/.next/server/app/recover.rsc +2 -2
- package/.next/server/app/recover.segments/_full.segment.rsc +2 -2
- package/.next/server/app/recover.segments/_head.segment.rsc +1 -1
- package/.next/server/app/recover.segments/_index.segment.rsc +2 -2
- package/.next/server/app/recover.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/recover.segments/recover/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/recover.segments/recover.segment.rsc +1 -1
- package/.next/server/app-paths-manifest.json +5 -5
- package/.next/server/chunks/1517.js +4 -4
- package/.next/server/chunks/5489.js +3 -3
- package/.next/server/chunks/7362.js +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/{6110-7a2311ab143b31e2.js → 6110-ac70431887855d47.js} +1 -1
- package/.next/static/chunks/{9575-032510d25828764f.js → 9575-0c26b1d8b9a98d3b.js} +3 -3
- package/.next/static/chunks/app/{layout-a7fca69a4fa9b2c2.js → layout-bc09a4f5bbf65cee.js} +1 -1
- package/.next/trace +108 -108
- package/.next/trace-build +1 -1
- package/Tools/CUELO_Setup/files/native-runtime-patch.js +1 -1
- package/Tools/OMP_Global_Config/agent/extensions/docmap-route.ts +124 -26
- package/Tools/OMP_Global_Config/agent/extensions/edit-lessons.ts +158 -0
- package/Tools/OMP_Global_Config/agent/extensions/jev-runtime.ts +1 -1
- package/Tools/OMP_Global_Config/agent/extensions/lib/docmap-search.ts +38 -2
- package/Tools/OMP_Global_Config/agent/extensions/lib/memory-excerpt.ts +5 -0
- package/Tools/OMP_Global_Config/agent/extensions/lib/native-judge.ts +82 -0
- package/Tools/OMP_Global_Config/agent/extensions/mcp-selection.ts +11 -78
- package/Tools/OMP_Global_Config/agent/rules/docs-handoff.md +1 -1
- package/Tools/OMP_Global_Config/patches/apply-core-patch.mjs +117 -198
- package/package.json +1 -1
- /package/.next/static/{IKi2VjUzAnD5AUcDn7QNi → MDZ1YH4IJ1RwwO-lRfPFz}/_buildManifest.js +0 -0
- /package/.next/static/{IKi2VjUzAnD5AUcDn7QNi → MDZ1YH4IJ1RwwO-lRfPFz}/_ssgManifest.js +0 -0
package/.next/trace-build
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
[{"name":"run-webpack","duration":
|
|
1
|
+
[{"name":"run-webpack","duration":118341191,"timestamp":692166,"id":14,"parentId":1,"tags":{},"startTime":1792207112919,"traceId":"3028e5e77e38b16d"},{"name":"run-typescript","duration":21384990,"timestamp":119434669,"id":11087,"parentId":1,"tags":{},"startTime":1792207231663,"traceId":"3028e5e77e38b16d"},{"name":"static-check","duration":4272323,"timestamp":140918414,"id":11090,"parentId":1,"tags":{},"startTime":1792207253146,"traceId":"3028e5e77e38b16d"},{"name":"static-generation","duration":13354939,"timestamp":145308193,"id":11262,"parentId":1,"tags":{},"startTime":1792207257536,"traceId":"3028e5e77e38b16d"},{"name":"collect-build-traces","duration":22691837,"timestamp":145285325,"id":11259,"parentId":1,"tags":{},"startTime":1792207257513,"traceId":"3028e5e77e38b16d"},{"name":"telemetry-flush","duration":87,"timestamp":167982010,"id":11271,"parentId":1,"tags":{},"startTime":1792207280210,"traceId":"3028e5e77e38b16d"},{"name":"next-build","duration":167399180,"timestamp":582941,"id":1,"tags":{"buildMode":"default","version":"16.2.12","bundler":"webpack","has-custom-webpack-config":"true","use-build-worker":"false"},"startTime":1792207112810,"traceId":"3028e5e77e38b16d"}]
|
|
@@ -9,7 +9,7 @@ const MEMORY_RE = /(const phase1Model = await resolveMemoryModel\(\{[^}]*?fallba
|
|
|
9
9
|
const MEMORY_DONE = /const phase1Model = await resolveMemoryModel\(\{[^}]*?fallbackRole: "smol"/;
|
|
10
10
|
const EXPECTED = {
|
|
11
11
|
name: 'cuelo',
|
|
12
|
-
version: '0.10.
|
|
12
|
+
version: '0.10.23',
|
|
13
13
|
distribution: 'cuelo',
|
|
14
14
|
coreVersion: '18.11.3',
|
|
15
15
|
};
|
|
@@ -1,34 +1,132 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
2
|
-
import { routeDocmap } from "./lib/docmap-search";
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@oh-my-pi/pi-coding-agent";
|
|
2
|
+
import { type DocmapCandidate, type DocmapRoute, routeDocmap } from "./lib/docmap-search";
|
|
3
|
+
import { type CoreJudgmentModules, defaultLoadCore, JUDGE_STOPPED, type NativeJudge, resolveNativeJudge, stoppedBy } from "./lib/native-judge";
|
|
3
4
|
|
|
4
5
|
// 2026-10-16 재조사(doc/history/2026/10/16-rag-reinvestigation): 격리 표본 12건에서 문서 지도를 연 경우가 0건이었다.
|
|
5
6
|
// 도구나 규칙만 두면 같은 문제가 되풀이되므로, 사용자 요청마다 하네스가 지도·RECORD 헤더 검색을 직접 돌려
|
|
6
7
|
// 후보 기록과 맞는 구간을 그 prompt의 숨은 메시지로 넘긴다. 하한 미만·지도 없음·오류면 아무것도 붙이지 않는다.
|
|
7
8
|
// 안내는 external-advice-check와 같은 이유로 aside가 아니라 그 prompt의 `before_agent_start` 메시지로만 붙인다.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
//
|
|
10
|
+
// 2026-10-17 사용자 결정(doc/history/2026/10/17-docmap-judge): 낱말로 고른 후보의 절반은 주제만 겹치는 지시였다
|
|
11
|
+
// (실제 프롬프트 표본 60건 중 맞는 첨부 29·엉뚱한 첨부 31). 낱말·길이·의도 표지로는 둘이 갈리지 않아, 붙이기 전에
|
|
12
|
+
// native JEV에 후보마다 「요청이 가리키는 그 과거 작업의 기록인가」를 한 요청으로 묻고 P>=0.6인 후보만 붙인다(사용자 선택).
|
|
13
|
+
// 같은 표본에서 맞는 첨부 19/29를 지키고 엉뚱한 첨부는 5/31만 남았다(0.5면 23/29·10/31). 판정 실패·제한 시간·native 판정기 없음은
|
|
14
|
+
// 낱말 후보를 그대로 붙이고(재현율 보존), 실행 중 steering·queued 재요청은 판정하지 않는다(스티어링 지연 방지).
|
|
15
|
+
|
|
16
|
+
export interface DocmapRouteDeps {
|
|
17
|
+
/** 결정적 판정기 seam(테스트). 없으면 {@link loadCore}의 native JEV를 쓴다. */
|
|
18
|
+
resolveJudge?: (ctx: ExtensionContext) => Promise<NativeJudge | undefined>;
|
|
19
|
+
loadCore?: () => Promise<CoreJudgmentModules>;
|
|
20
|
+
/** 판정 대기 상한. 테스트만 짧게 준다. 기본은 DOCMAP_JUDGE_TIMEOUT_MS다. */
|
|
21
|
+
judgeTimeoutMs?: number;
|
|
22
|
+
/** 날짜 의도 판정의 기준 시각(테스트). */
|
|
23
|
+
now?: () => Date;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 판정 대기 상한. 넘으면 낱말 후보를 그대로 붙인다. */
|
|
27
|
+
export const DOCMAP_JUDGE_TIMEOUT_MS = 1_500;
|
|
28
|
+
/** 후보를 남기는 P(관련) 하한. */
|
|
29
|
+
export const DOCMAP_JUDGE_KEEP = 0.6;
|
|
30
|
+
const PROMPT_HEAD_CHARS = 1000;
|
|
31
|
+
const PROMPT_TAIL_CHARS = 1000;
|
|
32
|
+
const CUSTOM_TYPE = "docmap-route";
|
|
33
|
+
|
|
34
|
+
function judgePrompt(prompt: string): string {
|
|
35
|
+
return prompt.length <= PROMPT_HEAD_CHARS + PROMPT_TAIL_CHARS ? prompt : `${prompt.slice(0, PROMPT_HEAD_CHARS)}\n…\n${prompt.slice(-PROMPT_TAIL_CHARS)}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 후보마다 bool 질문 하나. 같은 state를 보므로 한 요청에 묶는다. */
|
|
39
|
+
export function buildDocmapJudgeRequest(prompt: string, route: DocmapRoute): { state: unknown; questions: Record<string, unknown> } {
|
|
40
|
+
const questions: Record<string, unknown> = {};
|
|
41
|
+
route.judgeCandidates.forEach((_, i) => {
|
|
42
|
+
questions[`c${i + 1}`] = {
|
|
43
|
+
type: "bool",
|
|
44
|
+
instructions: `Is candidates[${i}] a record of the earlier work the prompt refers to (the same event, decision, procedure or state), not merely sharing words with it? The prompt is one message a user sent to a coding agent mid-conversation; a new instruction, a reaction or small talk that only shares vocabulary is "no".`,
|
|
45
|
+
criteria: { true: "the prompt asks about or continues the work this record covers", false: "new instruction, reaction, small talk, or only topical word overlap" },
|
|
46
|
+
};
|
|
17
47
|
});
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
48
|
+
return { state: { prompt: judgePrompt(prompt), candidates: route.judgeCandidates }, questions };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 판정 답에서 남길 후보. 답이 없는 후보는 버린다. */
|
|
52
|
+
export function keptByJudge(route: DocmapRoute, answers: Record<string, { bool?: number } | undefined>): DocmapCandidate[] {
|
|
53
|
+
return route.candidates.filter((_, i) => (answers[`c${i + 1}`]?.bool ?? 0) >= DOCMAP_JUDGE_KEEP);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createDocmapRoute(deps: DocmapRouteDeps = {}) {
|
|
57
|
+
const loadCore = deps.loadCore ?? defaultLoadCore;
|
|
58
|
+
const resolveJudge = deps.resolveJudge ?? ((ctx: ExtensionContext) => resolveNativeJudge(ctx, loadCore, "docmap_route"));
|
|
59
|
+
const judgeTimeoutMs = deps.judgeTimeoutMs ?? DOCMAP_JUDGE_TIMEOUT_MS;
|
|
60
|
+
|
|
61
|
+
return function docmapRoute(pi: ExtensionAPI): void {
|
|
62
|
+
let pendingText: string | undefined;
|
|
63
|
+
let judgePromise: Promise<NativeJudge | undefined> | undefined;
|
|
64
|
+
/** agent_start~agent_end 사이. 실행 중 steering·queued 재요청은 판정 없이 붙인다(mcp-selection과 같은 추적). */
|
|
65
|
+
let inRun = false;
|
|
66
|
+
const judgeFor = (ctx: ExtensionContext) => (judgePromise ??= resolveJudge(ctx).catch(() => undefined));
|
|
67
|
+
|
|
68
|
+
pi.on("session_start", () => {
|
|
69
|
+
pendingText = undefined;
|
|
70
|
+
judgePromise = undefined;
|
|
71
|
+
inRun = false;
|
|
72
|
+
});
|
|
73
|
+
pi.on("session_shutdown", () => { pendingText = undefined; });
|
|
74
|
+
pi.on("agent_start", () => { inRun = true; });
|
|
75
|
+
pi.on("agent_end", () => { inRun = false; });
|
|
76
|
+
pi.on("input", (event) => {
|
|
77
|
+
pendingText = undefined;
|
|
78
|
+
if (event.source !== "interactive" && event.source !== "rpc") return;
|
|
79
|
+
const text = event.text.trim();
|
|
80
|
+
if (text) pendingText = text;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/** 판정으로 거른 안내문. 판정 실패·제한 시간이면 낱말 후보 그대로. */
|
|
84
|
+
const filtered = async (route: DocmapRoute, prompt: string, ctx: ExtensionContext): Promise<string> => {
|
|
85
|
+
const controller = new AbortController();
|
|
86
|
+
const stopped = stoppedBy(controller.signal);
|
|
87
|
+
let timedOut = false;
|
|
88
|
+
const timer = setTimeout(() => {
|
|
89
|
+
timedOut = true;
|
|
90
|
+
controller.abort();
|
|
91
|
+
}, judgeTimeoutMs);
|
|
92
|
+
try {
|
|
93
|
+
const judge = await Promise.race([judgeFor(ctx), stopped]);
|
|
94
|
+
if (judge === JUDGE_STOPPED) throw new Error("판정 취소");
|
|
95
|
+
if (!judge) throw new Error("native JEV 판정기 없음");
|
|
96
|
+
const result = await Promise.race([judge.judge(buildDocmapJudgeRequest(prompt, route), { signal: controller.signal }), stopped]);
|
|
97
|
+
if (result === JUDGE_STOPPED) throw new Error("판정 취소");
|
|
98
|
+
return route.render(keptByJudge(route, result.answers));
|
|
99
|
+
} catch (error) {
|
|
100
|
+
pi.logger.debug("docmap-route: JEV 후보 판정 없이 낱말 후보를 붙임", {
|
|
101
|
+
error: timedOut ? "판정이 제한 시간 안에 끝나지 않음" : error instanceof Error ? error.message : String(error),
|
|
102
|
+
});
|
|
103
|
+
return route.text;
|
|
104
|
+
} finally {
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
110
|
+
// 대화형·RPC는 사용자 입력과 같은 본문의 prompt에만 붙인다. CUELO의 재개·업데이트 통지와 goal 계속 같은
|
|
111
|
+
// 내부 prompt는 `input`을 내지 않아(lib/rpc-manager.ts isGenuineUserRequest) 여기서 빠진다.
|
|
112
|
+
// `omp -p`는 input 이벤트가 없어 최상위 세션의 CLI prompt 자체를 사용자 요청으로 본다. CUELO도 확장을
|
|
113
|
+
// mode 없이 초기화해 ctx.mode가 "print"이므로, UI 문맥이 없다는 것까지 봐야 CLI로 가를 수 있다.
|
|
114
|
+
const fromInput = pendingText !== undefined && event.prompt.includes(pendingText);
|
|
115
|
+
const cliPrompt = (ctx.mode === "print" || ctx.mode === "json") && !ctx.hasUI && ctx.agent?.kind === "main";
|
|
116
|
+
if (!fromInput && !cliPrompt) return;
|
|
117
|
+
const prompt = fromInput ? pendingText! : event.prompt;
|
|
118
|
+
let route: DocmapRoute | undefined;
|
|
119
|
+
try {
|
|
120
|
+
route = routeDocmap(ctx.cwd, prompt, deps.now?.());
|
|
121
|
+
} catch {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
28
124
|
if (!route) return;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
125
|
+
const content = inRun ? route.text : await filtered(route, prompt, ctx);
|
|
126
|
+
if (!content) return;
|
|
127
|
+
return { message: { customType: CUSTOM_TYPE, content, display: false, attribution: "agent" } };
|
|
128
|
+
});
|
|
129
|
+
};
|
|
34
130
|
}
|
|
131
|
+
|
|
132
|
+
export default createDocmapRoute();
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { posix, win32 } from "node:path";
|
|
2
|
+
import type { ExtensionAPI, ExtensionContext } from "@oh-my-pi/pi-coding-agent";
|
|
3
|
+
import { MEMORY_SECRET_PATTERN } from "./lib/memory-excerpt";
|
|
4
|
+
|
|
5
|
+
// 2026-10-17: 저장 교훈은 첫 턴 recall(`<memories>`)과 요청별 docmap-route로만 닿는다. 파일 경로에 묶인 교훈
|
|
6
|
+
// (예: `setup.ps1 -ValidateOnly`는 배포 전에 먼저 돌린다)은 그 파일을 고치는 순간에 필요한데, 그때는 어디서도 다시 오르지 않는다
|
|
7
|
+
// (doc/history/2026/10/05-memory-application, 10/04-memory-followup). 그래서 `edit`·`write`가 호출될 때 대상 경로로 기억을 찾아
|
|
8
|
+
// 그 도구 호출 직후 다음 모델 요청에 단서로 붙인다(tool_call `additionalContext`, 비차단).
|
|
9
|
+
// 안내는 단서일 뿐이다. 도구를 막지 않고, 기억 검색이 없거나 실패하거나 제한 시간을 넘으면 아무것도 붙이지 않는다.
|
|
10
|
+
//
|
|
11
|
+
// 질의·필터 근거(doc/history/2026/10/17-edit-lessons, 은행 사본 실측): 경로를 담은 교훈 123건·파일 81개에서 질의는 상대 경로 그대로가
|
|
12
|
+
// basename만이나 둘을 이은 것보다 낫거나 같았다(상위 8개 안 91/80/92쌍). 검색 결과의 content는 Mnemopi 회상 미리보기라 500자에서 잘려 오므로
|
|
13
|
+
// 파일명이 뒤쪽에 있는 교훈은 basename 필터에서 빠진다(상위 8의 91쌍 중 35쌍). 그 필터를 풀면 그 경로를 가리키지 않는 교훈이 대부분이 되어
|
|
14
|
+
// (보인 243건 중 169건) 단서가 묻히므로 필터는 유지한다. 본문은 공백만 정리해 미리보기 그대로 싣는다(`<memories>`도 원문을 싣는다).
|
|
15
|
+
|
|
16
|
+
export interface EditLessonsDeps {
|
|
17
|
+
/** 기억 검색 대기 상한. 테스트만 짧게 준다. 기본은 EDIT_LESSONS_SEARCH_TIMEOUT_MS다. */
|
|
18
|
+
searchTimeoutMs?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 한 호출의 기억 검색 대기 상한. 넘으면 아무것도 붙이지 않고 도구는 그대로 진행한다. */
|
|
22
|
+
export const EDIT_LESSONS_SEARCH_TIMEOUT_MS = 1_500;
|
|
23
|
+
/** 경로 하나에 붙이는 기억 상한. */
|
|
24
|
+
export const EDIT_LESSONS_PER_PATH = 3;
|
|
25
|
+
const SEARCH_LIMIT = 8;
|
|
26
|
+
const LESSON_SOURCES = new Set(["coding-agent-learn", "coding-agent-retain"]);
|
|
27
|
+
|
|
28
|
+
interface MemoryItem {
|
|
29
|
+
id?: string;
|
|
30
|
+
content: string;
|
|
31
|
+
source?: string;
|
|
32
|
+
timestamp?: string;
|
|
33
|
+
score?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 입력의 `paths`(문자열 배열)와 `path`를 입력 순서대로 모은다. */
|
|
37
|
+
function inputPaths(input: unknown): string[] {
|
|
38
|
+
if (!input || typeof input !== "object") return [];
|
|
39
|
+
const record = input as Record<string, unknown>;
|
|
40
|
+
const raw: unknown[] = [...(Array.isArray(record.paths) ? record.paths : []), record.path];
|
|
41
|
+
const paths: string[] = [];
|
|
42
|
+
for (const value of raw) {
|
|
43
|
+
if (typeof value !== "string") continue;
|
|
44
|
+
const trimmed = value.trim();
|
|
45
|
+
if (trimmed && !paths.includes(trimmed)) paths.push(trimmed);
|
|
46
|
+
}
|
|
47
|
+
return paths;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 입력 경로를 cwd 기준으로 정규화한다(역슬래시→`/`, `.`·`..` 정리). cwd 아래면 cwd 기준 `/` 경로, 밖이면 정규화한 절대 경로(`/` 구분).
|
|
52
|
+
* cwd가 없으면 준 문자열을 posix로 정규화만 한다. 같은 파일을 가리키는 다른 표기가 같은 검색·중복 키가 되게 한다.
|
|
53
|
+
*/
|
|
54
|
+
export function relativeTarget(cwd: string | undefined, path: string): string {
|
|
55
|
+
// 호스트 플랫폼이 아니라 cwd(없으면 입력)의 모양으로 경로 규칙을 고른다. Windows 러너에서 POSIX 입력에 드라이브가 붙지 않게 한다.
|
|
56
|
+
const p = /^[A-Za-z]:[\\/]/.test(cwd ?? path) ? win32 : posix;
|
|
57
|
+
const slashed = path.replace(/\\/g, "/");
|
|
58
|
+
if (!cwd) return p.normalize(slashed).replace(/\\/g, "/");
|
|
59
|
+
const abs = p.resolve(cwd, slashed);
|
|
60
|
+
const rel = p.relative(cwd, abs);
|
|
61
|
+
if (!rel || rel === ".." || rel.startsWith(`..${p.sep}`) || p.isAbsolute(rel)) return abs.replace(/\\/g, "/");
|
|
62
|
+
return rel.split(p.sep).join("/");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 검색 결과에서 이 경로에 묶인 교훈 후보만 남긴다(출처·basename 일치·id 필요). 같은 id는 처음 것만 둔다. */
|
|
66
|
+
function lessonItems(items: readonly MemoryItem[], base: string): MemoryItem[] {
|
|
67
|
+
const seen = new Set<string>();
|
|
68
|
+
return items.filter((item) => {
|
|
69
|
+
if (typeof item?.id !== "string" || item.id.length === 0 || seen.has(item.id)) return false;
|
|
70
|
+
if (typeof item.content !== "string" || typeof item.source !== "string" || !LESSON_SOURCES.has(item.source) || !item.content.includes(base)) return false;
|
|
71
|
+
seen.add(item.id);
|
|
72
|
+
return true;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createEditLessons(deps: EditLessonsDeps = {}) {
|
|
77
|
+
const searchTimeoutMs = deps.searchTimeoutMs ?? EDIT_LESSONS_SEARCH_TIMEOUT_MS;
|
|
78
|
+
|
|
79
|
+
return function editLessons(pi: ExtensionAPI): void {
|
|
80
|
+
/** 이 세션에서 이미 찾아 본 경로와 이미 붙인(또는 비밀 패턴으로 가린) 기억 id. */
|
|
81
|
+
const lookedUp = new Set<string>();
|
|
82
|
+
const attached = new Set<string>();
|
|
83
|
+
let sessionGeneration = 0;
|
|
84
|
+
const reset = (): void => {
|
|
85
|
+
sessionGeneration++;
|
|
86
|
+
lookedUp.clear();
|
|
87
|
+
attached.clear();
|
|
88
|
+
};
|
|
89
|
+
pi.on("session_start", reset);
|
|
90
|
+
pi.on("session_shutdown", reset);
|
|
91
|
+
|
|
92
|
+
/** 경로 하나의 후보 기억. 검색 없음·오류·제한 시간·빈 결과는 undefined. */
|
|
93
|
+
const search = async (ctx: ExtensionContext, path: string): Promise<MemoryItem[] | undefined> => {
|
|
94
|
+
const memory = ctx.memory;
|
|
95
|
+
if (!memory) return undefined;
|
|
96
|
+
const controller = new AbortController();
|
|
97
|
+
const { promise: timeout, resolve: expire } = Promise.withResolvers<undefined>();
|
|
98
|
+
const timer = setTimeout(() => {
|
|
99
|
+
controller.abort();
|
|
100
|
+
expire(undefined);
|
|
101
|
+
}, searchTimeoutMs);
|
|
102
|
+
try {
|
|
103
|
+
const result = await Promise.race([memory.search(path, { limit: SEARCH_LIMIT, signal: controller.signal }), timeout]);
|
|
104
|
+
return result?.items?.length ? (result.items as MemoryItem[]) : undefined;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
pi.logger.debug("edit-lessons: 기억 검색 없이 진행", { path, error: error instanceof Error ? error.message : String(error) });
|
|
107
|
+
return undefined;
|
|
108
|
+
} finally {
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
114
|
+
if (event.toolName !== "edit" && event.toolName !== "write") return;
|
|
115
|
+
try {
|
|
116
|
+
// 같은 경로·같은 id는 병렬 호출에서도 한 번만 다루도록 await 전에 표시한다.
|
|
117
|
+
const targets: string[] = [];
|
|
118
|
+
for (const path of inputPaths(event.input)) {
|
|
119
|
+
const target = relativeTarget(ctx.cwd, path);
|
|
120
|
+
if (!target || lookedUp.has(target)) continue;
|
|
121
|
+
lookedUp.add(target);
|
|
122
|
+
targets.push(target);
|
|
123
|
+
}
|
|
124
|
+
if (targets.length === 0) return;
|
|
125
|
+
const generation = sessionGeneration;
|
|
126
|
+
const found = await Promise.all(targets.map((target) => search(ctx, target)));
|
|
127
|
+
// 기다리는 사이 세션이 바뀌었으면(session_start·session_shutdown) 이 결과는 옛 세션 것이라 집합을 건드리지 않고 버린다.
|
|
128
|
+
if (generation !== sessionGeneration) return;
|
|
129
|
+
const sections: string[] = [];
|
|
130
|
+
for (const [index, target] of targets.entries()) {
|
|
131
|
+
const items = found[index];
|
|
132
|
+
if (!items) continue;
|
|
133
|
+
const fresh = lessonItems(items, target.slice(target.lastIndexOf("/") + 1)).filter((item) => !attached.has(item.id!));
|
|
134
|
+
const withheld = fresh.filter((item) => MEMORY_SECRET_PATTERN.test(item.content));
|
|
135
|
+
const shown = fresh.filter((item) => !MEMORY_SECRET_PATTERN.test(item.content))
|
|
136
|
+
.sort((a, b) => (b.score ?? 0) - (a.score ?? 0))
|
|
137
|
+
.slice(0, EDIT_LESSONS_PER_PATH);
|
|
138
|
+
if (shown.length + withheld.length === 0) continue;
|
|
139
|
+
for (const item of [...shown, ...withheld]) attached.add(item.id!);
|
|
140
|
+
const lines = [`[edit-lessons] ${target}에 묶인 기억 ${shown.length + withheld.length}건. 단서일 뿐이며 현재 지시·정본·승인이 우선한다. 원문은 memory://<id>로 읽는다.`];
|
|
141
|
+
for (const item of shown) {
|
|
142
|
+
const date = item.timestamp?.match(/^\d{4}-\d{2}-\d{2}/)?.[0];
|
|
143
|
+
lines.push(`- ${item.content.replace(/\s+/g, " ").trim()} (id: ${item.id}${date ? `, ${date}` : ""})`);
|
|
144
|
+
}
|
|
145
|
+
if (withheld.length > 0) lines.push(`- 비밀 패턴이 있어 보이지 않음: ${withheld.map((item) => item.id).join(", ")}`);
|
|
146
|
+
sections.push(lines.join("\n"));
|
|
147
|
+
}
|
|
148
|
+
if (sections.length === 0) return;
|
|
149
|
+
return { additionalContext: sections.join("\n\n") };
|
|
150
|
+
} catch (error) {
|
|
151
|
+
pi.logger.debug("edit-lessons: 안내 없이 진행", { error: error instanceof Error ? error.message : String(error) });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export default createEditLessons();
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type JobIncarnation,
|
|
11
11
|
type JobObservationClock,
|
|
12
12
|
} from "./lib/job-observation";
|
|
13
|
+
import { MEMORY_SECRET_PATTERN } from "./lib/memory-excerpt";
|
|
13
14
|
import { clearPreparedTaskSession } from "./lib/prepared-task";
|
|
14
15
|
import { createRoutingLedger, DEFAULT_LEDGER_PATH, scopedAttempts, type AttemptIdentity, type DispatchOwnership, type DispatchRecord, type OutcomeRecord, type RevisionRelation, type VerdictRecord } from "./lib/routing-ledger";
|
|
15
16
|
import {
|
|
@@ -836,7 +837,6 @@ function renderTodoProgressAdvice(assessment: TodoProgressAssessment): string {
|
|
|
836
837
|
// emitBeforeAgentStart → 같은 commit으로 메시지 전달). 원문 system prompt·대화·비밀 항목은 보내지 않는다.
|
|
837
838
|
// ---------------------------------------------------------------------------
|
|
838
839
|
|
|
839
|
-
const MEMORY_SECRET_PATTERN = /(?:^|[\s"'`:])(?:password|passwd|api[_ -]?key|bearer|secret|token|credential|client[_ -]?secret)\b|(?:비밀번호|자격증명|인증키|토큰)|(?:sk-[A-Za-z0-9_-]{12,})/i;
|
|
840
840
|
const MEMORY_ITEM_LIMIT = 8;
|
|
841
841
|
const MEMORY_EXCERPT_CHARS = 500;
|
|
842
842
|
const REQUEST_EXCERPT_CHARS = 1200;
|
|
@@ -100,11 +100,24 @@ export interface DocmapSection {
|
|
|
100
100
|
readonly excerpt: string;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/** 후보 하나를 판정기에 보여 주는 모양. 경로·지도 줄·SCOPE·DATE·가장 맞는 절의 발췌만 싣는다. */
|
|
104
|
+
export interface DocmapJudgeCandidate {
|
|
105
|
+
readonly path: string;
|
|
106
|
+
readonly map?: string;
|
|
107
|
+
readonly scope?: string;
|
|
108
|
+
readonly date?: string;
|
|
109
|
+
readonly excerpt?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
103
112
|
export interface DocmapRoute {
|
|
104
113
|
readonly topScore: number;
|
|
105
114
|
readonly signals: DocmapSignals;
|
|
106
115
|
readonly candidates: readonly DocmapCandidate[];
|
|
107
116
|
readonly text: string;
|
|
117
|
+
/** 판정기에 넘길 후보 요약. `candidates`와 같은 순서다. */
|
|
118
|
+
readonly judgeCandidates: readonly DocmapJudgeCandidate[];
|
|
119
|
+
/** 후보 일부만 남겼을 때의 안내문. 빈 배열이면 빈 문자열. */
|
|
120
|
+
render(kept: readonly DocmapCandidate[]): string;
|
|
108
121
|
}
|
|
109
122
|
|
|
110
123
|
/** 소문자 ASCII 낱말(2자 이상)과 한글 연속 구간의 2글자 조각. */
|
|
@@ -529,12 +542,17 @@ function clip(text: string, max: number): string {
|
|
|
529
542
|
return one.length > max ? `${one.slice(0, max - 1)}…` : one;
|
|
530
543
|
}
|
|
531
544
|
|
|
545
|
+
/** 지도 줄에서 링크 대상·머리 기호·표 구분을 빼고 한 줄로 줄인다. */
|
|
546
|
+
function mapLineText(line: string): string {
|
|
547
|
+
return clip(line.replace(/\]\([^)]*\)/g, "]").replace(/^[-*]\s*/, "").replace(/\s*\|\s*/g, " "), 200);
|
|
548
|
+
}
|
|
549
|
+
|
|
532
550
|
export function renderDocmapRoute(corpus: DocmapCorpus, candidates: readonly DocmapCandidate[], sections: ReadonlyMap<string, DocmapSection>, maxChars = DOCMAP_MAX_CHARS): string {
|
|
533
551
|
const blocks = candidates.map((c, i) => {
|
|
534
552
|
const u = c.unit;
|
|
535
553
|
const lines = [`${i + 1}. ${rel(corpus.root, u.file)}${u.fields.STATUS ? ` (STATUS: ${clip(u.fields.STATUS, 60)})` : ""}`];
|
|
536
554
|
if (c.supersededFrom) lines.push(` 대체 전: ${rel(corpus.root, c.supersededFrom)}`);
|
|
537
|
-
if (u.mapLines[0]) lines.push(` 지도: ${
|
|
555
|
+
if (u.mapLines[0]) lines.push(` 지도: ${mapLineText(u.mapLines[0])}`);
|
|
538
556
|
else if (u.title) lines.push(` 제목: ${clip(u.title, 160)}`);
|
|
539
557
|
if (u.fields.SCOPE) lines.push(` SCOPE: ${clip(u.fields.SCOPE, 160)}`);
|
|
540
558
|
const sec = sections.get(u.file);
|
|
@@ -572,5 +590,23 @@ export function routeDocmap(cwd: string, prompt: string, now = new Date()): Docm
|
|
|
572
590
|
if (!docmapPasses(signals)) return undefined;
|
|
573
591
|
const candidates = pickDocmapCandidates(corpus, ranked, DOCMAP_MAX_CANDIDATES, DOCMAP_CANDIDATE_RATIO, docmapDateIntent(prompt, now));
|
|
574
592
|
const sections = bestDocmapSections(candidates.map((c) => c.unit.file), prompt);
|
|
575
|
-
|
|
593
|
+
const judgeCandidates = candidates.map((c) => {
|
|
594
|
+
const u = c.unit;
|
|
595
|
+
const sec = sections.get(u.file);
|
|
596
|
+
return {
|
|
597
|
+
path: rel(corpus.root, u.file),
|
|
598
|
+
...(u.mapLines[0] ? { map: mapLineText(u.mapLines[0]) } : u.title ? { map: clip(u.title, 160) } : {}),
|
|
599
|
+
...(u.fields.SCOPE ? { scope: clip(u.fields.SCOPE, 160) } : {}),
|
|
600
|
+
...(u.fields.DATE ? { date: u.fields.DATE } : {}),
|
|
601
|
+
...(sec ? { excerpt: `${clip(sec.heading, 60)}${sec.excerpt ? ` — ${sec.excerpt}` : ""}` } : {}),
|
|
602
|
+
};
|
|
603
|
+
});
|
|
604
|
+
return {
|
|
605
|
+
topScore: top.score,
|
|
606
|
+
signals,
|
|
607
|
+
candidates,
|
|
608
|
+
text: renderDocmapRoute(corpus, candidates, sections),
|
|
609
|
+
judgeCandidates,
|
|
610
|
+
render: (kept) => (kept.length === 0 ? "" : renderDocmapRoute(corpus, kept, sections)),
|
|
611
|
+
};
|
|
576
612
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// 저장 기억을 판정기나 모델 문맥에 다시 실을 때 비밀을 가리는 공통 패턴. jev-runtime(회상 기억 적용 점검)과
|
|
2
|
+
// edit-lessons(편집 경로 교훈 단서)가 같은 규칙을 쓰도록 한 곳에 둔다.
|
|
3
|
+
|
|
4
|
+
/** 비밀로 보이는 낱말·접두. 걸리면 내용은 싣지 않고 id만 남긴다. */
|
|
5
|
+
export const MEMORY_SECRET_PATTERN = /(?:^|[\s"'`:])(?:password|passwd|api[_ -]?key|bearer|secret|token|credential|client[_ -]?secret)\b|(?:비밀번호|자격증명|인증키|토큰)|(?:sk-[A-Za-z0-9_-]{12,})/i;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@oh-my-pi/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* core의 native JEV를 확장에서 쓰는 공통 틀. mcp-selection과 docmap-route가 같이 쓴다.
|
|
5
|
+
* judge 역할의 첫 후보가 native가 아니면 판정하지 않고, 세션 모델 fallback을 붙이지 않으며(sessionModel 미전달),
|
|
6
|
+
* 체인이 native가 아닌 후보를 내면 그 후보에서는 실행하지 않는다.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface JudgeAnswers {
|
|
10
|
+
answers: Record<string, { type: string; choice?: string; bool?: number } | undefined>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 판정기 seam. 기본 구현은 native 후보에서만 판정을 실행한다. */
|
|
14
|
+
export interface NativeJudge {
|
|
15
|
+
judge(request: { state: unknown; questions: Record<string, unknown> }, options?: { signal?: AbortSignal }): Promise<JudgeAnswers>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type JudgeKind = "native" | "local" | "online";
|
|
19
|
+
|
|
20
|
+
interface ChainJudgeLike {
|
|
21
|
+
withCandidate<T>(run: (judge: NativeJudge, kind: JudgeKind) => Promise<T>, options?: { signal?: AbortSignal }): Promise<T>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 기존 core 판정 모듈. 런타임 값은 host가 해석하고 타입은 여기 필요한 만큼만 둔다. */
|
|
25
|
+
export interface CoreJudgmentModules {
|
|
26
|
+
judgment: {
|
|
27
|
+
resolveJudge(deps: {
|
|
28
|
+
settings: unknown;
|
|
29
|
+
registry: unknown;
|
|
30
|
+
sessionId?: string;
|
|
31
|
+
purpose: string;
|
|
32
|
+
onUsage?: unknown;
|
|
33
|
+
}): ChainJudgeLike;
|
|
34
|
+
hasNativeJudge(settings: unknown, registry: unknown): boolean;
|
|
35
|
+
journalJudgmentUsage?(manager: unknown): unknown;
|
|
36
|
+
};
|
|
37
|
+
settings: { findScopedSettings(cwd?: string): unknown };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function defaultLoadCore(): Promise<CoreJudgmentModules> {
|
|
41
|
+
// 지연 import: 테스트 미러에는 node_modules가 없고, 런타임은 host 번들로 해석한다(jev-runtime과 같다).
|
|
42
|
+
const [judgment, settings] = await Promise.all([
|
|
43
|
+
import("@oh-my-pi/pi-coding-agent/judgment"),
|
|
44
|
+
import("@oh-my-pi/pi-coding-agent/config/settings"),
|
|
45
|
+
]);
|
|
46
|
+
return { judgment, settings } as unknown as CoreJudgmentModules;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function resolveNativeJudge(ctx: ExtensionContext, loadCore: () => Promise<CoreJudgmentModules>, purpose: string): Promise<NativeJudge> {
|
|
50
|
+
const { judgment, settings: settingsModule } = await loadCore();
|
|
51
|
+
const settings = settingsModule.findScopedSettings(ctx.cwd);
|
|
52
|
+
if (!settings) throw new Error("세션 설정을 찾지 못함");
|
|
53
|
+
if (!judgment.hasNativeJudge(settings, ctx.modelRegistry)) throw new Error("judge 역할 첫 후보가 native JEV가 아님");
|
|
54
|
+
const chain = judgment.resolveJudge({
|
|
55
|
+
settings,
|
|
56
|
+
registry: ctx.modelRegistry,
|
|
57
|
+
sessionId: ctx.sessionManager?.getSessionId?.(),
|
|
58
|
+
purpose,
|
|
59
|
+
onUsage: judgment.journalJudgmentUsage?.(ctx.sessionManager),
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
judge: (request, options) =>
|
|
63
|
+
chain.withCandidate(
|
|
64
|
+
(candidate, kind) =>
|
|
65
|
+
kind === "native" ? candidate.judge(request, options) : Promise.reject(new Error(`native가 아닌 judge 후보(${kind})`)),
|
|
66
|
+
options,
|
|
67
|
+
),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 취소·제한 시간이 판정보다 먼저 끝났음을 나타내는 race 표지. */
|
|
72
|
+
export const JUDGE_STOPPED: unique symbol = Symbol("judge-stopped");
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* `signal`이 끊기면 바로 돌아오는 promise. provider가 signal을 무시해도 호출부가 `Promise.race`로 제때 빠져나온다.
|
|
76
|
+
*/
|
|
77
|
+
export function stoppedBy(signal: AbortSignal): Promise<typeof JUDGE_STOPPED> {
|
|
78
|
+
const stopped = Promise.withResolvers<typeof JUDGE_STOPPED>();
|
|
79
|
+
if (signal.aborted) stopped.resolve(JUDGE_STOPPED);
|
|
80
|
+
else signal.addEventListener("abort", () => stopped.resolve(JUDGE_STOPPED), { once: true });
|
|
81
|
+
return stopped.promise;
|
|
82
|
+
}
|
|
@@ -16,6 +16,9 @@ import {
|
|
|
16
16
|
summarizeProjectStack,
|
|
17
17
|
VERIFIED_CATALOG,
|
|
18
18
|
} from "./lib/mcp-selection";
|
|
19
|
+
import { type CoreJudgmentModules, defaultLoadCore, JUDGE_STOPPED, type NativeJudge, resolveNativeJudge, stoppedBy } from "./lib/native-judge";
|
|
20
|
+
|
|
21
|
+
export type { CoreJudgmentModules, NativeJudge } from "./lib/native-judge";
|
|
19
22
|
|
|
20
23
|
/**
|
|
21
24
|
* 요청별 MCP 선택(`mcp.selection: per-request`). 패치된 core가 매 사용자 요청의 system prompt를 만들기
|
|
@@ -26,37 +29,6 @@ import {
|
|
|
26
29
|
* `mcp_select`를 모르는 core(standalone omp 등)에서는 핸들러가 호출되지 않아 아무 일도 하지 않는다.
|
|
27
30
|
*/
|
|
28
31
|
|
|
29
|
-
interface JudgeAnswers {
|
|
30
|
-
answers: Record<string, { type: string; choice?: string } | undefined>;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** 판정기 seam. 기본 구현은 native 후보에서만 판정을 실행한다. */
|
|
34
|
-
export interface NativeJudge {
|
|
35
|
-
judge(request: { state: unknown; questions: Record<string, unknown> }, options?: { signal?: AbortSignal }): Promise<JudgeAnswers>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
type JudgeKind = "native" | "local" | "online";
|
|
39
|
-
|
|
40
|
-
interface ChainJudgeLike {
|
|
41
|
-
withCandidate<T>(run: (judge: NativeJudge, kind: JudgeKind) => Promise<T>, options?: { signal?: AbortSignal }): Promise<T>;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** 기존 core 판정 모듈. 런타임 값은 host가 해석하고 타입은 여기 필요한 만큼만 둔다. */
|
|
45
|
-
export interface CoreJudgmentModules {
|
|
46
|
-
judgment: {
|
|
47
|
-
resolveJudge(deps: {
|
|
48
|
-
settings: unknown;
|
|
49
|
-
registry: unknown;
|
|
50
|
-
sessionId?: string;
|
|
51
|
-
purpose: string;
|
|
52
|
-
onUsage?: unknown;
|
|
53
|
-
}): ChainJudgeLike;
|
|
54
|
-
hasNativeJudge(settings: unknown, registry: unknown): boolean;
|
|
55
|
-
journalJudgmentUsage?(manager: unknown): unknown;
|
|
56
|
-
};
|
|
57
|
-
settings: { findScopedSettings(cwd?: string): unknown };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
32
|
export interface McpSelectionDeps {
|
|
61
33
|
/** 결정적 판정기 seam(테스트). 없으면 {@link loadCore}의 native JEV를 쓴다. */
|
|
62
34
|
resolveJudge?: (ctx: ExtensionContext) => Promise<NativeJudge | undefined>;
|
|
@@ -89,8 +61,6 @@ const STACK_DIR_LIMIT = 40;
|
|
|
89
61
|
/** 새 요청의 판정 대기 상한. 넘으면 판정을 취소하고 판정 실패와 같게 새 연결 없이 진행한다(runner 30초 제한에 기대지 않는다). */
|
|
90
62
|
const MCP_SELECTION_TIMEOUT_MS = 3_000;
|
|
91
63
|
const JUDGE_TIMEOUT_REASON = "판정이 제한 시간 안에 끝나지 않음";
|
|
92
|
-
/** 취소·제한 시간이 판정보다 먼저 끝났음을 나타내는 race 표지. */
|
|
93
|
-
const STOPPED: unique symbol = Symbol("mcp-selection-stopped");
|
|
94
64
|
|
|
95
65
|
async function readJson(path: string): Promise<Record<string, unknown> | undefined> {
|
|
96
66
|
try {
|
|
@@ -167,46 +137,11 @@ async function readProjectStack(cwd: string): Promise<string> {
|
|
|
167
137
|
return summarizeProjectStack({ fileNames, dependencyNames });
|
|
168
138
|
}
|
|
169
139
|
|
|
170
|
-
async function defaultLoadCore(): Promise<CoreJudgmentModules> {
|
|
171
|
-
// 지연 import: 테스트 미러에는 node_modules가 없고, 런타임은 host 번들로 해석한다(jev-runtime과 같다).
|
|
172
|
-
const [judgment, settings] = await Promise.all([
|
|
173
|
-
import("@oh-my-pi/pi-coding-agent/judgment"),
|
|
174
|
-
import("@oh-my-pi/pi-coding-agent/config/settings"),
|
|
175
|
-
]);
|
|
176
|
-
return { judgment, settings } as unknown as CoreJudgmentModules;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* 기존 native JEV만 쓴다. judge 역할의 첫 후보가 native가 아니면 판정하지 않고, 세션 모델 fallback을
|
|
181
|
-
* 붙이지 않으며(sessionModel 미전달), 체인이 native가 아닌 후보를 내면 그 후보에서는 실행하지 않는다.
|
|
182
|
-
*/
|
|
183
|
-
async function resolveNativeJudge(ctx: ExtensionContext, loadCore: () => Promise<CoreJudgmentModules>): Promise<NativeJudge> {
|
|
184
|
-
const { judgment, settings: settingsModule } = await loadCore();
|
|
185
|
-
const settings = settingsModule.findScopedSettings(ctx.cwd);
|
|
186
|
-
if (!settings) throw new Error("세션 설정을 찾지 못함");
|
|
187
|
-
if (!judgment.hasNativeJudge(settings, ctx.modelRegistry)) throw new Error("judge 역할 첫 후보가 native JEV가 아님");
|
|
188
|
-
const chain = judgment.resolveJudge({
|
|
189
|
-
settings,
|
|
190
|
-
registry: ctx.modelRegistry,
|
|
191
|
-
sessionId: ctx.sessionManager?.getSessionId?.(),
|
|
192
|
-
purpose: "mcp_selection",
|
|
193
|
-
onUsage: judgment.journalJudgmentUsage?.(ctx.sessionManager),
|
|
194
|
-
});
|
|
195
|
-
return {
|
|
196
|
-
judge: (request, options) =>
|
|
197
|
-
chain.withCandidate(
|
|
198
|
-
(candidate, kind) =>
|
|
199
|
-
kind === "native" ? candidate.judge(request, options) : Promise.reject(new Error(`native가 아닌 judge 후보(${kind})`)),
|
|
200
|
-
options,
|
|
201
|
-
),
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
140
|
export function createMcpSelection(deps: McpSelectionDeps = {}) {
|
|
206
141
|
const agentDir = deps.agentDir ?? join(import.meta.dir, "..");
|
|
207
142
|
const catalog = deps.catalog ?? VERIFIED_CATALOG;
|
|
208
143
|
const loadCore = deps.loadCore ?? defaultLoadCore;
|
|
209
|
-
const resolveJudge = deps.resolveJudge ?? ((ctx: ExtensionContext) => resolveNativeJudge(ctx, loadCore));
|
|
144
|
+
const resolveJudge = deps.resolveJudge ?? ((ctx: ExtensionContext) => resolveNativeJudge(ctx, loadCore, "mcp_selection"));
|
|
210
145
|
const judgeTimeoutMs = deps.judgeTimeoutMs ?? MCP_SELECTION_TIMEOUT_MS;
|
|
211
146
|
|
|
212
147
|
return function mcpSelection(pi: ExtensionAPI): void {
|
|
@@ -280,22 +215,20 @@ export function createMcpSelection(deps: McpSelectionDeps = {}) {
|
|
|
280
215
|
const answers: Record<string, string | undefined> = {};
|
|
281
216
|
const signal = event.signal ? AbortSignal.any([event.signal, token.controller.signal]) : token.controller.signal;
|
|
282
217
|
// provider가 signal을 무시해도 취소·제한 시간에 바로 돌아온다.
|
|
283
|
-
const stopped =
|
|
284
|
-
if (signal.aborted) stopped.resolve(STOPPED);
|
|
285
|
-
else signal.addEventListener("abort", () => stopped.resolve(STOPPED), { once: true });
|
|
218
|
+
const stopped = stoppedBy(signal);
|
|
286
219
|
let timedOut = false;
|
|
287
220
|
const timer = setTimeout(() => {
|
|
288
221
|
timedOut = true;
|
|
289
222
|
token.controller.abort();
|
|
290
223
|
}, judgeTimeoutMs);
|
|
291
224
|
try {
|
|
292
|
-
const judge = await Promise.race([judgeFor(ctx), stopped
|
|
293
|
-
if (judge ===
|
|
225
|
+
const judge = await Promise.race([judgeFor(ctx), stopped]);
|
|
226
|
+
if (judge === JUDGE_STOPPED) throw new Error("판정 취소");
|
|
294
227
|
if (!judge) throw new Error("JEV 판정기를 해석하지 못함");
|
|
295
|
-
const stack = await Promise.race([projectStack, stopped
|
|
296
|
-
if (stack ===
|
|
297
|
-
const result = await Promise.race([judge.judge(buildJudgeRequest(cue, stack, candidates), { signal }), stopped
|
|
298
|
-
if (result ===
|
|
228
|
+
const stack = await Promise.race([projectStack, stopped]);
|
|
229
|
+
if (stack === JUDGE_STOPPED) throw new Error("판정 취소");
|
|
230
|
+
const result = await Promise.race([judge.judge(buildJudgeRequest(cue, stack, candidates), { signal }), stopped]);
|
|
231
|
+
if (result === JUDGE_STOPPED) throw new Error("판정 취소");
|
|
299
232
|
for (const [key, answer] of Object.entries(result.answers)) {
|
|
300
233
|
answers[key] = answer?.type === "choice" ? answer.choice : undefined;
|
|
301
234
|
}
|