agentlas 0.6.0 → 0.9.1
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 +190 -0
- package/README.md +220 -4
- package/bin/agentlas.cjs +8 -8
- package/engine/agentlas-capabilities.cjs +34 -3
- package/engine/agentlas-core-harness.cjs +205 -0
- package/engine/agentlas-desktop-loadout.cjs +527 -0
- package/engine/agentlas-doctor.cjs +1 -1
- package/engine/agentlas-experience-exchange.cjs +2151 -0
- package/engine/agentlas-experience-intake.cjs +444 -0
- package/engine/agentlas-experience-mcp.cjs +1709 -0
- package/engine/agentlas-i18n.cjs +10 -10
- package/engine/agentlas-input.cjs +5 -4
- package/engine/agentlas-mcp-env.cjs +219 -0
- package/engine/agentlas-mcp-wrapper.cjs +51 -0
- package/engine/agentlas-memory-governance.cjs +1029 -0
- package/engine/agentlas-native-host.cjs +129 -39
- package/engine/agentlas-parity.cjs +339 -154
- package/engine/agentlas-repl.cjs +327 -44
- package/engine/agentlas-workforce.cjs +2991 -0
- package/engine/agentlas-workload-routing.cjs +523 -0
- package/engine/agentlas.cjs +1886 -270
- package/engine/bootstrap-schema.sql +1 -1
- package/engine/experience-taxonomy-v1.json +49 -0
- package/package.json +8 -4
- package/scripts/gen-bootstrap-schema.sh +0 -23
- package/test/bootstrap-race.cjs +0 -47
- package/test/capture-runtime-guard.cjs +0 -122
- package/test/cloud-asset-restore.cjs +0 -423
- package/test/cloud-cas-client.cjs +0 -333
- package/test/cloud-owner-restore.cjs +0 -183
- package/test/cloud-runtime-paths.cjs +0 -40
- package/test/cloud-save-publish.cjs +0 -453
- package/test/credential-env-regression.cjs +0 -52
- package/test/login-loopback-security.cjs +0 -115
- package/test/mcp-config-isolation.cjs +0 -36
- package/test/permission-mapping.cjs +0 -180
- package/test/route-regression.cjs +0 -121
- package/test/run-api-regression.cjs +0 -322
- package/test/runtime-env-protection.cjs +0 -45
- package/test/semver-precedence.cjs +0 -39
- package/test/smoke.sh +0 -90
- package/test/sqlite-driver-probe.cjs +0 -22
- package/test/terminal-ui-regression.cjs +0 -472
- package/test/timeout-regression.cjs +0 -218
- package/test/tool-workspace-boundary.cjs +0 -165
- package/test/update-safety.cjs +0 -376
package/engine/agentlas-repl.cjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* First launch runs an onboarding wizard (language → runtime → permission), stored in prefs.
|
|
9
9
|
*/
|
|
10
10
|
const readline = require("node:readline");
|
|
11
|
+
const crypto = require("node:crypto");
|
|
11
12
|
const { Ui } = require("./agentlas-ui.cjs");
|
|
12
13
|
const banner = require("./agentlas-banner.cjs");
|
|
13
14
|
const { runNativeTurn } = require("./agentlas-native-host.cjs");
|
|
@@ -24,6 +25,12 @@ function runtimeLabel(rt) {
|
|
|
24
25
|
return `${rt.backend}${rt.model ? " · " + rt.model : ""}`;
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
function runtimePromptForSession(prompt, ctx, rt, session, helpers) {
|
|
29
|
+
const resumesServerSide = rt && (rt.kind === "claude-code" || rt.kind === "codex");
|
|
30
|
+
if (!session?.id || !resumesServerSide || typeof helpers?.memoryEmitterPrompt !== "function") return prompt;
|
|
31
|
+
return `${prompt}\n\n${helpers.memoryEmitterPrompt(prompt, ctx)}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
27
34
|
function normalizeLang(value) {
|
|
28
35
|
const v = String(value || "").toLowerCase();
|
|
29
36
|
return v === "en" || v === "ko" ? v : null;
|
|
@@ -162,7 +169,7 @@ function makeStyleGuard(ui) {
|
|
|
162
169
|
function startRepl(opts) {
|
|
163
170
|
const { db } = opts;
|
|
164
171
|
const H = opts.helpers;
|
|
165
|
-
const prefs = opts.prefs || {};
|
|
172
|
+
const prefs = applyPreferenceDefaults(opts.prefs || {});
|
|
166
173
|
prefs.agentRuntime = prefs.agentRuntime || {}; // { agentSlug|firmSlug: runtimeSpec|"auto" }
|
|
167
174
|
let baseRuntime = opts.runtime; // session default; per-agent runtime auto-routes from this
|
|
168
175
|
const ui = new Ui({ lang: terminalLang(prefs, opts) });
|
|
@@ -176,6 +183,8 @@ function startRepl(opts) {
|
|
|
176
183
|
projectPath: opts.projectPath || null,
|
|
177
184
|
routePreambleOnce: null,
|
|
178
185
|
effort: prefs.effort || null, // /effort: low|medium|high|max → 런타임별 reasoning 강도
|
|
186
|
+
modelPinned: false, // true only after an explicit /model <id>; /model auto clears it
|
|
187
|
+
effortPinned: prefs.effortPinned === true || Boolean(prefs.effort), // /effort auto is the only automatic mode
|
|
179
188
|
cost: {}, // runtimeLabel → { turns, in, out, cost, ms } — session usage ledger
|
|
180
189
|
};
|
|
181
190
|
|
|
@@ -298,6 +307,16 @@ function startRepl(opts) {
|
|
|
298
307
|
busy = true;
|
|
299
308
|
currentAbort = new AbortController();
|
|
300
309
|
const signal = currentAbort.signal;
|
|
310
|
+
let experienceRunId = `terminal-run:${crypto.randomUUID()}`;
|
|
311
|
+
const runStartedAt = Date.now();
|
|
312
|
+
let experienceFinalized = false;
|
|
313
|
+
let experienceContext = null;
|
|
314
|
+
let memorySettled = false;
|
|
315
|
+
let memoryContext = null;
|
|
316
|
+
let memoryRuntime = null;
|
|
317
|
+
let memoryModel = null;
|
|
318
|
+
let memoryOutput = "";
|
|
319
|
+
let memoryOutcome = "failed";
|
|
301
320
|
try {
|
|
302
321
|
ui.beginTurn({
|
|
303
322
|
...composerMeta(),
|
|
@@ -308,11 +327,67 @@ function startRepl(opts) {
|
|
|
308
327
|
ui.warn(ui.t("interrupted"));
|
|
309
328
|
},
|
|
310
329
|
}); // 작업 중에도 composer/status bar와 실제 runtime task 목록을 화면 하단에 유지
|
|
330
|
+
if (H.ensureProjectForExecution) {
|
|
331
|
+
state.projectPath = H.ensureProjectForExecution(
|
|
332
|
+
db,
|
|
333
|
+
state.cwd,
|
|
334
|
+
state.permission,
|
|
335
|
+
"terminal-interactive-turn",
|
|
336
|
+
);
|
|
337
|
+
}
|
|
311
338
|
const recordHistoryEntry = !runOptions.side;
|
|
312
339
|
const targetLang = H.detectResponseLanguage ? H.detectResponseLanguage(prompt, ui.lang) : ui.lang;
|
|
313
|
-
const
|
|
340
|
+
const curatedMemories = [];
|
|
314
341
|
const rt = state.runtime;
|
|
315
|
-
const
|
|
342
|
+
const priorContextDigest = crypto.createHash("sha256")
|
|
343
|
+
.update(JSON.stringify(state.history.map((item) => ({ role: item.role, text: item.text }))))
|
|
344
|
+
.digest("hex");
|
|
345
|
+
const memoryTurn = typeof H.beginMemoryTurn === "function"
|
|
346
|
+
? H.beginMemoryTurn(db, prompt, {
|
|
347
|
+
...ctxNow(),
|
|
348
|
+
surface: runOptions.side ? "terminal-side-turn" : "terminal-interactive-turn",
|
|
349
|
+
conversationRef: rt.mode === "cli" ? state.native[rt.kind]?.id : null,
|
|
350
|
+
priorContextDigest,
|
|
351
|
+
// Provider session resume is conversation context, not turn
|
|
352
|
+
// identity. Every submitted host turn receives a fresh id.
|
|
353
|
+
stableTurnId: `terminal-turn:${crypto.randomUUID()}`,
|
|
354
|
+
})
|
|
355
|
+
: null;
|
|
356
|
+
if (memoryTurn?.turnId) experienceRunId = `terminal-run:${memoryTurn.turnId}`;
|
|
357
|
+
const ctx = {
|
|
358
|
+
...ctxNow(),
|
|
359
|
+
lang: targetLang,
|
|
360
|
+
uiLang: ui.lang,
|
|
361
|
+
curatedMemories,
|
|
362
|
+
memoryTurn,
|
|
363
|
+
turnId: memoryTurn?.turnId || null,
|
|
364
|
+
};
|
|
365
|
+
memoryContext = ctx;
|
|
366
|
+
memoryRuntime = rt;
|
|
367
|
+
let workloadResolution = null;
|
|
368
|
+
if (state.subject && state.subject.kind === "firm" && typeof H.allocateWorkload === "function") {
|
|
369
|
+
ui.status(targetLang === "ko" ? "상위 AI가 팀 작업의 모델 비용을 배정 중…" : "Higher-level AI is allocating the team model…");
|
|
370
|
+
const planned = await H.allocateWorkload(db, prompt, {
|
|
371
|
+
runtime: rt,
|
|
372
|
+
cwd: state.cwd,
|
|
373
|
+
projectPath: state.projectPath,
|
|
374
|
+
agentId: state.subject.id,
|
|
375
|
+
lang: targetLang,
|
|
376
|
+
mode: "team",
|
|
377
|
+
modelPin: state.modelPinned ? rt.model : null,
|
|
378
|
+
effortPin: state.effortPinned ? (state.effort || "none") : undefined,
|
|
379
|
+
onWarning: (message) => ui.warn(message),
|
|
380
|
+
});
|
|
381
|
+
workloadResolution = planned && planned.resolution;
|
|
382
|
+
if (workloadResolution && workloadResolution.fallbackReason) {
|
|
383
|
+
ui.info(`model route: ${workloadResolution.source} · ${workloadResolution.model || runtimeLabel(rt)} · ${workloadResolution.fallbackReason}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const selectedModel = (workloadResolution && workloadResolution.model) || rt.model || null;
|
|
387
|
+
memoryModel = selectedModel;
|
|
388
|
+
const selectedEffort = workloadResolution ? workloadResolution.effort : state.effort || null;
|
|
389
|
+
const costLabel = selectedModel ? `${runtimeLabel(rt)} · ${selectedModel}` : runtimeLabel(rt);
|
|
390
|
+
experienceContext = { ctx, rt, selectedModel, curatedMemories, targetLang };
|
|
316
391
|
const runEnv = H.buildChildEnv ? await H.buildChildEnv(db, { ...ctx, cwd: state.cwd }) : process.env;
|
|
317
392
|
ui._lastUsage = null;
|
|
318
393
|
const assistantUi = makeStyleGuard(ui);
|
|
@@ -325,35 +400,87 @@ function startRepl(opts) {
|
|
|
325
400
|
? `${state.routePreambleOnce}\n\n${state.subject.system}`
|
|
326
401
|
: state.subject.system;
|
|
327
402
|
state.routePreambleOnce = null;
|
|
328
|
-
const sys = H.augmentSystem(db, subjectSystem, ctx,
|
|
403
|
+
const sys = H.augmentSystem(db, subjectSystem, ctx, true, prompt);
|
|
404
|
+
// claude(--resume)·codex(resume <thread_id>)만 서버측에서 대화를 이어받아 이전 턴의
|
|
405
|
+
// 시스템 프롬프트를 유지한다(native-host.cjs claudeArgs/codexArgs). gemini CLI는 resume
|
|
406
|
+
// 플래그가 없어 매 턴이 새 프로세스 — session.id로 시스템 프롬프트를 비우면 2턴째부터
|
|
407
|
+
// 페르소나가 통째로 사라진다. 그래서 gemini는 매 턴 시스템 프롬프트를 다시 보낸다.
|
|
408
|
+
const resumesServerSide = rt.kind === "claude-code" || rt.kind === "codex";
|
|
409
|
+
// Claude/Codex resume keeps the first turn's system prompt. Append the
|
|
410
|
+
// current host contract to this turn's user payload so a resumed model
|
|
411
|
+
// cannot reuse a stale Memory Ticket id from an earlier turn.
|
|
412
|
+
const runtimePrompt = runtimePromptForSession(prompt, ctx, rt, session, H);
|
|
413
|
+
const memoryGuard = makeMemoryGuard(assistantUi, H.eventsHeading());
|
|
414
|
+
const connectedMcpServers = state.permission === "full" && H.mcpServers
|
|
415
|
+
? H.mcpServers(db).filter((server) =>
|
|
416
|
+
server.enabled && server.transport === "stdio" && server.runtimeEligible === true && server.runtimeConsented === true)
|
|
417
|
+
: [];
|
|
329
418
|
const res = await runNativeTurn({
|
|
330
419
|
kind: rt.kind,
|
|
331
420
|
bin,
|
|
332
|
-
prompt,
|
|
333
|
-
systemPrompt: session.id ? "" : sys,
|
|
421
|
+
prompt: runtimePrompt,
|
|
422
|
+
systemPrompt: session.id && resumesServerSide ? "" : sys,
|
|
334
423
|
cwd: state.cwd,
|
|
335
424
|
permission: state.permission,
|
|
336
425
|
session,
|
|
337
|
-
model:
|
|
338
|
-
effort:
|
|
339
|
-
mcpServers:
|
|
340
|
-
state.permission === "full" && H.mcpServers
|
|
341
|
-
? H.mcpServers(db).filter((s) => s.enabled && s.transport === "stdio")
|
|
342
|
-
: [],
|
|
426
|
+
model: selectedModel, // explicit /model pin or higher-level AI allocation
|
|
427
|
+
effort: selectedEffort, // explicit /effort pin or higher-level AI allocation
|
|
428
|
+
mcpServers: connectedMcpServers,
|
|
343
429
|
env: runEnv,
|
|
344
|
-
ui:
|
|
430
|
+
ui: memoryGuard,
|
|
345
431
|
signal,
|
|
346
432
|
});
|
|
347
|
-
|
|
433
|
+
memoryOutput = String(res.text || "");
|
|
434
|
+
memoryOutcome = res.error ? "failed" : "succeeded";
|
|
435
|
+
let at = "";
|
|
436
|
+
let memoryResult = null;
|
|
437
|
+
if (typeof H.completeMemoryTurn === "function") {
|
|
438
|
+
try {
|
|
439
|
+
memoryResult = await H.completeMemoryTurn(db, memoryOutput, ctx, rt, {
|
|
440
|
+
model: selectedModel,
|
|
441
|
+
outcome: memoryOutcome,
|
|
442
|
+
requestText: prompt,
|
|
443
|
+
invokeCurator: !res.error,
|
|
444
|
+
});
|
|
445
|
+
memorySettled = true;
|
|
446
|
+
} catch {
|
|
447
|
+
// finally retries once without semantic Curator authority.
|
|
448
|
+
}
|
|
449
|
+
for (const memory of memoryResult?.curatedMemories || []) {
|
|
450
|
+
if (memory && !curatedMemories.some((item) => item.id === memory.id)) curatedMemories.push(memory);
|
|
451
|
+
}
|
|
452
|
+
at = (H.sanitizeAssistantText(memoryResult?.cleaned ?? memoryOutput) || "").trim();
|
|
453
|
+
} else {
|
|
454
|
+
at = (H.sanitizeAssistantText(res.text || "") || "").trim();
|
|
455
|
+
}
|
|
348
456
|
if (recordHistoryEntry && at && !res.error) state.history.push({ role: "user", text: prompt }, { role: "assistant", text: at });
|
|
349
457
|
recordCost(costLabel, res.usage);
|
|
350
458
|
if (!runOptions.side && session.id && !res.error) persistSession(rt.kind, session.id, prompt);
|
|
459
|
+
if (typeof H.finalizeExperienceRun === "function") {
|
|
460
|
+
H.finalizeExperienceRun(db, {
|
|
461
|
+
agentId: ctx.agentId,
|
|
462
|
+
projectPath: ctx.projectPath,
|
|
463
|
+
cwd: state.cwd,
|
|
464
|
+
runtime: rt,
|
|
465
|
+
permission: state.permission,
|
|
466
|
+
model: selectedModel,
|
|
467
|
+
mcpServers: connectedMcpServers,
|
|
468
|
+
curatedMemories,
|
|
469
|
+
taskHint: prompt,
|
|
470
|
+
outcome: { status: res.error ? "failed" : "succeeded", failureCode: res.error ? "runtime-error" : null },
|
|
471
|
+
usage: res.usage,
|
|
472
|
+
durationMs: Date.now() - runStartedAt,
|
|
473
|
+
runId: experienceRunId,
|
|
474
|
+
lang: targetLang,
|
|
475
|
+
});
|
|
476
|
+
experienceFinalized = true;
|
|
477
|
+
}
|
|
351
478
|
} else {
|
|
352
479
|
const subjectSystem = state.routePreambleOnce
|
|
353
480
|
? `${state.routePreambleOnce}\n\n${state.subject.system}`
|
|
354
481
|
: state.subject.system;
|
|
355
482
|
state.routePreambleOnce = null;
|
|
356
|
-
const sys = H.augmentSystem(db, subjectSystem, ctx, true);
|
|
483
|
+
const sys = H.augmentSystem(db, subjectSystem, ctx, true, prompt);
|
|
357
484
|
let apiKey = null;
|
|
358
485
|
if (rt.backend !== "ollama") {
|
|
359
486
|
apiKey = await H.apiKey(rt.backend);
|
|
@@ -369,7 +496,7 @@ function startRepl(opts) {
|
|
|
369
496
|
const guard = makeMemoryGuard(assistantUi, H.eventsHeading());
|
|
370
497
|
const res = await runApiTurn({
|
|
371
498
|
backend: rt.backend,
|
|
372
|
-
model:
|
|
499
|
+
model: selectedModel || H.defaultApiModel(rt.backend),
|
|
373
500
|
apiKey,
|
|
374
501
|
system: sys,
|
|
375
502
|
messages,
|
|
@@ -377,12 +504,69 @@ function startRepl(opts) {
|
|
|
377
504
|
ui: guard,
|
|
378
505
|
signal,
|
|
379
506
|
});
|
|
380
|
-
|
|
507
|
+
memoryOutput = String(res.text || "");
|
|
508
|
+
memoryOutcome = "succeeded";
|
|
509
|
+
let cleaned = "";
|
|
510
|
+
if (typeof H.completeMemoryTurn === "function") {
|
|
511
|
+
let memoryResult = null;
|
|
512
|
+
try {
|
|
513
|
+
memoryResult = await H.completeMemoryTurn(db, memoryOutput, ctx, rt, {
|
|
514
|
+
model: selectedModel,
|
|
515
|
+
outcome: memoryOutcome,
|
|
516
|
+
requestText: prompt,
|
|
517
|
+
});
|
|
518
|
+
memorySettled = true;
|
|
519
|
+
} catch {
|
|
520
|
+
// finally retries once without semantic Curator authority.
|
|
521
|
+
}
|
|
522
|
+
for (const memory of memoryResult?.curatedMemories || []) {
|
|
523
|
+
if (memory && !curatedMemories.some((item) => item.id === memory.id)) curatedMemories.push(memory);
|
|
524
|
+
}
|
|
525
|
+
cleaned = (H.sanitizeAssistantText(memoryResult?.cleaned ?? memoryOutput) || "").trim();
|
|
526
|
+
} else {
|
|
527
|
+
cleaned = (H.sanitizeAssistantText(res.text || "") || "").trim();
|
|
528
|
+
}
|
|
381
529
|
if (recordHistoryEntry && cleaned) state.history.push({ role: "user", text: prompt }, { role: "assistant", text: cleaned });
|
|
382
530
|
recordCost(costLabel, ui._lastUsage);
|
|
531
|
+
if (typeof H.finalizeExperienceRun === "function") {
|
|
532
|
+
H.finalizeExperienceRun(db, {
|
|
533
|
+
agentId: ctx.agentId,
|
|
534
|
+
projectPath: ctx.projectPath,
|
|
535
|
+
cwd: state.cwd,
|
|
536
|
+
runtime: rt,
|
|
537
|
+
permission: state.permission,
|
|
538
|
+
model: selectedModel,
|
|
539
|
+
curatedMemories,
|
|
540
|
+
taskHint: prompt,
|
|
541
|
+
outcome: { status: "succeeded", failureCode: null },
|
|
542
|
+
usage: ui._lastUsage,
|
|
543
|
+
durationMs: Date.now() - runStartedAt,
|
|
544
|
+
runId: experienceRunId,
|
|
545
|
+
lang: targetLang,
|
|
546
|
+
});
|
|
547
|
+
experienceFinalized = true;
|
|
548
|
+
}
|
|
383
549
|
}
|
|
384
550
|
} catch (e) {
|
|
385
551
|
ui.stopSpinner();
|
|
552
|
+
memoryOutcome = signal.aborted ? "cancelled" : "failed";
|
|
553
|
+
if (!experienceFinalized && experienceContext && typeof H.finalizeExperienceRun === "function") {
|
|
554
|
+
H.finalizeExperienceRun(db, {
|
|
555
|
+
agentId: experienceContext.ctx.agentId,
|
|
556
|
+
projectPath: experienceContext.ctx.projectPath,
|
|
557
|
+
cwd: state.cwd,
|
|
558
|
+
runtime: experienceContext.rt,
|
|
559
|
+
permission: experienceContext.ctx.permission,
|
|
560
|
+
model: experienceContext.selectedModel,
|
|
561
|
+
curatedMemories: experienceContext.curatedMemories,
|
|
562
|
+
taskHint: prompt,
|
|
563
|
+
outcome: { status: signal.aborted ? "cancelled" : "failed", failureCode: signal.aborted ? "operator-cancelled" : "runtime-error" },
|
|
564
|
+
durationMs: Date.now() - runStartedAt,
|
|
565
|
+
runId: experienceRunId,
|
|
566
|
+
lang: experienceContext.targetLang,
|
|
567
|
+
});
|
|
568
|
+
experienceFinalized = true;
|
|
569
|
+
}
|
|
386
570
|
if (signal.aborted) {
|
|
387
571
|
// user Ctrl-C — SIGINT handler already printed
|
|
388
572
|
} else if (e && e.name === "AbortError") {
|
|
@@ -391,6 +575,21 @@ function startRepl(opts) {
|
|
|
391
575
|
ui.error((e && e.message) || String(e));
|
|
392
576
|
}
|
|
393
577
|
} finally {
|
|
578
|
+
if (!memorySettled && memoryContext?.turnId && typeof H.completeMemoryTurn === "function") {
|
|
579
|
+
try {
|
|
580
|
+
await H.completeMemoryTurn(db, memoryOutput, memoryContext, memoryRuntime, {
|
|
581
|
+
model: memoryModel,
|
|
582
|
+
outcome: memoryOutcome,
|
|
583
|
+
requestText: prompt,
|
|
584
|
+
invokeCurator: false,
|
|
585
|
+
});
|
|
586
|
+
memorySettled = true;
|
|
587
|
+
} catch {
|
|
588
|
+
ui.warn(memoryContext.lang === "ko"
|
|
589
|
+
? "메모리 영수증을 저장하지 못했습니다. 응답 실행은 유지됩니다."
|
|
590
|
+
: "The memory receipt could not be persisted; the response run is preserved.");
|
|
591
|
+
}
|
|
592
|
+
}
|
|
394
593
|
busy = false;
|
|
395
594
|
ui.endTurn();
|
|
396
595
|
currentAbort = null;
|
|
@@ -407,6 +606,7 @@ function startRepl(opts) {
|
|
|
407
606
|
const bin = H.which(H.RUNTIME_BIN[a]);
|
|
408
607
|
if (!bin) return ui.error(ui.t("runtimeNotInstalled", a));
|
|
409
608
|
state.runtime = { mode: "cli", kind: a };
|
|
609
|
+
state.modelPinned = false;
|
|
410
610
|
baseRuntime = state.runtime; // 명시적 /runtime 은 세션 기본으로 고정 (이후 auto-route가 덮어쓰지 않게)
|
|
411
611
|
state.native = {};
|
|
412
612
|
return ui.ok(ui.t("runtimeSet", a));
|
|
@@ -416,6 +616,7 @@ function startRepl(opts) {
|
|
|
416
616
|
a === "ollama"
|
|
417
617
|
? { mode: "api", backend: "ollama", model: state.runtime.backend === "ollama" ? state.runtime.model : null }
|
|
418
618
|
: { mode: "api", backend: a, model: null };
|
|
619
|
+
state.modelPinned = false;
|
|
419
620
|
baseRuntime = state.runtime; // 명시적 /runtime 은 세션 기본으로 고정
|
|
420
621
|
return ui.ok(ui.t("runtimeSet", runtimeLabel(state.runtime)));
|
|
421
622
|
}
|
|
@@ -438,6 +639,7 @@ function startRepl(opts) {
|
|
|
438
639
|
if (pinned && pinned !== "auto") spec = pinned;
|
|
439
640
|
else spec = caps.autoRuntimeFor(subject.capAgent, { installedKinds: installedKinds(), activeSpec: caps.specOf(baseRuntime) });
|
|
440
641
|
state.runtime = caps.runtimeFromSpec(spec);
|
|
642
|
+
state.modelPinned = false;
|
|
441
643
|
state.native = {};
|
|
442
644
|
}
|
|
443
645
|
// Tell the user when we routed to an image-capable runtime, or when the current one can't make images.
|
|
@@ -492,7 +694,7 @@ function startRepl(opts) {
|
|
|
492
694
|
slug: firm.slug,
|
|
493
695
|
label: displayName(firm) + " CEO",
|
|
494
696
|
system: sys,
|
|
495
|
-
capAgent: { name: firm.name, name_en: firm.name_en || firm.name, tagline: firm.tagline, system_prompt: sys },
|
|
697
|
+
capAgent: { name: firm.name, name_en: firm.name_en || firm.name, tagline: firm.tagline, tagline_en: firm.tagline_en, entity_kind: "team", system_prompt: sys },
|
|
496
698
|
};
|
|
497
699
|
state.history = [];
|
|
498
700
|
state.routePreambleOnce = null;
|
|
@@ -772,13 +974,33 @@ function startRepl(opts) {
|
|
|
772
974
|
research = true;
|
|
773
975
|
goal = goal.replace(/\s?--research(-evidence)?\b/g, "").trim();
|
|
774
976
|
}
|
|
775
|
-
|
|
977
|
+
if (H.ensureProjectForExecution) {
|
|
978
|
+
state.projectPath = H.ensureProjectForExecution(db, state.cwd, state.permission, "terminal-storm");
|
|
979
|
+
}
|
|
980
|
+
await H.stormRun(db, goal, {
|
|
981
|
+
ui,
|
|
982
|
+
cwd: state.cwd,
|
|
983
|
+
permission: state.permission,
|
|
984
|
+
projectPath: state.projectPath,
|
|
985
|
+
research,
|
|
986
|
+
});
|
|
776
987
|
return true;
|
|
777
988
|
}
|
|
778
989
|
case "build": {
|
|
779
990
|
if (!arg) return ui.warn("usage: /build <만들고 싶은 에이전트/팀 설명>"), true;
|
|
780
991
|
ui.line("");
|
|
781
|
-
|
|
992
|
+
if (typeof H.terminalBuild !== "function") throw new Error("Terminal MCP build preflight is unavailable");
|
|
993
|
+
if (H.ensureProjectForExecution) {
|
|
994
|
+
state.projectPath = H.ensureProjectForExecution(db, state.cwd, state.permission, "terminal-build");
|
|
995
|
+
}
|
|
996
|
+
await H.terminalBuild(db, arg, {
|
|
997
|
+
cwd: state.cwd,
|
|
998
|
+
modelPin: state.modelPinned ? state.runtime.model : null,
|
|
999
|
+
effortPin: state.effortPinned ? (state.effort || "none") : undefined,
|
|
1000
|
+
input: process.stdin,
|
|
1001
|
+
promptOutput: process.stderr,
|
|
1002
|
+
out: (line) => ui.line(String(line)),
|
|
1003
|
+
});
|
|
782
1004
|
return true;
|
|
783
1005
|
}
|
|
784
1006
|
case "route": {
|
|
@@ -799,17 +1021,38 @@ function startRepl(opts) {
|
|
|
799
1021
|
else await H.hepRun(["hep-search", arg], { cwd: state.cwd });
|
|
800
1022
|
return true;
|
|
801
1023
|
}
|
|
802
|
-
case "
|
|
803
|
-
if (!arg) return ui.warn("usage: /install <slug> (/search 로 먼저 찾으세요)"), true;
|
|
804
|
-
if (H.cloudInstall) {
|
|
805
|
-
try { const r = await H.cloudInstall(db, arg.trim()); ui.ok((r && r.name) ? `설치됨: ${r.name}` : `설치됨: ${arg.trim()}`); }
|
|
806
|
-
catch (e) { ui.error((e && e.message) || String(e)); }
|
|
807
|
-
} else ui.warn("설치 기능을 사용할 수 없습니다.");
|
|
808
|
-
return true;
|
|
809
|
-
}
|
|
1024
|
+
case "workforce":
|
|
810
1025
|
case "network":
|
|
811
1026
|
case "taskforce": {
|
|
812
|
-
if (!arg) return ui.warn("usage: /network <요청> (
|
|
1027
|
+
if (!arg) return ui.warn("usage: /network <요청> [--benchmark] (Agent Workforce Ontology)"), true;
|
|
1028
|
+
let goal = arg;
|
|
1029
|
+
const benchmark = /(?:^|\s)--benchmark(?:\s|$)/.test(goal);
|
|
1030
|
+
goal = goal.replace(/(?:^|\s)--benchmark(?=\s|$)/g, " ").trim();
|
|
1031
|
+
let concurrency;
|
|
1032
|
+
const parallel = goal.match(/(?:^|\s)--parallel\s+(\d+)\b/);
|
|
1033
|
+
if (parallel) {
|
|
1034
|
+
concurrency = Number(parallel[1]);
|
|
1035
|
+
goal = goal.replace(parallel[0], "").trim();
|
|
1036
|
+
}
|
|
1037
|
+
if (H.ensureProjectForExecution) {
|
|
1038
|
+
state.projectPath = H.ensureProjectForExecution(db, state.cwd, state.permission, "terminal-workforce");
|
|
1039
|
+
}
|
|
1040
|
+
ui.line("");
|
|
1041
|
+
await H.workforceRun(db, goal, {
|
|
1042
|
+
ui,
|
|
1043
|
+
cwd: state.cwd,
|
|
1044
|
+
permission: state.permission,
|
|
1045
|
+
runtime: state.runtime,
|
|
1046
|
+
projectPath: state.projectPath,
|
|
1047
|
+
modelPin: state.modelPinned ? state.runtime.model : null,
|
|
1048
|
+
effortPin: state.effortPinned ? (state.effort || "none") : undefined,
|
|
1049
|
+
concurrency,
|
|
1050
|
+
benchmark,
|
|
1051
|
+
});
|
|
1052
|
+
return true;
|
|
1053
|
+
}
|
|
1054
|
+
case "legacy-network": {
|
|
1055
|
+
if (!arg) return ui.warn("usage: /legacy-network <요청>"), true;
|
|
813
1056
|
ui.line("");
|
|
814
1057
|
await H.hepRun(["hep-network", arg, "--project", state.cwd, "--runtime", "terminal"], { cwd: state.cwd });
|
|
815
1058
|
return true;
|
|
@@ -833,6 +1076,9 @@ function startRepl(opts) {
|
|
|
833
1076
|
concurrency = Number(m[1]);
|
|
834
1077
|
goal = goal.replace(m[0], "").trim();
|
|
835
1078
|
}
|
|
1079
|
+
if (H.ensureProjectForExecution) {
|
|
1080
|
+
state.projectPath = H.ensureProjectForExecution(db, state.cwd, state.permission, "terminal-swarm");
|
|
1081
|
+
}
|
|
836
1082
|
await H.swarmRun(db, goal, {
|
|
837
1083
|
ui,
|
|
838
1084
|
cwd: state.cwd,
|
|
@@ -841,27 +1087,36 @@ function startRepl(opts) {
|
|
|
841
1087
|
concurrency,
|
|
842
1088
|
agent: state.subject && state.subject.capAgent,
|
|
843
1089
|
projectPath: state.projectPath,
|
|
1090
|
+
modelPin: state.modelPinned ? state.runtime.model : null,
|
|
1091
|
+
effortPin: state.effortPinned ? (state.effort || "none") : undefined,
|
|
844
1092
|
});
|
|
845
1093
|
return true;
|
|
846
1094
|
}
|
|
847
|
-
case "model":
|
|
1095
|
+
case "model": {
|
|
848
1096
|
// CLI(claude/codex/gemini)와 BYOK/Ollama 모두 지원 — 각 런타임의 모델 플래그로 전달.
|
|
849
|
-
|
|
1097
|
+
const requested = (arg || "").trim();
|
|
1098
|
+
state.runtime.model = !requested || requested.toLowerCase() === "auto" ? null : requested;
|
|
1099
|
+
state.modelPinned = Boolean(state.runtime.model);
|
|
850
1100
|
state.native = {}; // 새 모델로 세션 리셋
|
|
851
|
-
ui.ok(ui.t("modelSet", state.runtime.model ||
|
|
1101
|
+
ui.ok(ui.t("modelSet", state.runtime.model || "auto"));
|
|
852
1102
|
return true;
|
|
1103
|
+
}
|
|
853
1104
|
case "effort": {
|
|
854
1105
|
const lv = (arg || "").toLowerCase().trim();
|
|
855
1106
|
if (!lv) {
|
|
856
|
-
ui.info(ui.t("effortCurrent", state.effort ||
|
|
1107
|
+
ui.info(ui.t("effortCurrent", state.effortPinned ? (state.effort || "off") : "auto"));
|
|
857
1108
|
return true;
|
|
858
1109
|
}
|
|
859
1110
|
if (!["low", "medium", "high", "max", "auto", "off"].includes(lv)) {
|
|
860
1111
|
return ui.warn(ui.t("effortUsage")), true;
|
|
861
1112
|
}
|
|
862
1113
|
state.effort = lv === "auto" || lv === "off" ? null : lv;
|
|
1114
|
+
state.effortPinned = lv !== "auto";
|
|
1115
|
+
prefs.effort = state.effort;
|
|
1116
|
+
prefs.effortPinned = state.effortPinned;
|
|
1117
|
+
if (opts.savePrefs) opts.savePrefs(prefs);
|
|
863
1118
|
state.native = {};
|
|
864
|
-
ui.ok(ui.t("effortSet", state.effort ||
|
|
1119
|
+
ui.ok(ui.t("effortSet", state.effortPinned ? (state.effort || "off") : "auto"));
|
|
865
1120
|
return true;
|
|
866
1121
|
}
|
|
867
1122
|
case "permission":
|
|
@@ -982,16 +1237,15 @@ function startRepl(opts) {
|
|
|
982
1237
|
const servers = H.mcpServers ? H.mcpServers(db) : [];
|
|
983
1238
|
ui.line("");
|
|
984
1239
|
ui.rule("MCP");
|
|
985
|
-
ui.line(" " + ui.c.emerald(ui.t("mcp.playwright").padEnd(22)) + ui.c.blue("stdio ") + ui.c.green("on ") + ui.c.dim(" " + ui.t("mcp.fullOnly")));
|
|
986
1240
|
for (const s of servers) {
|
|
987
1241
|
let envKeys = [];
|
|
988
1242
|
try { envKeys = JSON.parse(s.env_keys_json || "[]"); } catch { /* ignore */ }
|
|
989
1243
|
const name = ui.lang === "en" ? (s.name_en || s.name) : s.name;
|
|
990
|
-
const on = s.enabled ? ui.c.green("on ") : ui.c.faint("off");
|
|
1244
|
+
const on = s.enabled && s.runtimeConsented ? ui.c.green("on ") : s.enabled ? ui.c.faint("consent") : ui.c.faint("off");
|
|
991
1245
|
const envStr = envKeys.length ? envKeys.join(", ") : "no key";
|
|
992
1246
|
ui.line(" " + ui.c.emerald(String(name).padEnd(22)) + ui.c.blue(String(s.transport || "").padEnd(7)) + on + ui.c.dim(" " + envStr));
|
|
993
1247
|
}
|
|
994
|
-
const wired = servers.filter((s) => s.enabled && s.transport === "stdio"
|
|
1248
|
+
const wired = servers.filter((s) => s.enabled && s.transport === "stdio" && s.runtimeEligible === true && s.runtimeConsented === true).length;
|
|
995
1249
|
ui.line(" " + ui.c.faint(ui.t("mcp.wired", String(wired))));
|
|
996
1250
|
ui.line(" " + ui.c.faint(ui.t("mcp.usage")));
|
|
997
1251
|
return true;
|
|
@@ -1026,11 +1280,16 @@ function startRepl(opts) {
|
|
|
1026
1280
|
} else {
|
|
1027
1281
|
return ui.error(ui.t("noAgent", s.agentSlug)), true;
|
|
1028
1282
|
}
|
|
1029
|
-
|
|
1283
|
+
// 저장된 CLI 런타임이 실제로 설치돼 있을 때만 그 세션을 복원한다. s.kind가 없거나
|
|
1284
|
+
// (손상된 resume 레코드) 런타임이 미설치면, 잘못된/"undefined" 키로 native 맵을
|
|
1285
|
+
// 덮어써 현재 세션을 잃는 것을 막고 새 대화로 시작한다.
|
|
1286
|
+
if (s.kind && s.sessionId && H.RUNTIME_BIN[s.kind] && H.which(H.RUNTIME_BIN[s.kind])) {
|
|
1030
1287
|
state.runtime = { mode: "cli", kind: s.kind };
|
|
1031
1288
|
baseRuntime = state.runtime;
|
|
1289
|
+
state.native = { [s.kind]: { id: s.sessionId } };
|
|
1290
|
+
} else {
|
|
1291
|
+
state.native = {};
|
|
1032
1292
|
}
|
|
1033
|
-
state.native = { [s.kind]: { id: s.sessionId } };
|
|
1034
1293
|
try {
|
|
1035
1294
|
const fs2 = require("node:fs");
|
|
1036
1295
|
if (s.cwd && fs2.existsSync(s.cwd)) state.cwd = s.cwd;
|
|
@@ -1213,17 +1472,29 @@ function startRepl(opts) {
|
|
|
1213
1472
|
}
|
|
1214
1473
|
}
|
|
1215
1474
|
}
|
|
1216
|
-
//
|
|
1475
|
+
// Workforce is the default for direct, goal-like work. An explicit
|
|
1476
|
+
// `/config network off` remains a durable opt-out; Storm stays opt-in.
|
|
1217
1477
|
if (state.subject && state.subject.kind === "direct" && goalLikePrompt(t)) {
|
|
1218
1478
|
if (prefs.autoStorm && H.stormRun) {
|
|
1219
1479
|
ui.info(ui.t("config.autoEngage", "stormbreaker", "storm"));
|
|
1220
1480
|
await H.stormRun(db, t, { ui, cwd: state.cwd, research: false });
|
|
1221
1481
|
return;
|
|
1222
1482
|
}
|
|
1223
|
-
if (prefs.autoNetwork && H.
|
|
1224
|
-
ui.info(ui.t("config.autoEngage", "
|
|
1483
|
+
if (prefs.autoNetwork && H.workforceRun) {
|
|
1484
|
+
ui.info(ui.t("config.autoEngage", "workforce ontology", "network"));
|
|
1485
|
+
if (H.ensureProjectForExecution) {
|
|
1486
|
+
state.projectPath = H.ensureProjectForExecution(db, state.cwd, state.permission, "terminal-workforce-auto");
|
|
1487
|
+
}
|
|
1225
1488
|
ui.line("");
|
|
1226
|
-
await H.
|
|
1489
|
+
await H.workforceRun(db, t, {
|
|
1490
|
+
ui,
|
|
1491
|
+
cwd: state.cwd,
|
|
1492
|
+
permission: state.permission,
|
|
1493
|
+
runtime: state.runtime,
|
|
1494
|
+
projectPath: state.projectPath,
|
|
1495
|
+
modelPin: state.modelPinned ? state.runtime.model : null,
|
|
1496
|
+
effortPin: state.effortPinned ? (state.effort || "none") : undefined,
|
|
1497
|
+
});
|
|
1227
1498
|
return;
|
|
1228
1499
|
}
|
|
1229
1500
|
}
|
|
@@ -1400,6 +1671,11 @@ function startRepl(opts) {
|
|
|
1400
1671
|
bootstrap();
|
|
1401
1672
|
}
|
|
1402
1673
|
|
|
1674
|
+
function applyPreferenceDefaults(prefs) {
|
|
1675
|
+
if (!Object.prototype.hasOwnProperty.call(prefs, "autoNetwork")) prefs.autoNetwork = true;
|
|
1676
|
+
return prefs;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1403
1679
|
function printHelp(ui) {
|
|
1404
1680
|
const c = ui.c;
|
|
1405
1681
|
ui.line("");
|
|
@@ -1472,4 +1748,11 @@ function printKeybindings(ui) {
|
|
|
1472
1748
|
for (const [k, v] of tips) ui.line(" " + c.emerald(k.padEnd(24)) + c.dim(v));
|
|
1473
1749
|
}
|
|
1474
1750
|
|
|
1475
|
-
module.exports = {
|
|
1751
|
+
module.exports = {
|
|
1752
|
+
startRepl,
|
|
1753
|
+
runtimeLabel,
|
|
1754
|
+
runtimePromptForSession,
|
|
1755
|
+
makeMemoryGuard,
|
|
1756
|
+
makeStyleGuard,
|
|
1757
|
+
applyPreferenceDefaults,
|
|
1758
|
+
};
|