agentlas 0.7.0 → 0.9.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 +199 -0
- package/README.md +161 -18
- package/bin/agentlas.cjs +8 -8
- package/engine/agentlas-core-harness.cjs +212 -0
- package/engine/agentlas-desktop-loadout.cjs +527 -0
- package/engine/agentlas-doctor.cjs +1 -1
- package/engine/agentlas-experience-exchange.cjs +835 -85
- package/engine/agentlas-experience-intake.cjs +444 -0
- package/engine/agentlas-experience-mcp.cjs +580 -18
- 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 +306 -31
- package/engine/agentlas-workforce.cjs +2991 -0
- package/engine/agentlas-workload-routing.cjs +523 -0
- package/engine/agentlas.cjs +1619 -234
- 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 -487
- package/test/credential-env-regression.cjs +0 -52
- package/test/engine-hardening-regression.cjs +0 -74
- package/test/experience-exchange-contract.cjs +0 -569
- package/test/experience-mcp-contract.cjs +0 -391
- package/test/fixtures/portable-experience-bundle-v1-golden.json +0 -124
- 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 -357
- package/test/run-api-regression.cjs +0 -322
- package/test/runtime-env-protection.cjs +0 -89
- package/test/semver-precedence.cjs +0 -39
- package/test/smoke.sh +0 -93
- package/test/sqlite-driver-probe.cjs +0 -22
- package/test/terminal-ui-regression.cjs +0 -477
- 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,40 +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);
|
|
329
404
|
// claude(--resume)·codex(resume <thread_id>)만 서버측에서 대화를 이어받아 이전 턴의
|
|
330
405
|
// 시스템 프롬프트를 유지한다(native-host.cjs claudeArgs/codexArgs). gemini CLI는 resume
|
|
331
406
|
// 플래그가 없어 매 턴이 새 프로세스 — session.id로 시스템 프롬프트를 비우면 2턴째부터
|
|
332
407
|
// 페르소나가 통째로 사라진다. 그래서 gemini는 매 턴 시스템 프롬프트를 다시 보낸다.
|
|
333
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
|
+
: [];
|
|
334
418
|
const res = await runNativeTurn({
|
|
335
419
|
kind: rt.kind,
|
|
336
420
|
bin,
|
|
337
|
-
prompt,
|
|
421
|
+
prompt: runtimePrompt,
|
|
338
422
|
systemPrompt: session.id && resumesServerSide ? "" : sys,
|
|
339
423
|
cwd: state.cwd,
|
|
340
424
|
permission: state.permission,
|
|
341
425
|
session,
|
|
342
|
-
model:
|
|
343
|
-
effort:
|
|
344
|
-
mcpServers:
|
|
345
|
-
state.permission === "full" && H.mcpServers
|
|
346
|
-
? H.mcpServers(db).filter((s) => s.enabled && s.transport === "stdio")
|
|
347
|
-
: [],
|
|
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,
|
|
348
429
|
env: runEnv,
|
|
349
|
-
ui:
|
|
430
|
+
ui: memoryGuard,
|
|
350
431
|
signal,
|
|
351
432
|
});
|
|
352
|
-
|
|
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
|
+
}
|
|
353
456
|
if (recordHistoryEntry && at && !res.error) state.history.push({ role: "user", text: prompt }, { role: "assistant", text: at });
|
|
354
457
|
recordCost(costLabel, res.usage);
|
|
355
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
|
+
}
|
|
356
478
|
} else {
|
|
357
479
|
const subjectSystem = state.routePreambleOnce
|
|
358
480
|
? `${state.routePreambleOnce}\n\n${state.subject.system}`
|
|
359
481
|
: state.subject.system;
|
|
360
482
|
state.routePreambleOnce = null;
|
|
361
|
-
const sys = H.augmentSystem(db, subjectSystem, ctx, true);
|
|
483
|
+
const sys = H.augmentSystem(db, subjectSystem, ctx, true, prompt);
|
|
362
484
|
let apiKey = null;
|
|
363
485
|
if (rt.backend !== "ollama") {
|
|
364
486
|
apiKey = await H.apiKey(rt.backend);
|
|
@@ -374,7 +496,7 @@ function startRepl(opts) {
|
|
|
374
496
|
const guard = makeMemoryGuard(assistantUi, H.eventsHeading());
|
|
375
497
|
const res = await runApiTurn({
|
|
376
498
|
backend: rt.backend,
|
|
377
|
-
model:
|
|
499
|
+
model: selectedModel || H.defaultApiModel(rt.backend),
|
|
378
500
|
apiKey,
|
|
379
501
|
system: sys,
|
|
380
502
|
messages,
|
|
@@ -382,12 +504,69 @@ function startRepl(opts) {
|
|
|
382
504
|
ui: guard,
|
|
383
505
|
signal,
|
|
384
506
|
});
|
|
385
|
-
|
|
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
|
+
}
|
|
386
529
|
if (recordHistoryEntry && cleaned) state.history.push({ role: "user", text: prompt }, { role: "assistant", text: cleaned });
|
|
387
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
|
+
}
|
|
388
549
|
}
|
|
389
550
|
} catch (e) {
|
|
390
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
|
+
}
|
|
391
570
|
if (signal.aborted) {
|
|
392
571
|
// user Ctrl-C — SIGINT handler already printed
|
|
393
572
|
} else if (e && e.name === "AbortError") {
|
|
@@ -396,6 +575,21 @@ function startRepl(opts) {
|
|
|
396
575
|
ui.error((e && e.message) || String(e));
|
|
397
576
|
}
|
|
398
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
|
+
}
|
|
399
593
|
busy = false;
|
|
400
594
|
ui.endTurn();
|
|
401
595
|
currentAbort = null;
|
|
@@ -412,6 +606,7 @@ function startRepl(opts) {
|
|
|
412
606
|
const bin = H.which(H.RUNTIME_BIN[a]);
|
|
413
607
|
if (!bin) return ui.error(ui.t("runtimeNotInstalled", a));
|
|
414
608
|
state.runtime = { mode: "cli", kind: a };
|
|
609
|
+
state.modelPinned = false;
|
|
415
610
|
baseRuntime = state.runtime; // 명시적 /runtime 은 세션 기본으로 고정 (이후 auto-route가 덮어쓰지 않게)
|
|
416
611
|
state.native = {};
|
|
417
612
|
return ui.ok(ui.t("runtimeSet", a));
|
|
@@ -421,6 +616,7 @@ function startRepl(opts) {
|
|
|
421
616
|
a === "ollama"
|
|
422
617
|
? { mode: "api", backend: "ollama", model: state.runtime.backend === "ollama" ? state.runtime.model : null }
|
|
423
618
|
: { mode: "api", backend: a, model: null };
|
|
619
|
+
state.modelPinned = false;
|
|
424
620
|
baseRuntime = state.runtime; // 명시적 /runtime 은 세션 기본으로 고정
|
|
425
621
|
return ui.ok(ui.t("runtimeSet", runtimeLabel(state.runtime)));
|
|
426
622
|
}
|
|
@@ -443,6 +639,7 @@ function startRepl(opts) {
|
|
|
443
639
|
if (pinned && pinned !== "auto") spec = pinned;
|
|
444
640
|
else spec = caps.autoRuntimeFor(subject.capAgent, { installedKinds: installedKinds(), activeSpec: caps.specOf(baseRuntime) });
|
|
445
641
|
state.runtime = caps.runtimeFromSpec(spec);
|
|
642
|
+
state.modelPinned = false;
|
|
446
643
|
state.native = {};
|
|
447
644
|
}
|
|
448
645
|
// Tell the user when we routed to an image-capable runtime, or when the current one can't make images.
|
|
@@ -777,15 +974,29 @@ function startRepl(opts) {
|
|
|
777
974
|
research = true;
|
|
778
975
|
goal = goal.replace(/\s?--research(-evidence)?\b/g, "").trim();
|
|
779
976
|
}
|
|
780
|
-
|
|
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
|
+
});
|
|
781
987
|
return true;
|
|
782
988
|
}
|
|
783
989
|
case "build": {
|
|
784
990
|
if (!arg) return ui.warn("usage: /build <만들고 싶은 에이전트/팀 설명>"), true;
|
|
785
991
|
ui.line("");
|
|
786
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
|
+
}
|
|
787
996
|
await H.terminalBuild(db, arg, {
|
|
788
997
|
cwd: state.cwd,
|
|
998
|
+
modelPin: state.modelPinned ? state.runtime.model : null,
|
|
999
|
+
effortPin: state.effortPinned ? (state.effort || "none") : undefined,
|
|
789
1000
|
input: process.stdin,
|
|
790
1001
|
promptOutput: process.stderr,
|
|
791
1002
|
out: (line) => ui.line(String(line)),
|
|
@@ -810,9 +1021,38 @@ function startRepl(opts) {
|
|
|
810
1021
|
else await H.hepRun(["hep-search", arg], { cwd: state.cwd });
|
|
811
1022
|
return true;
|
|
812
1023
|
}
|
|
1024
|
+
case "workforce":
|
|
813
1025
|
case "network":
|
|
814
1026
|
case "taskforce": {
|
|
815
|
-
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;
|
|
816
1056
|
ui.line("");
|
|
817
1057
|
await H.hepRun(["hep-network", arg, "--project", state.cwd, "--runtime", "terminal"], { cwd: state.cwd });
|
|
818
1058
|
return true;
|
|
@@ -836,6 +1076,9 @@ function startRepl(opts) {
|
|
|
836
1076
|
concurrency = Number(m[1]);
|
|
837
1077
|
goal = goal.replace(m[0], "").trim();
|
|
838
1078
|
}
|
|
1079
|
+
if (H.ensureProjectForExecution) {
|
|
1080
|
+
state.projectPath = H.ensureProjectForExecution(db, state.cwd, state.permission, "terminal-swarm");
|
|
1081
|
+
}
|
|
839
1082
|
await H.swarmRun(db, goal, {
|
|
840
1083
|
ui,
|
|
841
1084
|
cwd: state.cwd,
|
|
@@ -844,27 +1087,36 @@ function startRepl(opts) {
|
|
|
844
1087
|
concurrency,
|
|
845
1088
|
agent: state.subject && state.subject.capAgent,
|
|
846
1089
|
projectPath: state.projectPath,
|
|
1090
|
+
modelPin: state.modelPinned ? state.runtime.model : null,
|
|
1091
|
+
effortPin: state.effortPinned ? (state.effort || "none") : undefined,
|
|
847
1092
|
});
|
|
848
1093
|
return true;
|
|
849
1094
|
}
|
|
850
|
-
case "model":
|
|
1095
|
+
case "model": {
|
|
851
1096
|
// CLI(claude/codex/gemini)와 BYOK/Ollama 모두 지원 — 각 런타임의 모델 플래그로 전달.
|
|
852
|
-
|
|
1097
|
+
const requested = (arg || "").trim();
|
|
1098
|
+
state.runtime.model = !requested || requested.toLowerCase() === "auto" ? null : requested;
|
|
1099
|
+
state.modelPinned = Boolean(state.runtime.model);
|
|
853
1100
|
state.native = {}; // 새 모델로 세션 리셋
|
|
854
|
-
ui.ok(ui.t("modelSet", state.runtime.model ||
|
|
1101
|
+
ui.ok(ui.t("modelSet", state.runtime.model || "auto"));
|
|
855
1102
|
return true;
|
|
1103
|
+
}
|
|
856
1104
|
case "effort": {
|
|
857
1105
|
const lv = (arg || "").toLowerCase().trim();
|
|
858
1106
|
if (!lv) {
|
|
859
|
-
ui.info(ui.t("effortCurrent", state.effort ||
|
|
1107
|
+
ui.info(ui.t("effortCurrent", state.effortPinned ? (state.effort || "off") : "auto"));
|
|
860
1108
|
return true;
|
|
861
1109
|
}
|
|
862
1110
|
if (!["low", "medium", "high", "max", "auto", "off"].includes(lv)) {
|
|
863
1111
|
return ui.warn(ui.t("effortUsage")), true;
|
|
864
1112
|
}
|
|
865
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);
|
|
866
1118
|
state.native = {};
|
|
867
|
-
ui.ok(ui.t("effortSet", state.effort ||
|
|
1119
|
+
ui.ok(ui.t("effortSet", state.effortPinned ? (state.effort || "off") : "auto"));
|
|
868
1120
|
return true;
|
|
869
1121
|
}
|
|
870
1122
|
case "permission":
|
|
@@ -985,16 +1237,15 @@ function startRepl(opts) {
|
|
|
985
1237
|
const servers = H.mcpServers ? H.mcpServers(db) : [];
|
|
986
1238
|
ui.line("");
|
|
987
1239
|
ui.rule("MCP");
|
|
988
|
-
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")));
|
|
989
1240
|
for (const s of servers) {
|
|
990
1241
|
let envKeys = [];
|
|
991
1242
|
try { envKeys = JSON.parse(s.env_keys_json || "[]"); } catch { /* ignore */ }
|
|
992
1243
|
const name = ui.lang === "en" ? (s.name_en || s.name) : s.name;
|
|
993
|
-
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");
|
|
994
1245
|
const envStr = envKeys.length ? envKeys.join(", ") : "no key";
|
|
995
1246
|
ui.line(" " + ui.c.emerald(String(name).padEnd(22)) + ui.c.blue(String(s.transport || "").padEnd(7)) + on + ui.c.dim(" " + envStr));
|
|
996
1247
|
}
|
|
997
|
-
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;
|
|
998
1249
|
ui.line(" " + ui.c.faint(ui.t("mcp.wired", String(wired))));
|
|
999
1250
|
ui.line(" " + ui.c.faint(ui.t("mcp.usage")));
|
|
1000
1251
|
return true;
|
|
@@ -1221,17 +1472,29 @@ function startRepl(opts) {
|
|
|
1221
1472
|
}
|
|
1222
1473
|
}
|
|
1223
1474
|
}
|
|
1224
|
-
//
|
|
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.
|
|
1225
1477
|
if (state.subject && state.subject.kind === "direct" && goalLikePrompt(t)) {
|
|
1226
1478
|
if (prefs.autoStorm && H.stormRun) {
|
|
1227
1479
|
ui.info(ui.t("config.autoEngage", "stormbreaker", "storm"));
|
|
1228
1480
|
await H.stormRun(db, t, { ui, cwd: state.cwd, research: false });
|
|
1229
1481
|
return;
|
|
1230
1482
|
}
|
|
1231
|
-
if (prefs.autoNetwork && H.
|
|
1232
|
-
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
|
+
}
|
|
1233
1488
|
ui.line("");
|
|
1234
|
-
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
|
+
});
|
|
1235
1498
|
return;
|
|
1236
1499
|
}
|
|
1237
1500
|
}
|
|
@@ -1408,6 +1671,11 @@ function startRepl(opts) {
|
|
|
1408
1671
|
bootstrap();
|
|
1409
1672
|
}
|
|
1410
1673
|
|
|
1674
|
+
function applyPreferenceDefaults(prefs) {
|
|
1675
|
+
if (!Object.prototype.hasOwnProperty.call(prefs, "autoNetwork")) prefs.autoNetwork = true;
|
|
1676
|
+
return prefs;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1411
1679
|
function printHelp(ui) {
|
|
1412
1680
|
const c = ui.c;
|
|
1413
1681
|
ui.line("");
|
|
@@ -1480,4 +1748,11 @@ function printKeybindings(ui) {
|
|
|
1480
1748
|
for (const [k, v] of tips) ui.line(" " + c.emerald(k.padEnd(24)) + c.dim(v));
|
|
1481
1749
|
}
|
|
1482
1750
|
|
|
1483
|
-
module.exports = {
|
|
1751
|
+
module.exports = {
|
|
1752
|
+
startRepl,
|
|
1753
|
+
runtimeLabel,
|
|
1754
|
+
runtimePromptForSession,
|
|
1755
|
+
makeMemoryGuard,
|
|
1756
|
+
makeStyleGuard,
|
|
1757
|
+
applyPreferenceDefaults,
|
|
1758
|
+
};
|