agentlas 1.0.21 → 1.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +9 -26
- package/engine/agentlas-banner.cjs +0 -1
- package/engine/agentlas-capabilities.cjs +8 -71
- package/engine/agentlas-core-harness.cjs +18 -11
- package/engine/agentlas-experience-exchange.cjs +11 -75
- package/engine/agentlas-i18n.cjs +21 -21
- package/engine/agentlas-judgment.cjs +0 -0
- package/engine/agentlas-native-host.cjs +4 -31
- package/engine/agentlas-ui.cjs +13 -1
- package/engine/agentlas-workload-routing.cjs +11 -24
- package/engine/agentlas.cjs +3 -13
- package/engine/agents/router.cjs +0 -1
- package/engine/architecture.data.json +5 -5
- package/engine/bootstrap-schema.sql +3 -3
- package/engine/commands/doctor.cjs +3 -3
- package/engine/commands/help.cjs +4 -7
- package/engine/commands/index.cjs +1 -6
- package/engine/experience/runtime.cjs +1 -0
- package/engine/sessions/orchestrator.cjs +4 -21
- package/engine/sessions/session.cjs +66 -5
- package/engine/sessions/sink.cjs +13 -3
- package/engine/sessions/store.cjs +2 -2
- package/engine/ui/palette.cjs +3 -12
- package/engine/ui/renderer.cjs +3 -6
- package/engine/ui/repl.cjs +41 -93
- package/engine/workforce/deps.cjs +9 -6
- package/package.json +4 -3
- package/engine/agentlas-doctor.cjs +0 -224
- package/engine/commands/chat.cjs +0 -12
- package/engine/commands/chats.cjs +0 -33
- package/engine/commands/open.cjs +0 -52
package/engine/agentlas-ui.cjs
CHANGED
|
@@ -594,7 +594,19 @@ class Ui {
|
|
|
594
594
|
}
|
|
595
595
|
error(msg) {
|
|
596
596
|
this.stopSpinner();
|
|
597
|
-
|
|
597
|
+
// Last-resort presentation boundary for legacy/direct commands. Raw
|
|
598
|
+
// provider text, stack messages, paths and codes must never become UI.
|
|
599
|
+
// REPL/session paths route the private evidence to the controller before
|
|
600
|
+
// reaching this boundary; direct commands get a neutral recovery state.
|
|
601
|
+
void msg;
|
|
602
|
+
this._message(
|
|
603
|
+
"◆ ",
|
|
604
|
+
this.c.amber,
|
|
605
|
+
this.c.text,
|
|
606
|
+
this.lang === "ko"
|
|
607
|
+
? "One이 상태를 확인하고 복구하고 있습니다."
|
|
608
|
+
: "One is checking and recovering this operation.",
|
|
609
|
+
);
|
|
598
610
|
}
|
|
599
611
|
|
|
600
612
|
// 최종 텍스트(비스트리밍 경로)에 가벼운 마크다운 강조 적용 후 출력.
|
|
@@ -308,7 +308,7 @@ function resolveAllocation(options = {}) {
|
|
|
308
308
|
model: modelPin,
|
|
309
309
|
effort: pinnedEffort,
|
|
310
310
|
provider,
|
|
311
|
-
source: modelPin
|
|
311
|
+
source: modelPin ? "user-pin" : "unresolved",
|
|
312
312
|
fallbackReason: pinReasons.join(","),
|
|
313
313
|
aiReason: null,
|
|
314
314
|
};
|
|
@@ -355,24 +355,14 @@ function resolveAllocation(options = {}) {
|
|
|
355
355
|
|
|
356
356
|
let model = selected && selected.id;
|
|
357
357
|
let effectiveModelEntry = selected;
|
|
358
|
-
let source = decision.exactModelId ? "parent-ai-exact" : "
|
|
358
|
+
let source = decision.exactModelId ? "parent-ai-exact" : "unresolved";
|
|
359
359
|
if (modelPin) {
|
|
360
360
|
model = modelPin;
|
|
361
361
|
effectiveModelEntry = available.find((item) => item.id === modelPin) || null;
|
|
362
362
|
source = "user-pin";
|
|
363
363
|
reasons.push("explicit_model_pin");
|
|
364
364
|
} else if (!model) {
|
|
365
|
-
source = "
|
|
366
|
-
const activeId = cleanText(runtime && runtime.model, 160) || null;
|
|
367
|
-
const active = activeId ? available.find((item) => item.id === activeId) || null : null;
|
|
368
|
-
const issue = activeId ? candidateIssue(active, "active_model") : "active_model_unavailable";
|
|
369
|
-
if (issue) {
|
|
370
|
-
if (!reasons.includes(issue)) reasons.push(issue);
|
|
371
|
-
} else {
|
|
372
|
-
model = active.id;
|
|
373
|
-
effectiveModelEntry = active;
|
|
374
|
-
reasons.push("compliant_active_model_fallback");
|
|
375
|
-
}
|
|
365
|
+
source = "unresolved";
|
|
376
366
|
}
|
|
377
367
|
|
|
378
368
|
let effort;
|
|
@@ -406,21 +396,20 @@ function resolveAllocation(options = {}) {
|
|
|
406
396
|
function resolveAllocationAcrossRuntimes(options = {}) {
|
|
407
397
|
const runtimes = Array.isArray(options.runtimes) ? options.runtimes : [];
|
|
408
398
|
const decision = normalizeAllocation(options.decision);
|
|
409
|
-
const
|
|
410
|
-
const fallbackId = cleanText(fallbackRuntime && (fallbackRuntime.runtimeId || fallbackRuntime.id), 255) || null;
|
|
399
|
+
const explicitRuntime = options.runtime || null;
|
|
411
400
|
const requestedId = decision && decision.runtimeId;
|
|
412
401
|
const chosen = requestedId
|
|
413
402
|
? runtimes.find((runtime, index) => (cleanText(runtime && (runtime.runtimeId || runtime.id), 255) || `runtime-${index + 1}`) === requestedId) || null
|
|
414
|
-
:
|
|
415
|
-
const runtime = chosen ||
|
|
403
|
+
: explicitRuntime;
|
|
404
|
+
const runtime = chosen || null;
|
|
416
405
|
const resolution = resolveAllocation({ ...options, runtime, decision, availableModels: runtime && runtime.availableModels });
|
|
417
406
|
const requestedExact = Boolean(decision && decision.runtimeId && decision.exactModelId);
|
|
418
|
-
const runtimeId = cleanText(runtime && (runtime.runtimeId || runtime.id), 255) ||
|
|
407
|
+
const runtimeId = cleanText(runtime && (runtime.runtimeId || runtime.id), 255) || null;
|
|
419
408
|
if (requestedExact && chosen && resolution.model === decision.exactModelId) {
|
|
420
409
|
resolution.source = "parent-selected-live-runtime-model";
|
|
421
410
|
} else if (requestedExact) {
|
|
422
411
|
resolution.fallbackReason = [resolution.fallbackReason, chosen ? "parent_model_not_in_live_inventory" : "parent_runtime_not_in_live_inventory"].filter(Boolean).join(",");
|
|
423
|
-
if (resolution.source !== "user-pin") resolution.source = "
|
|
412
|
+
if (resolution.source !== "user-pin") resolution.source = "unresolved";
|
|
424
413
|
}
|
|
425
414
|
return { ...resolution, runtime, runtimeId, requestedRuntimeId: requestedId || null };
|
|
426
415
|
}
|
|
@@ -478,9 +467,7 @@ function createDecisionReceipt({ taskId, stage, decision, resolution, role, usag
|
|
|
478
467
|
);
|
|
479
468
|
const status = source === "user-pin" && hasResolvedCurrent
|
|
480
469
|
? "user-pin"
|
|
481
|
-
:
|
|
482
|
-
? "fallback-current"
|
|
483
|
-
: normalized && resolution && resolution.ok
|
|
470
|
+
: normalized && resolution && resolution.ok
|
|
484
471
|
? "resolved"
|
|
485
472
|
: "unresolved";
|
|
486
473
|
const riskCodes = new Set(normalized ? normalized.reasonCodes : []);
|
|
@@ -509,7 +496,7 @@ function createDecisionReceipt({ taskId, stage, decision, resolution, role, usag
|
|
|
509
496
|
},
|
|
510
497
|
reasonCodes,
|
|
511
498
|
inputFeatureHash: normalized && normalized.inputFeatureHash ? normalized.inputFeatureHash : featureHash,
|
|
512
|
-
selectorVersion: normalized ? normalized.selectorVersion : "
|
|
499
|
+
selectorVersion: normalized ? normalized.selectorVersion : "unresolved-no-model-judgment",
|
|
513
500
|
independentVerificationRequired:
|
|
514
501
|
riskCodes.has("high-risk") || riskCodes.has("critical-risk") || riskCodes.has("independent-verification"),
|
|
515
502
|
usage: normalizeObservedUsage(usage),
|
|
@@ -548,7 +535,7 @@ function plannerSystemPrompt({ language = "English", maxTasks = 12, mode = "swar
|
|
|
548
535
|
return [
|
|
549
536
|
`You are the higher-level workload allocator for an Agentlas ${mode}.`,
|
|
550
537
|
"Judge each child task using the full goal and planned dependency graph. Do not use a keyword lookup or fixed role-to-model table.",
|
|
551
|
-
"LIVE_RUNTIME_INVENTORY below is authoritative. For every child and synthesis, choose an exact runtimeId and exactModelId only from it. Do not infer, rename, or
|
|
538
|
+
"LIVE_RUNTIME_INVENTORY below is authoritative. For every child and synthesis, choose an exact runtimeId and exactModelId only from it. Do not infer, rename, invent, or substitute a model. If an exact choice cannot be justified, return an unresolved decision.",
|
|
552
539
|
`LIVE_RUNTIME_INVENTORY=${JSON.stringify(liveRuntimeInventory)}`,
|
|
553
540
|
"Choose effort none|minimal|low|medium|high|xhigh|max. Spend frontier/high effort only when the task's complexity, risk, context, or synthesis burden justifies it.",
|
|
554
541
|
`Return strict JSON only with at most ${Math.max(1, Math.min(24, maxTasks))} tasks:`,
|
package/engine/agentlas.cjs
CHANGED
|
@@ -136,11 +136,8 @@ function main() {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
if (code === undefined) {
|
|
139
|
-
// 알 수 없는
|
|
140
|
-
//
|
|
141
|
-
const { findAgent } = require("./agents/registry.cjs");
|
|
142
|
-
let agent = null;
|
|
143
|
-
try { agent = findAgent(ctx.db(), normalized[0]); } catch { /* db unavailable → run이 진단 */ }
|
|
139
|
+
// 알 수 없는 토큰은 프로젝트 작업으로 실행한다. 에이전트 이름 하나가
|
|
140
|
+
// 전역 대화 소유권으로 바뀌는 암묵 경로는 없다.
|
|
144
141
|
/*
|
|
145
142
|
* 오타 가드: 인자가 "공백 없는 한 단어" 하나뿐이고 명령도 에이전트도 아니면
|
|
146
143
|
* 그건 작업 지시가 아니라 명령 오타일 가능성이 압도적이다. 그대로 프롬프트로
|
|
@@ -148,7 +145,7 @@ function main() {
|
|
|
148
145
|
* 에서 ls -la 실행 실증). 가장 가까운 명령을 제안하고 정직하게 멈춘다.
|
|
149
146
|
* 진짜 한 단어 작업은 따옴표+run -p 로 그대로 실행된다.
|
|
150
147
|
*/
|
|
151
|
-
if (
|
|
148
|
+
if (normalized.length === 1 && /^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(normalized[0])) {
|
|
152
149
|
const token = normalized[0];
|
|
153
150
|
const names = Object.keys(commands.COMMANDS)
|
|
154
151
|
.concat(Object.keys(commands.COMMAND_ALIASES || {}))
|
|
@@ -163,13 +160,6 @@ function main() {
|
|
|
163
160
|
: `See: agentlas help · to run it as a task: agentlas run -p "${token}"`);
|
|
164
161
|
process.exit(1);
|
|
165
162
|
}
|
|
166
|
-
if (agent && normalized.length === 1) {
|
|
167
|
-
const { startRepl } = require("./ui/repl.cjs");
|
|
168
|
-
return startRepl(ctx, { agent: agent.slug }).then(
|
|
169
|
-
(replCode) => process.exit(replCode || 0),
|
|
170
|
-
(e) => { ctx.err(String((e && e.message) || e)); process.exit(1); },
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
163
|
code = commands.COMMANDS.run().run(ctx, normalized);
|
|
174
164
|
}
|
|
175
165
|
|
package/engine/agents/router.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
3
|
-
"emitterBlock": "## Memory (Agentlas curated memory)\n\
|
|
2
|
+
"version": "1.6.0",
|
|
3
|
+
"emitterBlock": "## Memory (Agentlas curated memory)\n\nAt the end of EVERY completed normal reply, emit exactly one hidden Memory Events\nenvelope. The runtime removes it before display. This envelope is the per-turn receipt:\nalways include a compact safe turn_summary, and use an empty candidates array when\nnothing durable was learned. Do not skip the envelope.\n\nRules:\n- Never include secrets, credentials, API keys, raw logs, or full transcripts.\n- Real credential values may live only in local project .env/.env.local,\n ignored signing/ or credentials/ files, or a local keychain/vault. Memory\n Events may mention env names and local relative paths only.\n- For deploy, release, store, billing, auth, API, or cloud work, first read the\n project's .agentlas/local-credentials.map.json and the top\n \"Local Credential Index\" section of .agentlas/project-soul-memory.md\n before saying a credential is missing.\n- One candidate per durable item. Keep \"content\" to one or two sentences.\n- \"memory_kind\": fact | decision | preference | risk | procedure | hypothesis | evidence | deprecation | conflict\n- \"suggested_scope\": user_identity | team_memory | project (this folder) | agent_repo | session (temporary) | discard\n- Use user_identity for a stable operator preference or personal fact (their name, role, language, tone,\n how they want you to behave) — these must outlive any one project. The curator only files user_identity\n when you label it so with \"confidence\": \"high\"; it never promotes into that scope, so a preference emitted\n at lower confidence is demoted to a throwaway session note.\n- \"agent_team\" is accepted only as a legacy alias for team_memory.\n- Add \"request_context\" when it improves future recall: user_intent, trigger_terms,\n cwd_at_request, target_project, target_path, cross_context, outcome.\n- Never put the raw user prompt or transcript in request_context.\n- Suggest a scope; the separate Memory Curator decides the final destination.\n- turn_summary is one value-free sentence about the completed outcome. It is not the\n user prompt, a transcript, raw log, secret, or absolute local path.\n\nFormat (always emit, including an empty candidates array):\n\n## Memory Events\n```json\n{\n \"schema_version\": \"agentlas.memory-ticket.v1\",\n \"turn_summary\": \"Completed outcome in one safe sentence.\",\n \"candidates\": [\n {\n \"memory_kind\": \"decision\",\n \"content\": \"...\",\n \"suggested_scope\": \"project\",\n \"confidence\": \"high\",\n \"sensitivity\": \"internal\",\n \"evidence_refs\": [],\n \"request_context\": {\n \"user_intent\": \"...\",\n \"trigger_terms\": [\"...\"],\n \"cwd_at_request\": null,\n \"target_project\": null,\n \"target_path\": null,\n \"cross_context\": false,\n \"outcome\": \"...\"\n }\n }\n ]\n}\n```",
|
|
4
4
|
"eventsHeading": "## Memory Events",
|
|
5
5
|
"memoryDir": ".agentlas",
|
|
6
6
|
"soulFile": "project-soul-memory.md",
|
|
@@ -72,12 +72,12 @@
|
|
|
72
72
|
"slug": "agentlas-orchestrator",
|
|
73
73
|
"name": "Agentlas 오케스트레이터",
|
|
74
74
|
"nameEn": "Agentlas Orchestrator",
|
|
75
|
-
"tagline": "
|
|
76
|
-
"taglineEn": "
|
|
75
|
+
"tagline": "프로젝트 컨트롤러를 지원하는 작업 단위 오케스트레이션",
|
|
76
|
+
"taglineEn": "Task-scoped orchestration under a project controller",
|
|
77
77
|
"role": "orchestrator",
|
|
78
78
|
"visibility": "background",
|
|
79
79
|
"tone": "blue",
|
|
80
|
-
"systemPrompt": "# Agentlas Orchestrator (built-in)\n\nYou are
|
|
80
|
+
"systemPrompt": "# Agentlas Orchestrator (built-in)\n\nYou are a project-bound orchestration capability, never a global chat owner.\n\nThe host supplies one connected Work project, its system prompt, ordered agent pool,\nmemory and the current WorkOrder. The first agent in that ordered pool owns the task.\nYou may act only when you are that first agent or when the owning controller delegates\nthis bounded WorkOrder to you.\n\n## Ownership and staffing\n- Preserve the project's ordered Orch/Worker model priorities.\n- Select task-scoped sub-agents from the project's explicit pool by full semantic\n judgment. Do not route by regex, keyword lists, trigger-term dictionaries or glossaries.\n- Every sub-agent remains subordinate to the project controller and exists only for\n its assignment. Never transfer session or project ownership.\n- Pin and validate exact releases before execution. Never silently substitute a\n missing, expired or incompatible agent.\n- An explicit named-agent call affects only that turn.\n\n## Recovery\n- Observe failures as private evidence, decide the safest available recovery with the\n connected model, execute reversible recovery automatically, and verify the outcome.\n- Do not expose raw errors, codes, stack traces, paths or internal component language.\n- Code provides state, evidence and finite capabilities only. You author the concise\n summary, question and action labels for the actual situation.\n- If model judgment is unavailable, remain unresolved. Never fabricate a semantic\n fallback or present a guessed diagnosis as success.\n\n## Completion\nReturn the verified project result and compact evidence. Record durable project memory\nwithout binding that memory to one replaceable agent release."
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
"id": "builtin-agentlas-app-builder",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
-- Agentlas first-run bootstrap schema (project-first Work contract)
|
|
2
|
-
-- Source DB user_version=
|
|
3
|
-
PRAGMA user_version=
|
|
2
|
+
-- Source DB user_version=86. Desktop remains the migration authority.
|
|
3
|
+
PRAGMA user_version=86;
|
|
4
4
|
CREATE TABLE active_runtime (
|
|
5
5
|
id INTEGER PRIMARY KEY CHECK(id = 1),
|
|
6
6
|
kind TEXT NOT NULL
|
|
@@ -35,7 +35,7 @@ CREATE TABLE chats (
|
|
|
35
35
|
agent_id TEXT NOT NULL,
|
|
36
36
|
title TEXT NOT NULL DEFAULT '새 채팅',
|
|
37
37
|
created_at TEXT NOT NULL,
|
|
38
|
-
updated_at TEXT NOT NULL, firm_id TEXT REFERENCES firms(id) ON DELETE SET NULL, archived_at TEXT, working_folder TEXT, kind TEXT NOT NULL DEFAULT 'user', parent_chat_id TEXT, used_at TEXT, continuous_mode INTEGER NOT NULL DEFAULT 0, swarm_mode INTEGER NOT NULL DEFAULT 0, last_viewed_at TEXT,
|
|
38
|
+
updated_at TEXT NOT NULL, firm_id TEXT REFERENCES firms(id) ON DELETE SET NULL, archived_at TEXT, working_folder TEXT, kind TEXT NOT NULL DEFAULT 'user', parent_chat_id TEXT, used_at TEXT, continuous_mode INTEGER NOT NULL DEFAULT 0, swarm_mode INTEGER NOT NULL DEFAULT 0, last_viewed_at TEXT, origin_surface TEXT NOT NULL DEFAULT 'work', runtime_selection_json TEXT,
|
|
39
39
|
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE SET NULL,
|
|
40
40
|
FOREIGN KEY(agent_id) REFERENCES installed_agents(id) ON DELETE CASCADE
|
|
41
41
|
);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/*
|
|
3
3
|
* doctor — 런타임·데이터·자격증명 건강 점검.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* 이 명령은 사용자가 명시적으로 요청한 현재 상태 관측만 한다.
|
|
5
|
+
* 자동 복구는 프로젝트 컨트롤러와 저장된 모델 우선순위가 맡으며, 이 명령은
|
|
6
|
+
* 오류 문자열을 분류하거나 설정을 자동 변경하지 않는다.
|
|
7
7
|
*/
|
|
8
8
|
const fs = require("node:fs");
|
|
9
9
|
const path = require("node:path");
|
package/engine/commands/help.cjs
CHANGED
|
@@ -4,14 +4,11 @@
|
|
|
4
4
|
const HELP = `agentlas — the operating system for agents, in your terminal
|
|
5
5
|
|
|
6
6
|
agentlas open the terminal (REPL)
|
|
7
|
-
agentlas <
|
|
8
|
-
agentlas "<task>" auto-route to the best agent and run once
|
|
7
|
+
agentlas "<task>" run once with this project's controller
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
run [agent] [prompt] one-shot (-p · --runtime · --model · --effort · --permission)
|
|
9
|
+
PROJECT WORK
|
|
10
|
+
run [agent] [prompt] project-first one-shot; exact agent is an explicit advanced override
|
|
13
11
|
firm <firm> [task] delegate to a CEO (--runtime · --model · --effort)
|
|
14
|
-
chats [n] · open <id> recent conversations · resume one
|
|
15
12
|
|
|
16
13
|
AGENTS & HUB
|
|
17
14
|
search "<what you need>" discover agents in the Hub
|
|
@@ -52,7 +49,7 @@ ACCOUNT & OPS
|
|
|
52
49
|
|
|
53
50
|
IN-REPL (agentlas → interactive, Orca multi-session)
|
|
54
51
|
/spawn <agent> [task] · /sessions · /tree · /s <n> · /steer <n> <msg> ·
|
|
55
|
-
/kill <n> · /rm <n> · /broadcast <msg> · /
|
|
52
|
+
/kill <n> · /rm <n> · /broadcast <msg> · /runtime · /model · /effort · /permission
|
|
56
53
|
typing during a running turn queues steering; ctrl-c interrupts the turn
|
|
57
54
|
|
|
58
55
|
Options: -p|--print · --runtime claude-code|codex|gemini · --model <exact-id> ·
|
|
@@ -12,7 +12,6 @@ const path = require("node:path");
|
|
|
12
12
|
const COMMANDS = {
|
|
13
13
|
version: () => require("./version.cjs"),
|
|
14
14
|
list: () => require("./list.cjs"),
|
|
15
|
-
chats: () => require("./chats.cjs"),
|
|
16
15
|
doctor: () => require("./doctor.cjs"),
|
|
17
16
|
mcp: () => require("./mcp.cjs"),
|
|
18
17
|
help: () => require("./help.cjs"),
|
|
@@ -32,10 +31,8 @@ const COMMANDS = {
|
|
|
32
31
|
install: () => require("./install.cjs"),
|
|
33
32
|
plugin: () => require("./plugin.cjs"),
|
|
34
33
|
plugins: () => require("./plugin.cjs"),
|
|
35
|
-
open: () => require("./open.cjs"),
|
|
36
34
|
automation: () => require("./automation.cjs"),
|
|
37
35
|
native: () => require("./native.cjs"),
|
|
38
|
-
chat: () => require("./chat.cjs"),
|
|
39
36
|
multimodal: () => require("./multimodal.cjs"),
|
|
40
37
|
workforce: () => require("./workforce.cjs"),
|
|
41
38
|
network: () => require("./workforce.cjs"),
|
|
@@ -94,11 +91,9 @@ const DESKTOP_ONLY_SURFACES = {
|
|
|
94
91
|
trex: "T-rex slide studio is Desktop-only.",
|
|
95
92
|
slides: "T-rex slide studio is Desktop-only.",
|
|
96
93
|
prompts: "Prompt Store is Desktop-only.",
|
|
97
|
-
dashboard: "Dashboard is Desktop-only — use: agentlas doctor · usage · list
|
|
94
|
+
dashboard: "Dashboard is Desktop-only — use: agentlas doctor · usage · list",
|
|
98
95
|
marketplace: "Marketplace browsing is Desktop-only — use: agentlas search \"<what you need>\"",
|
|
99
96
|
library: "Library is Desktop-only — use: agentlas list · env · mcp",
|
|
100
|
-
groups: "Agent groups (조합) are Desktop-only.",
|
|
101
|
-
"agent-groups": "Agent groups (조합) are Desktop-only.",
|
|
102
97
|
settings: "Settings UI is Desktop-only — use: agentlas setup · env · creds · multimodal · doctor",
|
|
103
98
|
apps: "Apps surface is Desktop-only.",
|
|
104
99
|
quests: "Quests are Desktop-only.",
|
|
@@ -137,6 +137,7 @@ function resolveRuntimeExperienceCli(agent, prompt, requested, cwd, overrides =
|
|
|
137
137
|
cwd,
|
|
138
138
|
prompt,
|
|
139
139
|
requested: prepared.requested || requested || {},
|
|
140
|
+
declaredTaskClasses: requested && requested.declaredTaskClasses,
|
|
140
141
|
agent,
|
|
141
142
|
agentRoot: agent ? (overrides.agentRoot || agentFolder(agent)) : null,
|
|
142
143
|
...(overrides.platform ? { platform: overrides.platform } : {}),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/*
|
|
3
|
-
* sessions/orchestrator —
|
|
3
|
+
* sessions/orchestrator — 프로젝트 Work의 컨트롤러/서브에이전트 실행 트리.
|
|
4
4
|
*
|
|
5
5
|
* 세션 번호는 s1, s2, … 로 붙는다(사람이 한 키로 지목할 수 있는 안정 번호).
|
|
6
6
|
* 활성 세션 하나만 터미널에 스트리밍되고, 나머지는 백그라운드에서 이벤트를
|
|
@@ -67,14 +67,12 @@ class Orchestrator extends EventEmitter {
|
|
|
67
67
|
|
|
68
68
|
session.on("event", (ev) => {
|
|
69
69
|
this.emit("session-event", { key, session, ev });
|
|
70
|
-
if (ev.type === "turn-end" && this.activeKey !== key) {
|
|
70
|
+
if (ev.type === "turn-end" && ev.ok && this.activeKey !== key) {
|
|
71
71
|
this.emit("notice", {
|
|
72
72
|
key,
|
|
73
73
|
session,
|
|
74
|
-
text:
|
|
75
|
-
|
|
76
|
-
: `${key} ${session.agent.slug}: ${ev.error || "failed"}`,
|
|
77
|
-
ok: !!ev.ok,
|
|
74
|
+
text: `${key} ${session.agent.slug}: done`,
|
|
75
|
+
ok: true,
|
|
78
76
|
});
|
|
79
77
|
}
|
|
80
78
|
});
|
|
@@ -157,21 +155,6 @@ class Orchestrator extends EventEmitter {
|
|
|
157
155
|
* 그래서 실패는 세션 단위로 모으고, 실제 전달된 목록은 무슨 일이 있어도 반환한다.
|
|
158
156
|
* (sendTo/spawn의 "상한 초과는 정직한 거부" 계약 자체는 그대로 둔다.)
|
|
159
157
|
*/
|
|
160
|
-
broadcast(prompt) {
|
|
161
|
-
const sent = [];
|
|
162
|
-
const skipped = [];
|
|
163
|
-
for (const [key, session] of this.sessions) {
|
|
164
|
-
if (session.status === "killed") continue;
|
|
165
|
-
try {
|
|
166
|
-
this.sendTo(key, prompt);
|
|
167
|
-
sent.push(key);
|
|
168
|
-
} catch (e) {
|
|
169
|
-
skipped.push({ key, error: String((e && e.message) || e) });
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
return { sent, skipped };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
158
|
/** 세션 표: [{key, active, agent, status, elapsed, lastLine, parentKey, depth}] */
|
|
176
159
|
list() {
|
|
177
160
|
const rows = [];
|
|
@@ -13,6 +13,9 @@ const crypto = require("node:crypto");
|
|
|
13
13
|
const { EventEmitter } = require("node:events");
|
|
14
14
|
const nativeHost = require("../agentlas-native-host.cjs");
|
|
15
15
|
const permissions = require("../agentlas-permissions.cjs");
|
|
16
|
+
const { RUNTIME_BIN, whichSync } = require("../runtimes/detect.cjs");
|
|
17
|
+
const { CLI_EXECUTABLE_KINDS } = require("../runtimes/resolve.cjs");
|
|
18
|
+
const { roleMembers } = require("../runtimes/roles.cjs");
|
|
16
19
|
const { EventSink } = require("./sink.cjs");
|
|
17
20
|
const store = require("./store.cjs");
|
|
18
21
|
|
|
@@ -37,6 +40,7 @@ class Session extends EventEmitter {
|
|
|
37
40
|
this.status = "idle"; // idle | running | done | failed | killed
|
|
38
41
|
this.lastLine = "";
|
|
39
42
|
this.lastError = null;
|
|
43
|
+
this._privateRecoveryEvidence = [];
|
|
40
44
|
this.startedAt = null;
|
|
41
45
|
this.endedAt = null;
|
|
42
46
|
this.usage = null;
|
|
@@ -52,7 +56,7 @@ class Session extends EventEmitter {
|
|
|
52
56
|
|
|
53
57
|
this.chatId = opts.chatId || store.createChat(this.db, {
|
|
54
58
|
agentId: this.agent.id,
|
|
55
|
-
title: opts.title || (opts.parent ? `sub: ${this.agent.slug}` : "New
|
|
59
|
+
title: opts.title || (opts.parent ? `sub: ${this.agent.slug}` : "New project task"),
|
|
56
60
|
kind: opts.parent ? "division" : "user",
|
|
57
61
|
parentChatId: opts.parent ? opts.parent.chatId : null,
|
|
58
62
|
workingFolder: this.cwd,
|
|
@@ -76,6 +80,11 @@ class Session extends EventEmitter {
|
|
|
76
80
|
this._sink = new EventSink({
|
|
77
81
|
lang: this.lang,
|
|
78
82
|
onEvent: (ev) => this._record(ev),
|
|
83
|
+
onPrivateEvidence: (text) => {
|
|
84
|
+
if (!text) return;
|
|
85
|
+
this._privateRecoveryEvidence.push(text.slice(0, 4000));
|
|
86
|
+
if (this._privateRecoveryEvidence.length > 16) this._privateRecoveryEvidence.shift();
|
|
87
|
+
},
|
|
79
88
|
});
|
|
80
89
|
}
|
|
81
90
|
|
|
@@ -143,16 +152,41 @@ class Session extends EventEmitter {
|
|
|
143
152
|
}
|
|
144
153
|
}
|
|
145
154
|
|
|
155
|
+
_nextRecoveryRuntime() {
|
|
156
|
+
const role = this.runtime.role === "worker" ? "worker" : "orchestrator";
|
|
157
|
+
const members = roleMembers(this.db, role);
|
|
158
|
+
const current = members.findIndex((member) =>
|
|
159
|
+
member.kind === this.runtime.kind &&
|
|
160
|
+
(member.model || null) === (this.runtime.model || null),
|
|
161
|
+
);
|
|
162
|
+
if (current < 0) return null;
|
|
163
|
+
for (const member of members.slice(current + 1)) {
|
|
164
|
+
if (!CLI_EXECUTABLE_KINDS.has(member.kind)) continue;
|
|
165
|
+
const bin = whichSync(RUNTIME_BIN[member.kind]);
|
|
166
|
+
if (!bin) continue;
|
|
167
|
+
return {
|
|
168
|
+
kind: member.kind,
|
|
169
|
+
bin,
|
|
170
|
+
model: member.model || undefined,
|
|
171
|
+
effort: member.effort || undefined,
|
|
172
|
+
role,
|
|
173
|
+
source: "model-role-pool-recovery",
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
146
179
|
async _runTurn(prompt) {
|
|
147
180
|
this.status = "running";
|
|
148
181
|
this.startedAt = Date.now();
|
|
149
182
|
this.lastError = null;
|
|
183
|
+
this._privateRecoveryEvidence.length = 0;
|
|
150
184
|
this._record({ type: "turn-start", at: Date.now(), prompt });
|
|
151
185
|
store.appendMessage(this.db, this.chatId, "user", prompt);
|
|
152
186
|
// 데스크탑처럼 첫 프롬프트로 자동 제목 — "New chat"으로 남는 목록 방지(실사용 테스트 발견).
|
|
153
187
|
try {
|
|
154
188
|
const row = this.db.prepare("SELECT title FROM chats WHERE id=?").get(this.chatId);
|
|
155
|
-
if (row && (
|
|
189
|
+
if (row && (["New chat", "New project task"].includes(row.title) || !row.title)) {
|
|
156
190
|
store.retitleChat(this.db, this.chatId, prompt.slice(0, 60));
|
|
157
191
|
}
|
|
158
192
|
} catch { /* 제목은 장식 — 실패해도 턴 진행 */ }
|
|
@@ -234,6 +268,33 @@ class Session extends EventEmitter {
|
|
|
234
268
|
if (this._timeoutConfig) req.timeoutConfig = this._timeoutConfig;
|
|
235
269
|
try {
|
|
236
270
|
res = await nativeHost.runNativeTurn(req);
|
|
271
|
+
if (res && res.error && !res.text && !res.finalText) {
|
|
272
|
+
const nextRuntime = this._nextRecoveryRuntime();
|
|
273
|
+
if (nextRuntime) {
|
|
274
|
+
const privateEvidence = [...this._privateRecoveryEvidence, String(res.error)]
|
|
275
|
+
.filter(Boolean).join("\n").slice(0, 12000);
|
|
276
|
+
this.runtime = nextRuntime;
|
|
277
|
+
this.runtimeSession = {};
|
|
278
|
+
this.fingerprint = crypto.createHash("sha256")
|
|
279
|
+
.update(`${nextRuntime.kind}\n${this.agent.id}\n${this.agent.systemPrompt || ""}`)
|
|
280
|
+
.digest("hex");
|
|
281
|
+
res = await nativeHost.runNativeTurn({
|
|
282
|
+
...req,
|
|
283
|
+
kind: nextRuntime.kind,
|
|
284
|
+
bin: nextRuntime.bin,
|
|
285
|
+
model: nextRuntime.model,
|
|
286
|
+
effort: nextRuntime.effort,
|
|
287
|
+
session: {},
|
|
288
|
+
prompt: [
|
|
289
|
+
prompt,
|
|
290
|
+
"",
|
|
291
|
+
"Private recovery evidence follows. Never repeat it to the user.",
|
|
292
|
+
privateEvidence,
|
|
293
|
+
"Inspect the complete situation, apply safe reversible recovery within the granted authority, verify it, and finish the original request. Ask one concise question only if user identity or an irreversible choice is required.",
|
|
294
|
+
].join("\n"),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
237
298
|
} catch (e) {
|
|
238
299
|
res = { text: "", session: req.session, error: (e && e.message) || String(e) };
|
|
239
300
|
}
|
|
@@ -274,8 +335,8 @@ class Session extends EventEmitter {
|
|
|
274
335
|
}
|
|
275
336
|
if (res && res.error) {
|
|
276
337
|
this.status = "failed";
|
|
277
|
-
this.lastError =
|
|
278
|
-
this._record({ type: "turn-end", at: Date.now(), ok: false,
|
|
338
|
+
this.lastError = null;
|
|
339
|
+
this._record({ type: "turn-end", at: Date.now(), ok: false, recoveryRequired: true });
|
|
279
340
|
} else {
|
|
280
341
|
this.status = "done";
|
|
281
342
|
// 링버퍼 표시 이벤트에도 raw 대신 cleanText — 제어 블록이 패널/lastLine 에 새지 않게.
|
|
@@ -287,7 +348,7 @@ class Session extends EventEmitter {
|
|
|
287
348
|
try {
|
|
288
349
|
require("./apply-fences.cjs").applyReplyFences(this, parsedFences, { orch: this.orchestrator });
|
|
289
350
|
} catch (e) {
|
|
290
|
-
this.
|
|
351
|
+
this._privateRecoveryEvidence.push(`fence apply failed: ${(e && e.message) || String(e)}`.slice(0, 4000));
|
|
291
352
|
}
|
|
292
353
|
}
|
|
293
354
|
return res;
|
package/engine/sessions/sink.cjs
CHANGED
|
@@ -16,11 +16,12 @@ const i18n = require("../agentlas-i18n.cjs");
|
|
|
16
16
|
const NOOP_PALETTE = new Proxy({}, { get: () => (s) => String(s) });
|
|
17
17
|
|
|
18
18
|
class EventSink {
|
|
19
|
-
constructor({ lang = "en", onEvent } = {}) {
|
|
19
|
+
constructor({ lang = "en", onEvent, onPrivateEvidence } = {}) {
|
|
20
20
|
this.lang = lang;
|
|
21
21
|
this.t = (key, ...args) => i18n.t(this.lang, key, ...args);
|
|
22
22
|
this.c = NOOP_PALETTE;
|
|
23
23
|
this._onEvent = onEvent || (() => {});
|
|
24
|
+
this._onPrivateEvidence = onPrivateEvidence || (() => {});
|
|
24
25
|
this._streamOpen = false;
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -30,10 +31,19 @@ class EventSink {
|
|
|
30
31
|
|
|
31
32
|
status(text) { this._emit("status", { text: String(text || "") }); }
|
|
32
33
|
warn(text) { this._emit("warn", { text: String(text || "") }); }
|
|
33
|
-
|
|
34
|
+
// Runtime/provider failures are recovery evidence, never presentation copy.
|
|
35
|
+
// Session owns the recovery loop and may give this evidence to the controller;
|
|
36
|
+
// Renderer must never receive it first.
|
|
37
|
+
error(text) { this._onPrivateEvidence(String(text || "")); }
|
|
34
38
|
line(text) { this._emit("line", { text: String(text || "") }); }
|
|
35
39
|
tool(name, summary) { this._emit("tool", { name: String(name || "tool"), summary: String(summary || "") }); }
|
|
36
|
-
toolResult(text, ok) {
|
|
40
|
+
toolResult(text, ok) {
|
|
41
|
+
if (ok === false) {
|
|
42
|
+
this._onPrivateEvidence(String(text || ""));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
this._emit("tool-result", { text: String(text || ""), ok: true });
|
|
46
|
+
}
|
|
37
47
|
streamStart() {
|
|
38
48
|
if (!this._streamOpen) { this._streamOpen = true; this._emit("stream-start", {}); }
|
|
39
49
|
}
|
|
@@ -21,7 +21,7 @@ function createChat(db, { agentId, title, kind = "user", parentChatId = null, wo
|
|
|
21
21
|
runWriteTransaction(db, () => {
|
|
22
22
|
db.prepare(
|
|
23
23
|
"INSERT INTO chats (id, agent_id, title, created_at, updated_at, kind, parent_chat_id, working_folder) VALUES (?,?,?,?,?,?,?,?)",
|
|
24
|
-
).run(id, agentId, title || "New
|
|
24
|
+
).run(id, agentId, title || "New project task", now, now, kind, parentChatId, workingFolder);
|
|
25
25
|
});
|
|
26
26
|
return id;
|
|
27
27
|
}
|
|
@@ -40,7 +40,7 @@ function appendMessage(db, chatId, role, text) {
|
|
|
40
40
|
|
|
41
41
|
function retitleChat(db, chatId, title) {
|
|
42
42
|
runWriteTransaction(db, () => {
|
|
43
|
-
db.prepare("UPDATE chats SET title=?, updated_at=? WHERE id=?").run(String(title || "New
|
|
43
|
+
db.prepare("UPDATE chats SET title=?, updated_at=? WHERE id=?").run(String(title || "New project task").slice(0, 120), nowIso(), chatId);
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
46
|
|
package/engine/ui/palette.cjs
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* ui/palette — v2 슬래시 명령 정본 + readline 완성기.
|
|
4
4
|
*
|
|
5
5
|
* 왜 v1 input 모듈의 SLASH_COMMANDS를 쓰지 않는가: 그 목록은 v1 REPL 전용이라
|
|
6
|
-
* v2에 없는 명령(/status /team /model /effort …)을
|
|
7
|
-
*
|
|
6
|
+
* v2에 없는 명령(/status /team /model /effort …)을 광고한다. 프로젝트 Work에서는
|
|
7
|
+
* 컨트롤러만 서브에이전트를 배정하므로 사용자용 임의 spawn/steer/broadcast는 없다.
|
|
8
8
|
* 거짓말하면 사용자는 없는 기능을 부른다(실사용 Tab 테스트에서 실증).
|
|
9
9
|
* 경로 완성만 v1 모듈의 completePath를 재사용한다.
|
|
10
10
|
*/
|
|
@@ -17,15 +17,10 @@ const SLASH_COMMANDS = [
|
|
|
17
17
|
{ command: "/tree", args: "", ko: "세션 트리", en: "Session tree" },
|
|
18
18
|
{ command: "/s", args: "<n>", ko: "활성 세션 전환", en: "Switch active session" },
|
|
19
19
|
{ command: "/switch", args: "<n>", ko: "활성 세션 전환", en: "Switch active session" },
|
|
20
|
-
{ command: "/spawn", args: "<agent> [task]", ko: "서브에이전트 세션 생성", en: "Spawn a subagent session" },
|
|
21
|
-
{ command: "/steer", args: "<n> <msg>", ko: "해당 세션에 지시 큐잉", en: "Queue steering for a session" },
|
|
22
20
|
{ command: "/kill", args: "<n>", ko: "실행 중 턴 중단", en: "Interrupt a running turn" },
|
|
23
21
|
{ command: "/rm", args: "<n>", ko: "세션 제거", en: "Remove a session" },
|
|
24
|
-
{ command: "/broadcast", args: "<msg>", ko: "모든 세션에 지시", en: "Send to every session" },
|
|
25
|
-
{ command: "/use", args: "<agent>", ko: "메인 세션 에이전트 교체", en: "Switch the main agent" },
|
|
26
22
|
{ command: "/agents", args: "", ko: "설치 에이전트 목록", en: "List installed agents" },
|
|
27
23
|
{ command: "/list", args: "", ko: "설치 에이전트 목록", en: "List installed agents" },
|
|
28
|
-
{ command: "/chats", args: "[n]", ko: "최근 대화", en: "Recent conversations" },
|
|
29
24
|
{ command: "/mcp", args: "", ko: "MCP 서버 목록", en: "MCP servers" },
|
|
30
25
|
{ command: "/doctor", args: "", ko: "런타임·데이터 점검", en: "Health check" },
|
|
31
26
|
{ command: "/runtime", args: "<kind>", ko: "새 세션 런타임 지정", en: "Set runtime for new sessions" },
|
|
@@ -99,8 +94,7 @@ const RUNTIME_KINDS = ["claude-code", "codex", "gemini"];
|
|
|
99
94
|
const EFFORT_LEVELS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
100
95
|
const PERM_LEVELS = ["read", "write", "full"];
|
|
101
96
|
// 세션 인자를 받는 명령 — 완성 후보를 살아있는 세션 키(s1, s2…)로 채운다.
|
|
102
|
-
const SESSION_ARG_COMMANDS = new Set(["/s", "/switch", "/
|
|
103
|
-
const AGENT_ARG_COMMANDS = new Set(["/use", "/spawn"]);
|
|
97
|
+
const SESSION_ARG_COMMANDS = new Set(["/s", "/switch", "/kill", "/rm"]);
|
|
104
98
|
|
|
105
99
|
function uniqStartsWith(list, prefix) {
|
|
106
100
|
const p = String(prefix || "");
|
|
@@ -137,9 +131,6 @@ function makeCompleter(ctx = {}) {
|
|
|
137
131
|
if (cmd === "/effort") return [uniqStartsWith(EFFORT_LEVELS, last), last];
|
|
138
132
|
if (cmd === "/permission") return [uniqStartsWith(PERM_LEVELS, last), last];
|
|
139
133
|
if (SESSION_ARG_COMMANDS.has(cmd) && tokens.length === 2) return [uniqStartsWith(getSessions(), last), last];
|
|
140
|
-
if (AGENT_ARG_COMMANDS.has(cmd) && tokens.length === 2) {
|
|
141
|
-
return [uniqStartsWith(getAgents().concat(getFirms()), last), last];
|
|
142
|
-
}
|
|
143
134
|
return [[], last];
|
|
144
135
|
};
|
|
145
136
|
}
|