agentlas 1.0.28 → 1.0.29
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/CHANGELOG.md +33 -0
- package/README.md +5 -2
- package/engine/agentlas-i18n.cjs +4 -4
- package/engine/agentlas-input.cjs +0 -2
- package/engine/agentlas-onboard.cjs +20 -0
- package/engine/agentlas-workforce.cjs +41 -8
- package/engine/agentlas.cjs +7 -1
- package/engine/commands/billing.cjs +3 -0
- package/engine/commands/creds.cjs +49 -1
- package/engine/commands/doctor.cjs +46 -3
- package/engine/commands/graph.cjs +1150 -0
- package/engine/commands/help.cjs +82 -6
- package/engine/commands/hep-cloud.cjs +9 -23
- package/engine/commands/hep-hub.cjs +9 -22
- package/engine/commands/hep-local.cjs +9 -24
- package/engine/commands/hep-network.cjs +9 -35
- package/engine/commands/index.cjs +49 -25
- package/engine/commands/mcp.cjs +6 -2
- package/engine/commands/native.cjs +18 -2
- package/engine/commands/plugin.cjs +22 -0
- package/engine/commands/roles.cjs +202 -0
- package/engine/commands/workforce.cjs +63 -12
- package/engine/graph/ask-model.cjs +131 -0
- package/engine/graph/interview.cjs +875 -0
- package/engine/graph/layout.cjs +137 -0
- package/engine/graph/package.cjs +223 -0
- package/engine/graph/vocabulary.generated.cjs +30 -0
- package/engine/hephaestus/local-core.cjs +159 -0
- package/engine/hephaestus/runtime.cjs +4 -8
- package/engine/runtimes/auth-evidence.cjs +78 -0
- package/engine/sessions/prompt.cjs +16 -0
- package/engine/sessions/session.cjs +9 -0
- package/engine/tools/access-notice.cjs +86 -0
- package/engine/ui/palette.cjs +6 -3
- package/engine/ui/repl.cjs +8 -2
- package/engine/workforce/deps.cjs +13 -0
- package/engine/workforce/local-core-transport.cjs +298 -0
- package/package.json +4 -3
- package/engine/commands/legacy-network.cjs +0 -29
|
@@ -190,6 +190,22 @@ function augmentSystem(db, baseSystem, ctx, withEmitter, request = "") {
|
|
|
190
190
|
if (connectionSkill) sys += "\n\n" + connectionSkill;
|
|
191
191
|
const mem = cliMemoryContext(db, ctx && ctx.projectPath, ctx && ctx.agentId, request);
|
|
192
192
|
if (mem) sys += "\n\n" + mem;
|
|
193
|
+
// 도구 접근 고지 — 터미널에는 이게 아예 없었다. 도구가 붙지 않은 턴에서 CLI는 아무
|
|
194
|
+
// 말도 하지 않았고, 에이전트는 "이 기계엔 도구가 없다"고 단정하거나 없는 도구를
|
|
195
|
+
// 불렀다. Desktop `shared/tool-access-notice.ts`와 같은 문장을 낸다(패리티 테스트로 고정).
|
|
196
|
+
//
|
|
197
|
+
// ★메모리 블록보다 **앞**에 둔다. 메모리 코어 예산은 `## Memory` 이후를 잘라서 재므로
|
|
198
|
+
// (test/memory-prompt-budget.cjs), 뒤에 붙이면 도구 고지가 메모리 예산으로 잘못 계산된다.
|
|
199
|
+
// 고지는 메모리 블록의 일부가 아니다.
|
|
200
|
+
try {
|
|
201
|
+
const { buildToolAccessNotice } = require("../tools/access-notice.cjs");
|
|
202
|
+
sys += "\n\n" + buildToolAccessNotice({
|
|
203
|
+
availableTools: (ctx && Array.isArray(ctx.availableTools)) ? ctx.availableTools : [],
|
|
204
|
+
// 터미널은 Hub 카탈로그를 hephaestus-network MCP로만 본다. 그 서버가 이번 턴에
|
|
205
|
+
// 붙지 않았으면 "찾아보라"고 말하면 안 된다 — 부를 수 없는 도구를 안내하는 셈이다.
|
|
206
|
+
hubCatalogAvailable: Boolean(ctx && ctx.hubCatalogAvailable),
|
|
207
|
+
});
|
|
208
|
+
} catch { /* 고지 실패가 턴을 막지 않는다 */ }
|
|
193
209
|
if (withEmitter) {
|
|
194
210
|
sys += "\n\n" + memoryEmitterPromptFor(request, arch, ctx && ctx.turnId, ctx && ctx.permission);
|
|
195
211
|
const credentialReminder = credentialIndexReminderFor(request);
|
|
@@ -205,12 +205,21 @@ class Session extends EventEmitter {
|
|
|
205
205
|
try {
|
|
206
206
|
const { augmentSystem } = require("./prompt.cjs");
|
|
207
207
|
const projectPath = governedTurn ? governedTurn.projectPath : memoryTurn.initializedProjectPath(this.cwd);
|
|
208
|
+
// 이번 턴에 실제로 붙는 도구를 고지에 넘긴다. 권한이 낮아 목록을 읽지 않는 턴은
|
|
209
|
+
// 빈 배열이 되고, 고지는 "붙은 도구가 없다"고 정확히 말한다 — 침묵하지 않는다.
|
|
210
|
+
const consentedServers = this._consentedMcpServers();
|
|
211
|
+
const toolNames = consentedServers
|
|
212
|
+
.map((server) => String((server && (server.name || server.id)) || "").trim())
|
|
213
|
+
.filter(Boolean);
|
|
208
214
|
systemPrompt = augmentSystem(this.db, systemPrompt, {
|
|
209
215
|
lang: this.lang,
|
|
210
216
|
projectPath,
|
|
211
217
|
agentId: this.agent.id,
|
|
212
218
|
turnId: governedTurn && governedTurn.memoryTurn.turnId,
|
|
213
219
|
permission: this.permission,
|
|
220
|
+
availableTools: toolNames,
|
|
221
|
+
// Hub 카탈로그는 hephaestus-network MCP를 통해서만 보인다.
|
|
222
|
+
hubCatalogAvailable: toolNames.some((name) => /hephaestus[-_]?network|agentlas/i.test(name)),
|
|
214
223
|
}, true, prompt);
|
|
215
224
|
} catch { /* 프롬프트 증강 실패는 턴을 막지 않는다 — 원 프롬프트로 진행 */ }
|
|
216
225
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// 도구 접근 고지 — Desktop `shared/tool-access-notice.ts`의 터미널 미러.
|
|
2
|
+
//
|
|
3
|
+
// 정본은 Desktop 쪽이고, 이 파일은 **같은 문장을 내야 한다**. 두 벌이 갈라지면 같은
|
|
4
|
+
// 제품이 표면마다 다른 말을 하게 되고, 사용자는 어느 쪽이 맞는지 알 수 없다.
|
|
5
|
+
// `test/tool-access-notice-parity.cjs`가 두 구현을 같은 입력으로 돌려 대조한다.
|
|
6
|
+
//
|
|
7
|
+
// 터미널에는 이 고지가 **아예 없었다**. 도구가 붙지 않은 실행에서 CLI는 아무 말도 하지
|
|
8
|
+
// 않았고, 에이전트는 "이 기계엔 도구가 없다"고 단정하거나 없는 도구를 부르거나 그냥
|
|
9
|
+
// 침묵했다. 도구가 없을 때가 안내가 가장 필요한 순간이다.
|
|
10
|
+
|
|
11
|
+
"use strict";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_RESOLVE_TOOL = "agentlas_resolve_plugins";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 모든 표면이 공유하는 도구 접근 고지.
|
|
17
|
+
* 절대 빈 문자열을 반환하지 않는다 — 붙은 도구가 없다는 사실 자체가 정보다.
|
|
18
|
+
*
|
|
19
|
+
* @param {{
|
|
20
|
+
* availableTools: string[],
|
|
21
|
+
* blockedTools?: string[],
|
|
22
|
+
* pendingApprovalTools?: string[],
|
|
23
|
+
* hubCatalogAvailable: boolean,
|
|
24
|
+
* hubCatalogError?: string|null,
|
|
25
|
+
* resolveToolName?: string,
|
|
26
|
+
* }} input
|
|
27
|
+
* @returns {string}
|
|
28
|
+
*/
|
|
29
|
+
function buildToolAccessNotice(input) {
|
|
30
|
+
const resolveTool = (input.resolveToolName || "").trim() || DEFAULT_RESOLVE_TOOL;
|
|
31
|
+
const clean = (list) => (Array.isArray(list) ? list : []).filter((name) => String(name || "").trim().length > 0);
|
|
32
|
+
const available = clean(input.availableTools);
|
|
33
|
+
const blocked = clean(input.blockedTools);
|
|
34
|
+
const pending = clean(input.pendingApprovalTools);
|
|
35
|
+
const lines = [];
|
|
36
|
+
|
|
37
|
+
lines.push(
|
|
38
|
+
available.length > 0
|
|
39
|
+
? `Tools available in this run: ${available.join(", ")}.`
|
|
40
|
+
: "No tools are connected in this run.",
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (pending.length > 0) {
|
|
44
|
+
lines.push(
|
|
45
|
+
`Already attached but switched off, waiting for the user to approve local execution: ${pending.join(", ")}. ` +
|
|
46
|
+
"Ask the user to approve it instead of installing anything new.",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (input.hubCatalogAvailable) {
|
|
51
|
+
lines.push(
|
|
52
|
+
`Before telling the user a capability is unavailable, call ${resolveTool} with the capability you need. ` +
|
|
53
|
+
"The Agentlas Hub catalog covers integrations that are not installed here yet.",
|
|
54
|
+
);
|
|
55
|
+
} else if (input.hubCatalogError) {
|
|
56
|
+
lines.push(
|
|
57
|
+
`The Agentlas Hub catalog could not be reached this run (${input.hubCatalogError}). ` +
|
|
58
|
+
"Say that the catalog is unreachable rather than that no such tool exists.",
|
|
59
|
+
);
|
|
60
|
+
} else {
|
|
61
|
+
lines.push(
|
|
62
|
+
"The Agentlas Hub catalog is not reachable from this surface. " +
|
|
63
|
+
"Say what you cannot do and why; do not claim a capability that is not connected.",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
lines.push(
|
|
68
|
+
"Never install or enable a tool on your own. Show the slug, what it will be allowed to do, " +
|
|
69
|
+
"and whether it needs credentials, then let the user decide.",
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (blocked.length > 0) {
|
|
73
|
+
lines.push(
|
|
74
|
+
`Matched but unusable until credentials are set: ${blocked.join(", ")}. ` +
|
|
75
|
+
"Ask for those only if this task actually needs them.",
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
lines.push(
|
|
80
|
+
"If nothing covers the need, say so plainly. Do not describe a tool call you did not make.",
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return lines.join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { buildToolAccessNotice, DEFAULT_RESOLVE_TOOL };
|
package/engine/ui/palette.cjs
CHANGED
|
@@ -21,6 +21,7 @@ const SLASH_COMMANDS = [
|
|
|
21
21
|
{ command: "/rm", args: "<n>", ko: "세션 제거", en: "Remove a session" },
|
|
22
22
|
{ command: "/agents", args: "", ko: "설치 에이전트 목록", en: "List installed agents" },
|
|
23
23
|
{ command: "/list", args: "", ko: "설치 에이전트 목록", en: "List installed agents" },
|
|
24
|
+
{ command: "/graph", args: "[run <이름>]", ko: "저장된 자동화 그래프", en: "Saved automation graphs" },
|
|
24
25
|
{ command: "/mcp", args: "", ko: "MCP 서버 목록", en: "MCP servers" },
|
|
25
26
|
{ command: "/doctor", args: "", ko: "런타임·데이터 점검", en: "Health check" },
|
|
26
27
|
{ command: "/runtime", args: "<kind>", ko: "새 세션 런타임 지정", en: "Set runtime for new sessions" },
|
|
@@ -39,9 +40,10 @@ const SLASH_COMMANDS = [
|
|
|
39
40
|
{ command: "/network", args: "<request>", ko: "Workforce 라우트", en: "Workforce route" },
|
|
40
41
|
{ command: "/workforce", args: "<request>", ko: "Workforce 라우트", en: "Workforce route" },
|
|
41
42
|
{ command: "/taskforce", args: "<request>", ko: "임시 태스크포스 편성", en: "Assemble a task force" },
|
|
42
|
-
// 소스
|
|
43
|
-
//
|
|
44
|
-
//
|
|
43
|
+
// 소스 스코프 편성 4종 — 2026-08-05 같은 날 삭제 후 네이티브 배선으로 복원.
|
|
44
|
+
// 이전에는 외부 CLI 스텁(exit 3)이라 죽은 메뉴였다. 지금은 이 터미널의 편성
|
|
45
|
+
// 루프가 직접 돌고 로컬 Agentlas-OS Core가 선언된 스코프의 메뉴를 연합한다.
|
|
46
|
+
// 팔레트 문구가 곧 사용자에게 하는 약속이다 — 스코프를 문장에 적는다.
|
|
45
47
|
{ command: "/hep-network", args: "\"<request>\"", ko: "로컬+오너 클라우드+공개 Hub 연합 편성", en: "Staff across Local + owner Cloud + public Hub" },
|
|
46
48
|
{ command: "/hep-local", args: "\"<request>\"", ko: "등록된 로컬 에이전트만으로 편성", en: "Staff from registered Local agents only" },
|
|
47
49
|
{ command: "/hep-cloud", args: "\"<request>\"", ko: "오너 Agent Cloud만으로 편성", en: "Staff from owner Agent Cloud only" },
|
|
@@ -75,6 +77,7 @@ const SLASH_COMMANDS = [
|
|
|
75
77
|
{ command: "/creds", args: "<sub>", ko: "자격증명", en: "Credentials" },
|
|
76
78
|
{ command: "/env", args: "", ko: "공유 환경 키", en: "Shared env keys" },
|
|
77
79
|
{ command: "/multimodal", args: "", ko: "이미지·영상·음성 설정", en: "Image/video/audio providers" },
|
|
80
|
+
{ command: "/roles", args: "[set <role> <runtime>]", ko: "오케스트레이터·워커 모델 역할 조회/설정", en: "Show or set orchestrator/worker model roles" },
|
|
78
81
|
{ command: "/telegram", args: "[sub]", ko: "텔레그램 연결", en: "Telegram bindings" },
|
|
79
82
|
{ command: "/oberon", args: "[sub]", ko: "AI 필름", en: "AI film" },
|
|
80
83
|
{ command: "/film", args: "<sub>", ko: "필름 렌더", en: "Film render" },
|
package/engine/ui/repl.cjs
CHANGED
|
@@ -585,8 +585,12 @@ function handleSlash(ctx, cmdline, api) {
|
|
|
585
585
|
|
|
586
586
|
case "runtime": {
|
|
587
587
|
if (!rest[0]) throw new Error("Usage: /runtime claude-code|codex|gemini");
|
|
588
|
+
// 세션 오버라이드는 저장되지 않는다 — 고지 없이는 사용자가 영구 설정으로
|
|
589
|
+
// 믿는다(2026-08-05 감사 결함 C). 영구 경로를 같은 줄에서 알려준다.
|
|
588
590
|
api.setRuntime(rest[0]);
|
|
589
|
-
ctx.out(ui.c.dim(`runtime: ${rest[0]} (${en
|
|
591
|
+
ctx.out(ui.c.dim(`runtime: ${rest[0]} (${en
|
|
592
|
+
? "new sessions in this REPL only — persist with: agentlas roles set orchestrator " + rest[0]
|
|
593
|
+
: "이 REPL의 새 세션 한정 — 영구 설정: agentlas roles set orchestrator " + rest[0]})`));
|
|
590
594
|
return;
|
|
591
595
|
}
|
|
592
596
|
case "model": {
|
|
@@ -595,7 +599,9 @@ function handleSlash(ctx, cmdline, api) {
|
|
|
595
599
|
const next = ["default", "inherit"].includes(model.toLowerCase()) ? null : model;
|
|
596
600
|
api.setModel(next);
|
|
597
601
|
ctx.out(ui.c.dim(
|
|
598
|
-
`model: ${next || "default"} (${en
|
|
602
|
+
`model: ${next || "default"} (${en
|
|
603
|
+
? "new sessions in this REPL only — persist with: agentlas roles set"
|
|
604
|
+
: "이 REPL의 새 세션 한정 — 영구 설정: agentlas roles set"})`,
|
|
599
605
|
));
|
|
600
606
|
return;
|
|
601
607
|
}
|
|
@@ -579,9 +579,22 @@ function workforceRuntime(ctx = {}) {
|
|
|
579
579
|
return _workforce;
|
|
580
580
|
}
|
|
581
581
|
|
|
582
|
+
/*
|
|
583
|
+
* 로컬 Core 전송을 실은 1회용 런타임 (2026-08-05, hep-* 네이티브 배선).
|
|
584
|
+
* 싱글턴을 쓰지 않는 이유: D.callHubTool은 명령 수명의 stdio 프로세스와 validate
|
|
585
|
+
* 계보 상태를 붙잡는다 — 공유하면 다음 편성이 앞 편성의 계보를 이어받는다.
|
|
586
|
+
* 원격 기본 경로(workforceRuntime)는 여기서 아무것도 바뀌지 않는다.
|
|
587
|
+
*/
|
|
588
|
+
function createLocalCoreWorkforceRuntime(ctx, transport) {
|
|
589
|
+
const deps = buildWorkforceDeps(ctx);
|
|
590
|
+
deps.callHubTool = transport.callHubTool;
|
|
591
|
+
return require("../agentlas-workforce.cjs").create(deps);
|
|
592
|
+
}
|
|
593
|
+
|
|
582
594
|
module.exports = {
|
|
583
595
|
buildWorkforceDeps,
|
|
584
596
|
workforceRuntime,
|
|
597
|
+
createLocalCoreWorkforceRuntime,
|
|
585
598
|
resolveWorkforceRuntime,
|
|
586
599
|
receiptFile,
|
|
587
600
|
appendReceipt,
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* workforce/local-core-transport — 편성 루프(agentlas-workforce.cjs)를 로컬
|
|
4
|
+
* Agentlas Core(연합 소유자)에 잇는 어댑터.
|
|
5
|
+
*
|
|
6
|
+
* 전선 계약 — 전부 2026-08-05 실측(모델 호출 0회 프로브)으로 확정:
|
|
7
|
+
* search_candidates 요청 {workOrder, sourceScope}
|
|
8
|
+
* 응답 agentlas.workforce-federation-result.v1 봉투
|
|
9
|
+
* → 루프에는 봉투를 벗긴 candidateSet만 준다.
|
|
10
|
+
* validate_selection 요청 {workOrder, selection, candidateSet}
|
|
11
|
+
* (federationResult를 실으면 invalid_federation_result —
|
|
12
|
+
* Core는 자기 선택 세션에서 연합을 이미 안다)
|
|
13
|
+
* 응답 안의 selectionValidation이 정확히
|
|
14
|
+
* agentlas.workforce-selection-validation.v1 — 루프 검증기와
|
|
15
|
+
* 동일 계약이라 그대로 돌려준다. 원본 응답은 여기 상태로
|
|
16
|
+
* 붙잡아 둔다(prepare가 요구).
|
|
17
|
+
* prepare_execution 요청 {workOrder, selection, candidateSet,
|
|
18
|
+
* federatedSelection: <validate 원본 응답>,
|
|
19
|
+
* validationReceipt: <동일>, projectDir}
|
|
20
|
+
* 응답 안의 executionPlan이 정확히
|
|
21
|
+
* agentlas.workforce-execution-plan.v5 (roster에
|
|
22
|
+
* directiveBundle·permissionPolicy 동봉) — 그대로 돌려준다.
|
|
23
|
+
*
|
|
24
|
+
* 원격(agentlas.cloud) 경로는 건드리지 않는다: 이 어댑터는 D.callHubTool로
|
|
25
|
+
* 주입될 때만 산다. Core 거절 코드는 원문 그대로 전파된다(local-core.cjs 계약).
|
|
26
|
+
*/
|
|
27
|
+
const crypto = require("node:crypto");
|
|
28
|
+
const { createLocalCoreClient } = require("../hephaestus/local-core.cjs");
|
|
29
|
+
|
|
30
|
+
const SOURCE_SCOPES = new Set(["network", "local", "cloud", "hub"]);
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
* ── id 정규화 (실측 2026-08-05 라이브 런 실패의 수리) ──
|
|
34
|
+
*
|
|
35
|
+
* 리더 LLM은 사람이 읽는 id를 짓는다(work-order:korean-summary-…-20260805,
|
|
36
|
+
* slotId "korean-doc-writer"). 원격 서버는 받아주지만 로컬 Core 경계는 finite id
|
|
37
|
+
* 정책으로 거절한다 — 실측 issues: work_order_id_not_public_finite,
|
|
38
|
+
* slot_id_not_public_finite. novel id 의 유효 형식은 <ns>:opaque-<64hex> 다.
|
|
39
|
+
*
|
|
40
|
+
* 그래서 Core 로 나가는 모든 workOrderId·slotId 를 결정론적 opaque 로 정규화하고
|
|
41
|
+
* (sha256(원본) — 같은 원본은 항상 같은 opaque, 재검색·재개에도 안정), Core 에서
|
|
42
|
+
* 들어오는 응답의 그 값들을 원본으로 역치환한다. 루프·리더 프롬프트·영수증은
|
|
43
|
+
* 사람이 읽는 원본만 본다. 치환은 "문자열 값의 정확 일치"로만 한다 — 부분 문자열
|
|
44
|
+
* 치환은 다이제스트·서술문을 오염시킨다.
|
|
45
|
+
*/
|
|
46
|
+
const sha256hex = (value) => crypto.createHash("sha256").update(String(value)).digest("hex");
|
|
47
|
+
const OPAQUE_RE = /^[a-z][a-z-]*:(?:opaque-[0-9a-f]{64}|ordinal-\d+)$/;
|
|
48
|
+
|
|
49
|
+
function opaqueId(namespace, id) {
|
|
50
|
+
return OPAQUE_RE.test(id) ? id : `${namespace}:opaque-${sha256hex(id)}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** workOrder에서 forward(원본→opaque)/reverse(opaque→원본) 값 지도를 만든다. */
|
|
54
|
+
function buildIdMaps(workOrder) {
|
|
55
|
+
const forward = new Map();
|
|
56
|
+
const reverse = new Map();
|
|
57
|
+
const add = (namespace, id) => {
|
|
58
|
+
if (typeof id !== "string" || !id) return;
|
|
59
|
+
const mapped = opaqueId(namespace, id);
|
|
60
|
+
if (mapped === id) return;
|
|
61
|
+
forward.set(id, mapped);
|
|
62
|
+
reverse.set(mapped, id);
|
|
63
|
+
};
|
|
64
|
+
add("work-order", workOrder.workOrderId);
|
|
65
|
+
for (const slot of workOrder.roleSlots || []) add("slot", slot.slotId);
|
|
66
|
+
return { forward, reverse };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/*
|
|
70
|
+
* 경계 거절 issues 의 path("edges[0].artifactKinds[0]")에서 실제 값을 찾는다.
|
|
71
|
+
* 선제 정규화(workOrderId·slotId)가 못 덮는 finite 어휘가 있다 — 실측:
|
|
72
|
+
* artifactKinds 는 finite 카탈로그다(artifact_concept_not_public_finite). 카탈로그를
|
|
73
|
+
* 하드코딩하면 Core 업데이트마다 어긋나므로, 거절이 지목한 값만 opaque 화해
|
|
74
|
+
* 1회 재시도한다. role/skill/community 는 open-world 라 여기 올 일이 없다.
|
|
75
|
+
*/
|
|
76
|
+
function valueAtPath(root, issuePath) {
|
|
77
|
+
const segments = String(issuePath || "").match(/[A-Za-z_][A-Za-z0-9_]*|\[\d+\]/g) || [];
|
|
78
|
+
let current = root;
|
|
79
|
+
for (const segment of segments) {
|
|
80
|
+
if (current == null) return undefined;
|
|
81
|
+
current = segment.startsWith("[") ? current[Number(segment.slice(1, -1))] : current[segment];
|
|
82
|
+
}
|
|
83
|
+
return current;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function boundaryIssues(error) {
|
|
87
|
+
const issues = error?.detail?.boundary?.issues;
|
|
88
|
+
return Array.isArray(issues) ? issues : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 깊은 순회로 문자열 값을 정확 일치 치환한 사본을 만든다. */
|
|
92
|
+
function mapDeep(value, map) {
|
|
93
|
+
if (map.size === 0) return value;
|
|
94
|
+
if (typeof value === "string") return map.get(value) || value;
|
|
95
|
+
if (Array.isArray(value)) return value.map((item) => mapDeep(item, map));
|
|
96
|
+
if (value && typeof value === "object") {
|
|
97
|
+
const out = {};
|
|
98
|
+
for (const [key, item] of Object.entries(value)) out[key] = mapDeep(item, map);
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function createLocalCoreHubTool({ sourceScope, projectDir, cwd, client } = {}) {
|
|
105
|
+
if (!SOURCE_SCOPES.has(sourceScope)) {
|
|
106
|
+
const error = new Error(`sourceScope must be network|local|cloud|hub, got: ${String(sourceScope)}`);
|
|
107
|
+
error.code = "source_scope_invalid";
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
if (!projectDir || typeof projectDir !== "string") {
|
|
111
|
+
const error = new Error("projectDir is required — local Core prepare_execution binds the preparation to a project");
|
|
112
|
+
error.code = "project_dir_required";
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
const core = client || createLocalCoreClient({ cwd: cwd || projectDir });
|
|
116
|
+
// 편성 계보 상태 — 전부 "Core 어휘"(opaque id) 원본이다:
|
|
117
|
+
// maps: 마지막 search 의 id 지도. 재검색(refinement)마다 재구성된다.
|
|
118
|
+
// coreCandidateSet: Core 가 준 원본 CandidateSet (validate/prepare 로 무수정 반송).
|
|
119
|
+
// lastValidationEnvelope: validate 원본 응답. prepare 의 federatedSelection 은
|
|
120
|
+
// 이것이어야 한다 — 루프가 들고 있는 것은 벗겨낸 selectionValidation 뿐이다.
|
|
121
|
+
let maps = { forward: new Map(), reverse: new Map() };
|
|
122
|
+
let coreCandidateSet = null;
|
|
123
|
+
let lastValidationEnvelope = null;
|
|
124
|
+
// validate 가 실제로 수락한 Core 어휘 selection. prepare 는 정확히 이 본을
|
|
125
|
+
// 반송해야 한다 — 반응형 reasonCode 수리가 있었으면 루프의 selection 을 다시
|
|
126
|
+
// 변환한 것과 다르다(exact binding).
|
|
127
|
+
let lastCoreSelection = null;
|
|
128
|
+
|
|
129
|
+
const invalid = (message) => {
|
|
130
|
+
const error = new Error(message);
|
|
131
|
+
error.code = "local_core_invalid_response";
|
|
132
|
+
return error;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
async function callHubTool(name, args) {
|
|
136
|
+
if (name === "workforce.search_candidates") {
|
|
137
|
+
maps = buildIdMaps(args.workOrder);
|
|
138
|
+
coreCandidateSet = null;
|
|
139
|
+
lastValidationEnvelope = null;
|
|
140
|
+
// fullDossier: 터미널 루프의 후보 검증기는 legacy full-echo 계약이다
|
|
141
|
+
// (qualificationEvidence·packageHash·contentDigest 필수 — 실측: 기본
|
|
142
|
+
// reference-first 메뉴는 candidate_set_invalid 로 거절된다).
|
|
143
|
+
let envelope;
|
|
144
|
+
try {
|
|
145
|
+
envelope = await core.call(name, { workOrder: mapDeep(args.workOrder, maps.forward), sourceScope, fullDossier: true });
|
|
146
|
+
} catch (error) {
|
|
147
|
+
// 반응형 정규화(1회): 경계가 지목한 finite 값만 opaque 로 바꿔 재시도.
|
|
148
|
+
const issues = error.code === "work_order_hub_boundary_rejected" ? boundaryIssues(error) : null;
|
|
149
|
+
if (!issues || !issues.length) throw error;
|
|
150
|
+
const sentWorkOrder = mapDeep(args.workOrder, maps.forward);
|
|
151
|
+
let repaired = 0;
|
|
152
|
+
for (const issue of issues) {
|
|
153
|
+
const value = valueAtPath(sentWorkOrder, issue.path);
|
|
154
|
+
if (typeof value !== "string" || !value) continue;
|
|
155
|
+
const namespace = value.includes(":") ? value.slice(0, value.indexOf(":")) : "id";
|
|
156
|
+
const original = maps.reverse.get(value) || value;
|
|
157
|
+
const mapped = opaqueId(namespace, value);
|
|
158
|
+
if (mapped === value) continue;
|
|
159
|
+
maps.forward.set(original, mapped);
|
|
160
|
+
maps.reverse.set(mapped, original);
|
|
161
|
+
repaired += 1;
|
|
162
|
+
}
|
|
163
|
+
if (!repaired) throw error;
|
|
164
|
+
envelope = await core.call(name, { workOrder: mapDeep(args.workOrder, maps.forward), sourceScope, fullDossier: true });
|
|
165
|
+
}
|
|
166
|
+
const candidateSet = envelope && envelope.candidateSet;
|
|
167
|
+
if (!candidateSet || typeof candidateSet !== "object") throw invalid("local Core federation returned no candidateSet");
|
|
168
|
+
coreCandidateSet = candidateSet;
|
|
169
|
+
return mapDeep(candidateSet, maps.reverse);
|
|
170
|
+
}
|
|
171
|
+
if (name === "workforce.validate_selection") {
|
|
172
|
+
if (!coreCandidateSet) {
|
|
173
|
+
const error = new Error("validate_selection called before search_candidates — the federated lineage is missing");
|
|
174
|
+
error.code = "local_core_lineage_missing";
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
// 루프의 candidateSet(역치환본)과 보관본의 계보 일치를 다이제스트로 확인한다.
|
|
178
|
+
if (args.candidateSet?.candidateSetDigest !== coreCandidateSet.candidateSetDigest) {
|
|
179
|
+
throw invalid("candidateSet lineage mismatch between the loop and the local Core session");
|
|
180
|
+
}
|
|
181
|
+
let envelope;
|
|
182
|
+
let coreSelection = mapDeep(args.selection, maps.forward);
|
|
183
|
+
try {
|
|
184
|
+
envelope = await core.call(name, {
|
|
185
|
+
workOrder: mapDeep(args.workOrder, maps.forward),
|
|
186
|
+
selection: coreSelection,
|
|
187
|
+
candidateSet: coreCandidateSet,
|
|
188
|
+
});
|
|
189
|
+
} catch (error) {
|
|
190
|
+
/*
|
|
191
|
+
* 반응형 정규화(1회): 리더의 자유 reasonCodes 는 finite 정책에 걸린다
|
|
192
|
+
* (실측: selection_reason_code_not_public_finite). 선택 경계의 finite id
|
|
193
|
+
* 정책은 "public finite reason codes" — opaque 형식 허용 문구가 없어
|
|
194
|
+
* 카탈로그 값으로만 대체한다. reason:host-semantic-judgment 는 관측된
|
|
195
|
+
* 카탈로그 값이며 사실 그 자체다(호스트 LLM 의 의미 판단). 사람이 읽을
|
|
196
|
+
* 원문 사유는 루프의 영수증(selection.assignments 원본)에 이미 남아 있다.
|
|
197
|
+
*/
|
|
198
|
+
const issues = error.code === "selection_hub_boundary_rejected" ? boundaryIssues(error) : null;
|
|
199
|
+
const reasonIssues = issues ? issues.filter((issue) => issue.code === "selection_reason_code_not_public_finite") : [];
|
|
200
|
+
if (!reasonIssues.length) throw error;
|
|
201
|
+
const repairedSelection = mapDeep(args.selection, maps.forward);
|
|
202
|
+
for (const assignment of repairedSelection.assignments || []) {
|
|
203
|
+
assignment.reasonCodes = ["reason:host-semantic-judgment"];
|
|
204
|
+
}
|
|
205
|
+
envelope = await core.call(name, {
|
|
206
|
+
workOrder: mapDeep(args.workOrder, maps.forward),
|
|
207
|
+
selection: repairedSelection,
|
|
208
|
+
candidateSet: coreCandidateSet,
|
|
209
|
+
});
|
|
210
|
+
coreSelection = repairedSelection;
|
|
211
|
+
}
|
|
212
|
+
if (!envelope || typeof envelope.selectionValidation !== "object") throw invalid("local Core validation returned no selectionValidation receipt");
|
|
213
|
+
lastValidationEnvelope = envelope;
|
|
214
|
+
lastCoreSelection = coreSelection;
|
|
215
|
+
return mapDeep(envelope.selectionValidation, maps.reverse);
|
|
216
|
+
}
|
|
217
|
+
if (name === "workforce.prepare_execution") {
|
|
218
|
+
if (!lastValidationEnvelope) {
|
|
219
|
+
const error = new Error("prepare_execution called before validate_selection — the federated lineage is missing");
|
|
220
|
+
error.code = "local_core_lineage_missing";
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
const envelope = await core.call(name, {
|
|
224
|
+
workOrder: mapDeep(args.workOrder, maps.forward),
|
|
225
|
+
// validate 가 수락한 정확한 본 — 반응형 reasonCode 수리를 반영한다.
|
|
226
|
+
selection: lastCoreSelection || mapDeep(args.selection, maps.forward),
|
|
227
|
+
candidateSet: coreCandidateSet,
|
|
228
|
+
federatedSelection: lastValidationEnvelope,
|
|
229
|
+
validationReceipt: lastValidationEnvelope,
|
|
230
|
+
projectDir,
|
|
231
|
+
});
|
|
232
|
+
if (!envelope || typeof envelope.executionPlan !== "object") throw invalid("local Core preparation returned no executionPlan");
|
|
233
|
+
const plan = mapDeep(envelope.executionPlan, maps.reverse);
|
|
234
|
+
/*
|
|
235
|
+
* 어휘 왕복의 마지막 두 조각 (실측: 루프의 execution_context_mismatch):
|
|
236
|
+
* 1) Core 는 워크오더의 미선언 slot 필드를 스키마 기본값 [] 로 정규화해
|
|
237
|
+
* 돌려준다. 루프의 expectedContext 는 원본 워크오더에서 유도하므로
|
|
238
|
+
* undefined ↔ [] 가 불일치가 된다. 원본에 없던 필드가 빈 값으로 돌아온
|
|
239
|
+
* 경우만 제거한다 — 값이 실제로 다르면 그대로 두어 루프가 잡게 한다.
|
|
240
|
+
* 2) 반응형 reasonCode 수리가 있었으면 Core 어휘의 assignments 에는
|
|
241
|
+
* 카탈로그 값이 들어 있다. 사람이 읽는 원문 사유(루프의 selection)를
|
|
242
|
+
* 자리(slotId+agentReleaseId) 기준으로 복원한다.
|
|
243
|
+
*/
|
|
244
|
+
const context = plan.executionContext;
|
|
245
|
+
if (context && Array.isArray(context.slots)) {
|
|
246
|
+
const originalSlots = new Map((args.workOrder.roleSlots || []).map((slot) => [slot.slotId, slot]));
|
|
247
|
+
for (const slot of context.slots) {
|
|
248
|
+
const original = originalSlots.get(slot.slotId);
|
|
249
|
+
if (!original) continue;
|
|
250
|
+
for (const [key, value] of Object.entries(slot)) {
|
|
251
|
+
const emptyDefault = Array.isArray(value) && value.length === 0;
|
|
252
|
+
if (emptyDefault && original[key] === undefined && !["allowedEntityKinds"].includes(key)) {
|
|
253
|
+
delete slot[key];
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (context && Array.isArray(context.assignments)) {
|
|
259
|
+
const originalAssignments = new Map((args.selection.assignments || []).map(
|
|
260
|
+
(assignment) => [`${assignment.slotId}${assignment.agentReleaseId}`, assignment],
|
|
261
|
+
));
|
|
262
|
+
for (const assignment of context.assignments) {
|
|
263
|
+
const original = originalAssignments.get(`${assignment.slotId}${assignment.agentReleaseId}`);
|
|
264
|
+
if (original) assignment.reasonCodes = original.reasonCodes;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/*
|
|
268
|
+
* 다이제스트 재서명: Core 의 executionContextDigest 는 Core 어휘(opaque id·
|
|
269
|
+
* 카탈로그 reason·[] 기본값) 위에서 계산됐다. 위의 어휘 왕복으로 루프 어휘
|
|
270
|
+
* 컨텍스트가 됐으므로 루프와 같은 함수로 재계산한다 — 게이트웨이에서의
|
|
271
|
+
* 재서명이지 위조가 아니다: 원본 무결성은 Core 세션과 영수증이 지킨다.
|
|
272
|
+
*/
|
|
273
|
+
if (context) {
|
|
274
|
+
const { _test } = require("../agentlas-workforce.cjs");
|
|
275
|
+
plan.executionContextDigest = _test.executionContextDigest(context);
|
|
276
|
+
// roster 행도 같은 이유로 재서명한다 — 역치환이 행 내용(slot 어휘)을
|
|
277
|
+
// 루프 어휘로 바꿨으므로 bundleDigest/executionGraphDigest 를 루프와 같은
|
|
278
|
+
// 함수로 재계산한다.
|
|
279
|
+
for (const row of plan.executionRoster || []) {
|
|
280
|
+
if (row && row.executionGraph && typeof row.executionGraphDigest === "string") {
|
|
281
|
+
row.executionGraphDigest = _test.executionGraphDigest(_test.validateExecutionGraph(row.executionGraph));
|
|
282
|
+
}
|
|
283
|
+
if (row && typeof row.bundleDigest === "string") {
|
|
284
|
+
row.bundleDigest = _test.workforceRuntimeBundleDigest(row);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return plan;
|
|
289
|
+
}
|
|
290
|
+
const error = new Error(`unsupported local-core workforce tool: ${name}`);
|
|
291
|
+
error.code = "local_core_unsupported_tool";
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return { callHubTool, close: () => core.close() };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
module.exports = { createLocalCoreHubTool, SOURCE_SCOPES };
|
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentlas",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Agentlas project terminal
|
|
3
|
+
"version": "1.0.29",
|
|
4
|
+
"description": "Agentlas project terminal \u2014 run project controllers and task-scoped agent teams from the terminal. Standalone: no desktop app process required.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"agentlas": "bin/agentlas.cjs"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
9
|
"smoke": "sh test/smoke.sh",
|
|
10
10
|
"test:release-contracts": "npm run smoke",
|
|
11
|
-
"sync:architecture": "node scripts/sync-architecture-from-desktop.cjs"
|
|
11
|
+
"sync:architecture": "node scripts/sync-architecture-from-desktop.cjs",
|
|
12
|
+
"test:tool-access-notice-parity": "node test/tool-access-notice-parity.cjs"
|
|
12
13
|
},
|
|
13
14
|
"engines": {
|
|
14
15
|
"node": ">=20"
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/*
|
|
3
|
-
* legacy-network — 호환 전용 Hephaestus 네트워크 경로 (명시적 탈출구).
|
|
4
|
-
*
|
|
5
|
-
* v1 디스패처 매핑 그대로:
|
|
6
|
-
* `agentlas legacy-network "<request>"` → cmdHep(["hep-network", ...rest])
|
|
7
|
-
*
|
|
8
|
-
* 오너 결정 배경: 기본 `network`/`workforce`는 host-LLM Agent Workforce
|
|
9
|
-
* Ontology(fail-closed) 경로다. 구 Hephaestus hep-network 라우터는 이
|
|
10
|
-
* "legacy-" 접두 명령으로만, 사용자가 이름으로 정확히 지목했을 때만 연다.
|
|
11
|
-
* 절대 기본 경로의 폴백으로 쓰지 않는다.
|
|
12
|
-
*
|
|
13
|
-
* v1 가드 그대로: help 토큰 → usage 0, 무인자 → usage 실패 exit 1.
|
|
14
|
-
*/
|
|
15
|
-
const { create, usageFor, isHelpToken } = require("../hephaestus/runtime.cjs");
|
|
16
|
-
|
|
17
|
-
async function run(ctx, args) {
|
|
18
|
-
if (args.some(isHelpToken)) {
|
|
19
|
-
ctx.out(usageFor("legacy-network", ctx.lang));
|
|
20
|
-
return 0;
|
|
21
|
-
}
|
|
22
|
-
if (!args.length) {
|
|
23
|
-
ctx.err("✖ " + usageFor("legacy-network", ctx.lang));
|
|
24
|
-
return 1;
|
|
25
|
-
}
|
|
26
|
-
return create(ctx).cmdHep(["hep-network", ...args]);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
module.exports = { run };
|