agentlas 0.9.10 → 1.0.2
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 +106 -0
- package/README.md +319 -341
- package/bin/agentlas.cjs +32 -9
- package/engine/agentlas-banner.cjs +24 -3
- package/engine/agentlas-composer.cjs +239 -31
- package/engine/agentlas-config.cjs +103 -7
- package/engine/agentlas-core-harness.cjs +48 -4
- package/engine/agentlas-evolution.cjs +3 -4
- package/engine/agentlas-i18n.cjs +88 -32
- package/engine/agentlas-input.cjs +234 -18
- package/engine/agentlas-memory-governance.cjs +3 -5
- package/engine/agentlas-memory-import.cjs +3 -3
- package/engine/agentlas-native-host.cjs +200 -9
- package/engine/agentlas-onboard.cjs +112 -20
- package/engine/agentlas-permissions.cjs +14 -7
- package/engine/agentlas-sqlite-policy.cjs +34 -0
- package/engine/agentlas-ui.cjs +93 -42
- package/engine/agentlas-workforce.cjs +472 -58
- package/engine/agentlas-workload-routing.cjs +8 -3
- package/engine/agentlas.cjs +140 -12367
- package/engine/agents/files.cjs +61 -0
- package/engine/agents/import-local.cjs +237 -0
- package/engine/agents/registry.cjs +158 -0
- package/engine/agents/router.cjs +565 -0
- package/engine/agents/routes.cjs +43 -0
- package/engine/architecture.data.json +2 -2
- package/engine/automation/daemon.cjs +335 -0
- package/engine/automation/schedule.cjs +181 -0
- package/engine/automation/store.cjs +209 -0
- package/engine/cloud/auth.cjs +279 -0
- package/engine/cloud/hub-client.cjs +239 -0
- package/engine/cloud-assets/cargo.cjs +49 -0
- package/engine/cloud-assets/cas.cjs +235 -0
- package/engine/cloud-assets/commands.cjs +273 -0
- package/engine/cloud-assets/package.cjs +936 -0
- package/engine/cloud-assets/restore.cjs +172 -0
- package/engine/cloud-assets/state.cjs +268 -0
- package/engine/commands/automation.cjs +195 -0
- package/engine/commands/billing.cjs +96 -0
- package/engine/commands/browser.cjs +20 -0
- package/engine/commands/build.cjs +33 -0
- package/engine/commands/call.cjs +24 -0
- package/engine/commands/career-graph.cjs +51 -0
- package/engine/commands/cd.cjs +22 -0
- package/engine/commands/chat.cjs +12 -0
- package/engine/commands/chats.cjs +28 -0
- package/engine/commands/cloud.cjs +17 -0
- package/engine/commands/connect.cjs +20 -0
- package/engine/commands/context.cjs +66 -0
- package/engine/commands/creds.cjs +203 -0
- package/engine/commands/doctor.cjs +68 -0
- package/engine/commands/env.cjs +33 -0
- package/engine/commands/evolve.cjs +23 -0
- package/engine/commands/experience.cjs +34 -0
- package/engine/commands/film.cjs +8 -0
- package/engine/commands/firm.cjs +115 -0
- package/engine/commands/help.cjs +65 -0
- package/engine/commands/hep.cjs +23 -0
- package/engine/commands/import.cjs +35 -0
- package/engine/commands/index.cjs +136 -0
- package/engine/commands/install.cjs +27 -0
- package/engine/commands/journal.cjs +31 -0
- package/engine/commands/legacy-network.cjs +29 -0
- package/engine/commands/list.cjs +49 -0
- package/engine/commands/login.cjs +68 -0
- package/engine/commands/logout.cjs +28 -0
- package/engine/commands/mcp.cjs +91 -0
- package/engine/commands/memory.cjs +21 -0
- package/engine/commands/multimodal.cjs +91 -0
- package/engine/commands/native.cjs +34 -0
- package/engine/commands/netadmin.cjs +31 -0
- package/engine/commands/oberon.cjs +70 -0
- package/engine/commands/ontology.cjs +24 -0
- package/engine/commands/open.cjs +48 -0
- package/engine/commands/plugin.cjs +101 -0
- package/engine/commands/project.cjs +47 -0
- package/engine/commands/research.cjs +36 -0
- package/engine/commands/route.cjs +37 -0
- package/engine/commands/run.cjs +149 -0
- package/engine/commands/search.cjs +55 -0
- package/engine/commands/setup.cjs +45 -0
- package/engine/commands/storm.cjs +75 -0
- package/engine/commands/swarm.cjs +75 -0
- package/engine/commands/telegram.cjs +32 -0
- package/engine/commands/uninstall.cjs +68 -0
- package/engine/commands/update.cjs +54 -0
- package/engine/commands/upload.cjs +18 -0
- package/engine/commands/usage.cjs +35 -0
- package/engine/commands/variant.cjs +25 -0
- package/engine/commands/version.cjs +9 -0
- package/engine/commands/whoami.cjs +38 -0
- package/engine/commands/workforce.cjs +103 -0
- package/engine/core/db.cjs +160 -0
- package/engine/core/paths.cjs +35 -0
- package/engine/experience/build.cjs +181 -0
- package/engine/experience/intents.cjs +492 -0
- package/engine/experience/runtime.cjs +242 -0
- package/engine/experience/variant.cjs +196 -0
- package/engine/firms/orchestrate.cjs +333 -0
- package/engine/hephaestus/runtime.cjs +697 -0
- package/engine/hub/install.cjs +872 -0
- package/engine/hub/plugins.cjs +213 -0
- package/engine/mcp/consent.cjs +289 -0
- package/engine/mcp/contract.cjs +202 -0
- package/engine/mcp/index.cjs +43 -0
- package/engine/mcp/inventory.cjs +322 -0
- package/engine/mcp/plan.cjs +286 -0
- package/engine/mcp/probe.cjs +151 -0
- package/engine/memory-cli/curate.cjs +163 -0
- package/engine/oberon/common.cjs +69 -0
- package/engine/oberon/manifest.cjs +164 -0
- package/engine/oberon/outputs.cjs +70 -0
- package/engine/oberon/render.cjs +164 -0
- package/engine/project/career-graph.cjs +249 -0
- package/engine/project/credentials.cjs +262 -0
- package/engine/project/env-file.cjs +46 -0
- package/engine/project/index.cjs +27 -0
- package/engine/project/memory-context.cjs +453 -0
- package/engine/project/ontology.cjs +467 -0
- package/engine/project/paths.cjs +39 -0
- package/engine/project/seed.cjs +200 -0
- package/engine/project/state.cjs +403 -0
- package/engine/project/super-ontology-seed.json +3288 -0
- package/engine/runtimes/detect.cjs +54 -0
- package/engine/runtimes/overrides.cjs +139 -0
- package/engine/runtimes/resolve.cjs +64 -0
- package/engine/sessions/apply-fences.cjs +188 -0
- package/engine/sessions/fences.cjs +362 -0
- package/engine/sessions/orchestrator.cjs +170 -0
- package/engine/sessions/prompt.cjs +212 -0
- package/engine/sessions/session.cjs +245 -0
- package/engine/sessions/sink.cjs +54 -0
- package/engine/sessions/store.cjs +79 -0
- package/engine/storm/deps.cjs +88 -0
- package/engine/storm/storm.cjs +218 -0
- package/engine/storm/swarm.cjs +422 -0
- package/engine/ui/palette.cjs +105 -0
- package/engine/ui/renderer.cjs +85 -0
- package/engine/ui/repl.cjs +444 -0
- package/engine/workforce/capture.cjs +701 -0
- package/engine/workforce/deps.cjs +472 -0
- package/package.json +2 -6
- package/engine/agentlas-experience-mcp.cjs +0 -1709
- package/engine/agentlas-parity.cjs +0 -1499
- package/engine/agentlas-repl.cjs +0 -1780
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* sessions/fences — 에이전트 응답 속 숨은 펜스 프로토콜 파서 (Desktop runner 패리티).
|
|
4
|
+
*
|
|
5
|
+
* Desktop electron/mcp/client.ts 가 매 응답에서 파싱하는 4개 제어 블록의
|
|
6
|
+
* WIRE FORMAT 을 그대로 복제한다 (파싱만; 적용은 apply-fences.cjs):
|
|
7
|
+
*
|
|
8
|
+
* 1. `## Memory Events` + ```json 펜스 — electron/memory/events.ts
|
|
9
|
+
* → engine/memory-cli/curate.cjs parseMemoryEventsCli 를 소비(재구현 금지).
|
|
10
|
+
* 2. `## Delegate` + ```json 펜스 — electron/mcp/delegate.ts parseDelegations
|
|
11
|
+
* 허용 형태: [{target,brief,allocation}] 배열 또는 {delegations:[...], synthesis}.
|
|
12
|
+
* 3. `## Automation` + ```json 펜스 — electron/automation-emitter.ts parseAutomations
|
|
13
|
+
* [{name,prompt,agent?,hubAgent?,schedule: 문자열|{preset,time,tz,dow,day}|{cron,tz}, steps?}]
|
|
14
|
+
* 단, Desktop 은 잘못된 스케줄을 daily-09:00 으로 폴백하지만 터미널은
|
|
15
|
+
* "스케줄을 추측하지 않는다" — 검증 실패는 errors 로 정직하게 거부한다.
|
|
16
|
+
* 4. <<agentlas-ask>>{json}<</agentlas-ask>> — renderer/lib/ask-question.ts extractQuestions
|
|
17
|
+
* (여는/닫는 토큰이 다르다: 닫는 쪽은 <</agentlas-ask>>. 닫는 토큰이 없으면
|
|
18
|
+
* 스트리밍 중일 수 있으므로 본문에 그대로 둔다.)
|
|
19
|
+
*
|
|
20
|
+
* 모든 블록은 cleanText(영속/표시용)에서 제거된다 — 사용자는 제어 블록을 보지 않는다.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const memoryCurate = require("../memory-cli/curate.cjs");
|
|
24
|
+
const workloadRouting = require("../agentlas-workload-routing.cjs");
|
|
25
|
+
const schedule = require("../automation/schedule.cjs");
|
|
26
|
+
|
|
27
|
+
// Desktop electron/mcp/delegate.ts:13
|
|
28
|
+
const DELEGATE_HEADING = "## Delegate";
|
|
29
|
+
// Desktop electron/automation-emitter.ts:11
|
|
30
|
+
const AUTOMATION_HEADING = "## Automation";
|
|
31
|
+
// Desktop electron/confirm/index.ts:15-16 · renderer/lib/ask-question.ts:21-22
|
|
32
|
+
const ASK_OPEN = "<<agentlas-ask>>";
|
|
33
|
+
const ASK_CLOSE = "<</agentlas-ask>>";
|
|
34
|
+
|
|
35
|
+
/* ── ## Delegate (delegate.ts parseDelegations 포팅) ─────────────────────── */
|
|
36
|
+
|
|
37
|
+
function parseDelegateBlock(text) {
|
|
38
|
+
const idx = text.lastIndexOf(DELEGATE_HEADING);
|
|
39
|
+
if (idx < 0) return { delegations: [], synthesisAllocation: null, cleanedText: text.trim() };
|
|
40
|
+
|
|
41
|
+
const after = text.slice(idx + DELEGATE_HEADING.length);
|
|
42
|
+
const fence = after.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
43
|
+
let delegations = [];
|
|
44
|
+
let synthesisAllocation = null;
|
|
45
|
+
if (fence) {
|
|
46
|
+
try {
|
|
47
|
+
const data = JSON.parse(fence[1].trim());
|
|
48
|
+
const rawDelegations = Array.isArray(data)
|
|
49
|
+
? data
|
|
50
|
+
: data && typeof data === "object" && Array.isArray(data.delegations)
|
|
51
|
+
? data.delegations
|
|
52
|
+
: [];
|
|
53
|
+
if (!Array.isArray(data) && data && typeof data === "object" && data.synthesis) {
|
|
54
|
+
synthesisAllocation = workloadRouting.normalizeAllocation(data.synthesis, "synthesize");
|
|
55
|
+
}
|
|
56
|
+
delegations = rawDelegations
|
|
57
|
+
.map((d) => {
|
|
58
|
+
if (!d || typeof d !== "object") return null;
|
|
59
|
+
const target = typeof d.target === "string" ? d.target.trim() : "";
|
|
60
|
+
const brief = typeof d.brief === "string" ? d.brief.trim() : "";
|
|
61
|
+
// Desktop 계약: target 만 필수. allocation 은 정규화 실패 시 null 로 남고
|
|
62
|
+
// (레거시 텍스트 위임과 동일) 실행 측이 현재 모델 폴백 영수증을 남긴다.
|
|
63
|
+
return target
|
|
64
|
+
? { target, brief, allocation: workloadRouting.normalizeAllocation(d.allocation, "delegate") }
|
|
65
|
+
: null;
|
|
66
|
+
})
|
|
67
|
+
.filter(Boolean);
|
|
68
|
+
} catch {
|
|
69
|
+
delegations = [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let cut = text.length;
|
|
74
|
+
if (fence && fence.index != null) {
|
|
75
|
+
cut = idx + DELEGATE_HEADING.length + fence.index + fence[0].length;
|
|
76
|
+
} else {
|
|
77
|
+
cut = idx; // 펜스 없으면 dangling heading 도 제거 (Desktop 과 동일)
|
|
78
|
+
}
|
|
79
|
+
const cleanedText = (text.slice(0, idx) + text.slice(cut)).trim();
|
|
80
|
+
return { delegations, synthesisAllocation, cleanedText };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* ── ## Automation (automation-emitter.ts parseAutomations 포팅, 정직 거부판) ── */
|
|
84
|
+
|
|
85
|
+
const DOW_TOKEN = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
|
|
86
|
+
const PRESETS = new Set(["daily", "weekday", "weekly", "monthly", "hourly"]);
|
|
87
|
+
|
|
88
|
+
// Desktop presetToLegacyToken(automation-emitter.ts:165) 과 동일한 미러 토큰.
|
|
89
|
+
function presetToLegacyToken(preset, time, o) {
|
|
90
|
+
switch (preset) {
|
|
91
|
+
case "hourly":
|
|
92
|
+
return "hourly";
|
|
93
|
+
case "daily":
|
|
94
|
+
return `daily-${time}`;
|
|
95
|
+
case "weekday":
|
|
96
|
+
return `weekday-${time}`;
|
|
97
|
+
case "weekly": {
|
|
98
|
+
let dow = 1; // Desktop 기본값: 월요일 (스케줄 추측이 아니라 유효 프리셋 내 기본값)
|
|
99
|
+
if (typeof o.dow === "number") dow = o.dow;
|
|
100
|
+
else if (typeof o.dow === "string") {
|
|
101
|
+
const i = DOW_TOKEN.indexOf(o.dow.toLowerCase());
|
|
102
|
+
if (i >= 0) dow = i;
|
|
103
|
+
}
|
|
104
|
+
return `weekly-${DOW_TOKEN[dow] || "mon"}-${time}`;
|
|
105
|
+
}
|
|
106
|
+
case "monthly":
|
|
107
|
+
return `monthly-${o.day && o.day >= 1 ? o.day : 1}-${time}`;
|
|
108
|
+
default:
|
|
109
|
+
return `daily-${time}`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/*
|
|
114
|
+
* 방출 스케줄 → 레거시 미러 토큰 + tz. Desktop resolveSchedule 과 wire 형태는
|
|
115
|
+
* 동일하지만 실패 정책이 다르다: Desktop 은 daily-09:00 폴백, 터미널은 null 반환
|
|
116
|
+
* (정직 거부 — 사용자가 시키지 않은 시각에 자동화를 돌리는 것이 침묵 폴백보다 나쁘다).
|
|
117
|
+
* 최종 검증은 engine/automation/schedule.cjs 로 한다.
|
|
118
|
+
*/
|
|
119
|
+
function resolveScheduleHonest(emitted, errors) {
|
|
120
|
+
// 레거시 문자열 형식 ("daily-09:00", "cron:* * * * *", 5필드 cron …)
|
|
121
|
+
if (typeof emitted === "string") {
|
|
122
|
+
const token = emitted.trim();
|
|
123
|
+
if (!token) {
|
|
124
|
+
errors.push("Automation schedule string was empty");
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
const spec = schedule.legacyScheduleSpec(token, null);
|
|
128
|
+
if (!spec || (spec.kind === "cron" && !schedule.nextCronRun(spec.expr, new Date(), spec.tz))) {
|
|
129
|
+
errors.push(`Unrecognized legacy schedule rejected: "${token}"`);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
return { token, tz: "" };
|
|
133
|
+
}
|
|
134
|
+
if (!emitted || typeof emitted !== "object") {
|
|
135
|
+
errors.push("Automation schedule missing — refusing to guess one");
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const tz = typeof emitted.tz === "string" && emitted.tz.trim() ? emitted.tz.trim() : "";
|
|
140
|
+
|
|
141
|
+
if (typeof emitted.cron === "string" && emitted.cron.trim()) {
|
|
142
|
+
const cron = emitted.cron.trim();
|
|
143
|
+
// nextCronRun 이 문법 + IANA tz 를 함께 검증한다 (croner validateCron 의 터미널 대응물).
|
|
144
|
+
if (!schedule.nextCronRun(cron, new Date(), tz || null)) {
|
|
145
|
+
errors.push(`Invalid cron expression rejected: "${cron}"`);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
return { token: `cron:${cron}`, tz };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const preset = typeof emitted.preset === "string" ? emitted.preset.trim().toLowerCase() : "";
|
|
152
|
+
if (!PRESETS.has(preset)) {
|
|
153
|
+
errors.push(`Unknown schedule preset rejected: "${preset || "(none)"}"`);
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
// hourly 는 시각이 필요 없다. 그 외 프리셋은 HH:MM 필수 — 시각을 추측하지 않는다.
|
|
157
|
+
let time = "09:00";
|
|
158
|
+
if (preset !== "hourly") {
|
|
159
|
+
time = typeof emitted.time === "string" && /^\d{1,2}:\d{2}$/.test(emitted.time) ? emitted.time : "";
|
|
160
|
+
if (!time) {
|
|
161
|
+
errors.push(`Schedule preset "${preset}" missing valid "time" (HH:MM) — refusing to guess`);
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const token = presetToLegacyToken(preset, time, emitted);
|
|
166
|
+
const spec = schedule.legacyScheduleSpec(token, tz || null);
|
|
167
|
+
if (!spec || (spec.kind === "cron" && !schedule.nextCronRun(spec.expr, new Date(), spec.tz))) {
|
|
168
|
+
// daily-99:99 같은 범위 밖 시각이 여기서 걸린다.
|
|
169
|
+
errors.push(`Could not compile schedule preset "${preset}" time "${time}"`);
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
return { token, tz };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function parseAutomationBlock(text) {
|
|
176
|
+
const idx = text.lastIndexOf(AUTOMATION_HEADING);
|
|
177
|
+
if (idx < 0) return { automations: [], cleanedText: text.trim(), errors: [] };
|
|
178
|
+
|
|
179
|
+
const after = text.slice(idx + AUTOMATION_HEADING.length);
|
|
180
|
+
const fence = after.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
181
|
+
const errors = [];
|
|
182
|
+
let automations = [];
|
|
183
|
+
|
|
184
|
+
if (fence) {
|
|
185
|
+
try {
|
|
186
|
+
const data = JSON.parse(fence[1].trim());
|
|
187
|
+
if (Array.isArray(data)) {
|
|
188
|
+
automations = data
|
|
189
|
+
.map((d) => {
|
|
190
|
+
if (!d || typeof d !== "object") {
|
|
191
|
+
errors.push("Automation entry was not an object");
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
const name = typeof d.name === "string" ? d.name.trim() : "";
|
|
195
|
+
const prompt = typeof d.prompt === "string" ? d.prompt.trim() : "";
|
|
196
|
+
const agent = typeof d.agent === "string" ? d.agent.trim() : "";
|
|
197
|
+
const hubAgent = typeof d.hubAgent === "string" ? d.hubAgent.trim() : "";
|
|
198
|
+
if (!name || !prompt) {
|
|
199
|
+
errors.push(`Automation "${name || "(unnamed)"}" missing name/prompt`);
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
const resolved = resolveScheduleHonest(d.schedule, errors);
|
|
203
|
+
if (!resolved) return null; // 정직 거부 — 스케줄 추측 금지
|
|
204
|
+
// steps[] 는 wire 패리티로 보존만 한다. Desktop 은 stepsToGraph 로
|
|
205
|
+
// 그래프를 합성하지만 터미널 데몬은 prompt_template 단일 실행이다 —
|
|
206
|
+
// 그래프 합성을 위장하지 않는다(비합성은 apply 쪽 영수증에 표기).
|
|
207
|
+
const steps = Array.isArray(d.steps) && d.steps.length
|
|
208
|
+
? d.steps.filter((s) => !!s && typeof s === "object")
|
|
209
|
+
: undefined;
|
|
210
|
+
return {
|
|
211
|
+
name,
|
|
212
|
+
schedule: resolved.token,
|
|
213
|
+
prompt,
|
|
214
|
+
...(agent ? { agent } : {}),
|
|
215
|
+
...(hubAgent ? { hubAgent } : {}),
|
|
216
|
+
tz: resolved.tz,
|
|
217
|
+
...(steps ? { steps } : {}),
|
|
218
|
+
};
|
|
219
|
+
})
|
|
220
|
+
.filter(Boolean);
|
|
221
|
+
} else {
|
|
222
|
+
errors.push("Automation block was not a JSON array");
|
|
223
|
+
}
|
|
224
|
+
} catch (err) {
|
|
225
|
+
errors.push(`Automation JSON parse failed: ${(err && err.message) || String(err)}`);
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
errors.push("Automation heading present but no JSON fence found");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let cut = text.length;
|
|
232
|
+
if (fence && fence.index != null) {
|
|
233
|
+
cut = idx + AUTOMATION_HEADING.length + fence.index + fence[0].length;
|
|
234
|
+
} else {
|
|
235
|
+
cut = idx;
|
|
236
|
+
}
|
|
237
|
+
const cleanedText = (text.slice(0, idx) + text.slice(cut)).trim();
|
|
238
|
+
return { automations, cleanedText, errors };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* ── <<agentlas-ask>> (renderer/lib/ask-question.ts extractQuestions 포팅) ── */
|
|
242
|
+
|
|
243
|
+
function tryParseAsk(body) {
|
|
244
|
+
// body 가 ```json 펜스로 감싸진 경우도 허용 (Desktop confirm/index.ts:77-80 동일)
|
|
245
|
+
const stripped = body
|
|
246
|
+
.replace(/^```(?:json)?\s*/i, "")
|
|
247
|
+
.replace(/```$/i, "")
|
|
248
|
+
.trim();
|
|
249
|
+
let o;
|
|
250
|
+
try {
|
|
251
|
+
o = JSON.parse(stripped);
|
|
252
|
+
} catch {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
if (!o || typeof o !== "object") return null;
|
|
256
|
+
const question = typeof o.question === "string" ? o.question.trim() : "";
|
|
257
|
+
if (!question) return null;
|
|
258
|
+
const optionsRaw = Array.isArray(o.options) ? o.options : [];
|
|
259
|
+
const options = [];
|
|
260
|
+
for (const opt of optionsRaw) {
|
|
261
|
+
if (!opt || typeof opt !== "object") continue;
|
|
262
|
+
const label = typeof opt.label === "string" ? opt.label.trim() : "";
|
|
263
|
+
if (!label) continue;
|
|
264
|
+
const description = typeof opt.description === "string" ? opt.description.trim() : "";
|
|
265
|
+
options.push({
|
|
266
|
+
label: label.slice(0, 200),
|
|
267
|
+
...(description ? { description: description.slice(0, 1000) } : {}),
|
|
268
|
+
});
|
|
269
|
+
if (options.length >= 8) break; // Desktop confirm/index.ts:99 상한
|
|
270
|
+
}
|
|
271
|
+
if (options.length < 2) return null; // Desktop 계약: 선택지 2개 미만은 유효 질문 아님
|
|
272
|
+
return {
|
|
273
|
+
question: question.slice(0, 4000),
|
|
274
|
+
...(typeof o.header === "string" && o.header.trim() ? { header: o.header.trim().slice(0, 200) } : {}),
|
|
275
|
+
multiSelect: o.multiSelect === true,
|
|
276
|
+
options,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function extractAsks(text) {
|
|
281
|
+
if (!text.includes(ASK_OPEN)) return { text, asks: [] };
|
|
282
|
+
const asks = [];
|
|
283
|
+
let remaining = text;
|
|
284
|
+
let buf = "";
|
|
285
|
+
for (;;) {
|
|
286
|
+
const open = remaining.indexOf(ASK_OPEN);
|
|
287
|
+
if (open < 0) {
|
|
288
|
+
buf += remaining;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
buf += remaining.slice(0, open);
|
|
292
|
+
const afterOpen = remaining.slice(open + ASK_OPEN.length);
|
|
293
|
+
const close = afterOpen.indexOf(ASK_CLOSE);
|
|
294
|
+
if (close < 0) {
|
|
295
|
+
// 닫는 토큰이 아직 없다 — 스트리밍 중 미완성일 수 있으니 본문에 그대로 둔다.
|
|
296
|
+
buf += remaining.slice(open);
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
const parsed = tryParseAsk(afterOpen.slice(0, close).trim());
|
|
300
|
+
if (parsed) asks.push(parsed);
|
|
301
|
+
// 파싱 실패해도 닫힌 펜스는 본문에서 제거 — malformed JSON raw 를 사용자에게 노출하지 않는다.
|
|
302
|
+
remaining = afterOpen.slice(close + ASK_CLOSE.length);
|
|
303
|
+
}
|
|
304
|
+
return { text: buf, asks };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/* ── 통합 진입점 ─────────────────────────────────────────────────────────── */
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* @returns {{
|
|
311
|
+
* cleanText: string, // 4개 펜스가 모두 제거된 표시/영속용 텍스트
|
|
312
|
+
* memoryEvents: object[], // parseMemoryEventsCli 원시 이벤트(게이트 전)
|
|
313
|
+
* delegates: Array<{target,brief,allocation}>,
|
|
314
|
+
* synthesisAllocation: object|null,
|
|
315
|
+
* automations: Array<{name,schedule,prompt,agent?,hubAgent?,tz,steps?}>,
|
|
316
|
+
* asks: Array<{question,header?,multiSelect,options}>,
|
|
317
|
+
* errors: string[] // 자동화 검증 실패 등 — 조용히 드롭하지 않고 표면화
|
|
318
|
+
* }}
|
|
319
|
+
*/
|
|
320
|
+
function parseReplyFences(text) {
|
|
321
|
+
const raw = String(text || "");
|
|
322
|
+
const errors = [];
|
|
323
|
+
|
|
324
|
+
// 1. Memory Events — 큐레이트 모듈의 파서를 소비(포맷 정본은 그쪽).
|
|
325
|
+
const mem = memoryCurate.parseMemoryEventsCli(raw);
|
|
326
|
+
let working = mem.cleaned;
|
|
327
|
+
|
|
328
|
+
// 2. Delegate
|
|
329
|
+
const del = parseDelegateBlock(working);
|
|
330
|
+
working = del.cleanedText;
|
|
331
|
+
|
|
332
|
+
// 3. Automation
|
|
333
|
+
const auto = parseAutomationBlock(working);
|
|
334
|
+
working = auto.cleanedText;
|
|
335
|
+
errors.push(...auto.errors);
|
|
336
|
+
|
|
337
|
+
// 4. Ask
|
|
338
|
+
const ask = extractAsks(working);
|
|
339
|
+
working = ask.text;
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
cleanText: working.trim(),
|
|
343
|
+
memoryEvents: mem.events,
|
|
344
|
+
delegates: del.delegations,
|
|
345
|
+
synthesisAllocation: del.synthesisAllocation,
|
|
346
|
+
automations: auto.automations,
|
|
347
|
+
asks: ask.asks,
|
|
348
|
+
errors,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
module.exports = {
|
|
353
|
+
DELEGATE_HEADING,
|
|
354
|
+
AUTOMATION_HEADING,
|
|
355
|
+
ASK_OPEN,
|
|
356
|
+
ASK_CLOSE,
|
|
357
|
+
parseReplyFences,
|
|
358
|
+
// 테스트/재사용용 개별 파서
|
|
359
|
+
parseDelegateBlock,
|
|
360
|
+
parseAutomationBlock,
|
|
361
|
+
extractAsks,
|
|
362
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* sessions/orchestrator — 오르카 계층: 멀티세션 스폰/전환/스티어/킬/브로드캐스트.
|
|
4
|
+
*
|
|
5
|
+
* 세션 번호는 s1, s2, … 로 붙는다(사람이 한 키로 지목할 수 있는 안정 번호).
|
|
6
|
+
* 활성 세션 하나만 터미널에 스트리밍되고, 나머지는 백그라운드에서 이벤트를
|
|
7
|
+
* 링버퍼에 쌓는다. 백그라운드 세션의 턴 종료는 'notice' 이벤트로 전면 세션에
|
|
8
|
+
* 한 줄 알림된다.
|
|
9
|
+
*
|
|
10
|
+
* 동시 실행 상한: AGENTLAS_MAX_PARALLEL (기본 4). 상한 초과 스폰은 대기가 아니라
|
|
11
|
+
* 정직한 거부 — 사용자가 세션을 정리하거나 상한을 올리게 안내한다.
|
|
12
|
+
*/
|
|
13
|
+
const { EventEmitter } = require("node:events");
|
|
14
|
+
const { Session } = require("./session.cjs");
|
|
15
|
+
|
|
16
|
+
function maxParallel() {
|
|
17
|
+
const n = Number(process.env.AGENTLAS_MAX_PARALLEL);
|
|
18
|
+
return Number.isInteger(n) && n > 0 ? Math.min(n, 16) : 4;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class Orchestrator extends EventEmitter {
|
|
22
|
+
constructor({ db, lang }) {
|
|
23
|
+
super();
|
|
24
|
+
this.db = db;
|
|
25
|
+
this.lang = lang || "en";
|
|
26
|
+
this.sessions = new Map(); // "s1" -> Session
|
|
27
|
+
this._seq = 0;
|
|
28
|
+
this.activeKey = null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_nextKey() {
|
|
32
|
+
this._seq += 1;
|
|
33
|
+
return `s${this._seq}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
runningCount() {
|
|
37
|
+
let n = 0;
|
|
38
|
+
for (const s of this.sessions.values()) if (s.isBusy()) n += 1;
|
|
39
|
+
return n;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 세션 생성(+선택적 즉시 실행). parentKey를 주면 그 세션의 서브에이전트
|
|
44
|
+
* (division 챗)로 붙는다.
|
|
45
|
+
*/
|
|
46
|
+
spawn({ agent, runtime, permission, cwd, title, parentKey, activate = true, spawnImpl, timeoutConfig, chatId }) {
|
|
47
|
+
const parent = parentKey ? this.sessions.get(parentKey) || null : null;
|
|
48
|
+
const session = new Session({
|
|
49
|
+
db: this.db,
|
|
50
|
+
agent,
|
|
51
|
+
runtime,
|
|
52
|
+
permission,
|
|
53
|
+
cwd,
|
|
54
|
+
lang: this.lang,
|
|
55
|
+
parent,
|
|
56
|
+
title,
|
|
57
|
+
spawnImpl,
|
|
58
|
+
timeoutConfig,
|
|
59
|
+
chatId,
|
|
60
|
+
});
|
|
61
|
+
const key = this._nextKey();
|
|
62
|
+
session.key = key;
|
|
63
|
+
session.orchestrator = this; // apply-fences 가 ## Delegate 스폰 시 쓰는 오르카 역참조
|
|
64
|
+
this.sessions.set(key, session);
|
|
65
|
+
if (parent) parent.children.push(key);
|
|
66
|
+
|
|
67
|
+
session.on("event", (ev) => {
|
|
68
|
+
this.emit("session-event", { key, session, ev });
|
|
69
|
+
if (ev.type === "turn-end" && this.activeKey !== key) {
|
|
70
|
+
this.emit("notice", {
|
|
71
|
+
key,
|
|
72
|
+
session,
|
|
73
|
+
text: ev.ok
|
|
74
|
+
? `${key} ${session.agent.slug}: done`
|
|
75
|
+
: `${key} ${session.agent.slug}: ${ev.error || "failed"}`,
|
|
76
|
+
ok: !!ev.ok,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (activate || !this.activeKey) this.activeKey = key;
|
|
82
|
+
this.emit("sessions-changed");
|
|
83
|
+
return session;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 실행 요청 — 동시 상한을 넘으면 스폰하지 않고 정직하게 throw. */
|
|
87
|
+
sendTo(key, prompt) {
|
|
88
|
+
const session = this.sessions.get(key);
|
|
89
|
+
if (!session) throw new Error(`no such session: ${key}`);
|
|
90
|
+
if (!session.isBusy() && this.runningCount() >= maxParallel()) {
|
|
91
|
+
throw new Error(`parallel limit ${maxParallel()} reached (running: ${this.runningCount()}). /kill or raise AGENTLAS_MAX_PARALLEL`);
|
|
92
|
+
}
|
|
93
|
+
return session.send(prompt);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
active() {
|
|
97
|
+
return this.activeKey ? this.sessions.get(this.activeKey) || null : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
setActive(key) {
|
|
101
|
+
if (!this.sessions.has(key)) throw new Error(`no such session: ${key}`);
|
|
102
|
+
this.activeKey = key;
|
|
103
|
+
this.emit("sessions-changed");
|
|
104
|
+
return this.sessions.get(key);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
kill(key) {
|
|
108
|
+
const session = this.sessions.get(key);
|
|
109
|
+
if (!session) throw new Error(`no such session: ${key}`);
|
|
110
|
+
session.kill();
|
|
111
|
+
this.emit("sessions-changed");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
remove(key) {
|
|
115
|
+
const session = this.sessions.get(key);
|
|
116
|
+
if (!session) return;
|
|
117
|
+
if (session.isBusy()) session.kill();
|
|
118
|
+
this.sessions.delete(key);
|
|
119
|
+
if (this.activeKey === key) {
|
|
120
|
+
const rest = [...this.sessions.keys()];
|
|
121
|
+
this.activeKey = rest.length ? rest[rest.length - 1] : null;
|
|
122
|
+
}
|
|
123
|
+
this.emit("sessions-changed");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
broadcast(prompt) {
|
|
127
|
+
const sent = [];
|
|
128
|
+
for (const [key, session] of this.sessions) {
|
|
129
|
+
if (session.status === "killed") continue;
|
|
130
|
+
this.sendTo(key, prompt);
|
|
131
|
+
sent.push(key);
|
|
132
|
+
}
|
|
133
|
+
return sent;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 세션 표: [{key, active, agent, status, elapsed, lastLine, parentKey, depth}] */
|
|
137
|
+
list() {
|
|
138
|
+
const rows = [];
|
|
139
|
+
const walk = (key, depth) => {
|
|
140
|
+
const s = this.sessions.get(key);
|
|
141
|
+
if (!s) return;
|
|
142
|
+
rows.push({
|
|
143
|
+
key,
|
|
144
|
+
active: key === this.activeKey,
|
|
145
|
+
agent: s.agent.slug,
|
|
146
|
+
status: s.status,
|
|
147
|
+
busy: s.isBusy(),
|
|
148
|
+
elapsedMs: s.startedAt ? (s.endedAt || Date.now()) - s.startedAt : 0,
|
|
149
|
+
lastLine: s.lastLine,
|
|
150
|
+
parentKey: s.parent ? s.parent.key : null,
|
|
151
|
+
depth,
|
|
152
|
+
queued: s.queue.length,
|
|
153
|
+
});
|
|
154
|
+
for (const c of s.children) walk(c, depth + 1);
|
|
155
|
+
};
|
|
156
|
+
for (const [key, s] of this.sessions) {
|
|
157
|
+
if (!s.parent) walk(key, 0);
|
|
158
|
+
}
|
|
159
|
+
return rows;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 모든 세션 종료(REPL 종료 시). */
|
|
163
|
+
shutdown() {
|
|
164
|
+
for (const s of this.sessions.values()) {
|
|
165
|
+
if (s.isBusy()) s.kill();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = { Orchestrator, maxParallel };
|