agentlas 1.0.28 → 1.0.35
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 +102 -0
- package/README.md +5 -2
- package/bin/agentlas.cjs +15 -0
- package/engine/agentlas-cloud-runtime.cjs +54 -4
- package/engine/agentlas-i18n.cjs +4 -4
- package/engine/agentlas-input.cjs +10 -3
- package/engine/agentlas-judgment.cjs +0 -0
- package/engine/agentlas-native-host.cjs +69 -4
- package/engine/agentlas-onboard.cjs +20 -0
- package/engine/agentlas-ui.cjs +2 -0
- package/engine/agentlas-workforce.cjs +69 -13
- package/engine/agentlas.cjs +71 -3
- package/engine/agents/builder.cjs +84 -0
- package/engine/agents/router.cjs +8 -2
- package/engine/automation/launchd.cjs +131 -0
- package/engine/bootstrap-schema.sql +6 -44
- package/engine/browser/cdp.cjs +188 -0
- package/engine/browser/vault.cjs +124 -0
- package/engine/cli-output.cjs +262 -0
- package/engine/cloud-assets/package.cjs +5 -1
- package/engine/commands/automation.cjs +37 -1
- package/engine/commands/billing.cjs +3 -0
- package/engine/commands/browser.cjs +166 -8
- package/engine/commands/build.cjs +101 -20
- package/engine/commands/connect.cjs +162 -9
- package/engine/commands/creds.cjs +65 -3
- package/engine/commands/doctor.cjs +66 -4
- package/engine/commands/document.cjs +79 -0
- package/engine/commands/graph.cjs +1190 -0
- package/engine/commands/help.cjs +87 -9
- package/engine/commands/hep-cloud.cjs +9 -23
- package/engine/commands/hep-hub.cjs +9 -22
- package/engine/commands/hep-local.cjs +9 -24
- package/engine/commands/hep-network.cjs +9 -35
- package/engine/commands/index.cjs +50 -25
- package/engine/commands/list.cjs +10 -1
- package/engine/commands/mcp.cjs +6 -2
- package/engine/commands/native.cjs +18 -2
- package/engine/commands/plugin.cjs +22 -0
- package/engine/commands/project.cjs +79 -16
- package/engine/commands/roles.cjs +210 -0
- package/engine/commands/telegram.cjs +23 -16
- package/engine/commands/workforce.cjs +63 -12
- package/engine/core/desktop-core-fetch.cjs +98 -0
- package/engine/core/desktop-core.cjs +170 -0
- package/engine/graph/ask-model.cjs +159 -0
- package/engine/graph/interview.cjs +960 -0
- package/engine/graph/layout.cjs +139 -0
- package/engine/graph/package.cjs +223 -0
- package/engine/graph/vocabulary.generated.cjs +30 -0
- package/engine/hephaestus/local-core.cjs +159 -0
- package/engine/hephaestus/runtime.cjs +14 -10
- package/engine/project/controller.cjs +8 -8
- package/engine/project/team.cjs +99 -0
- package/engine/runtime-refusal.cjs +71 -0
- package/engine/runtimes/auth-evidence.cjs +78 -0
- package/engine/runtimes/detect.cjs +3 -0
- package/engine/runtimes/resolve.cjs +1 -1
- package/engine/sessions/prompt.cjs +16 -0
- package/engine/sessions/session.cjs +24 -2
- package/engine/telegram/connect.cjs +202 -0
- package/engine/tools/access-notice.cjs +86 -0
- package/engine/ui/palette.cjs +7 -3
- package/engine/ui/repl.cjs +120 -3
- package/engine/vendor/desktop-core.manifest.json +7 -0
- package/engine/workforce/capture.cjs +51 -1
- package/engine/workforce/deps.cjs +13 -0
- package/engine/workforce/local-core-transport.cjs +298 -0
- package/package.json +4 -2
- package/engine/commands/legacy-network.cjs +0 -29
|
@@ -1097,10 +1097,15 @@ function validateWorkOrder(value) {
|
|
|
1097
1097
|
function validateCandidateSet(value, workOrder, now = new Date(), options = {}) {
|
|
1098
1098
|
const set = assertObject(value, "candidateSet");
|
|
1099
1099
|
assertNoForbiddenFitSignals(set);
|
|
1100
|
-
|
|
1100
|
+
// projection은 로컬 Core(연합) 응답에만 있는 메뉴 투영 메타데이터다(실측
|
|
1101
|
+
// 2026-08-05, reference-first: fullDossier=false). 원격 서버는 보내지 않는다.
|
|
1102
|
+
// 없애고 되돌려 보내면 Core 쪽 다이제스트 대조가 위험하므로 선택 키로 허용한다.
|
|
1103
|
+
const exactKeys = [
|
|
1101
1104
|
"schemaVersion", "selectionSessionId", "workOrderId", "ontologyVersion",
|
|
1102
1105
|
"candidateSetDigest", "decisionOwner", "historyInfluence", "slots", "issuedAt", "expiresAt",
|
|
1103
|
-
]
|
|
1106
|
+
];
|
|
1107
|
+
if (Object.prototype.hasOwnProperty.call(set, "projection")) exactKeys.push("projection");
|
|
1108
|
+
assertExactKeys(set, exactKeys, "candidateSet", "candidate_set_invalid");
|
|
1104
1109
|
if (set.schemaVersion !== "agentlas.workforce-candidate-set.v1") fail("candidate_set_invalid", "unsupported candidate set schema");
|
|
1105
1110
|
assertId(set.selectionSessionId, "candidateSet.selectionSessionId");
|
|
1106
1111
|
if (set.workOrderId !== workOrder.workOrderId) fail("candidate_set_invalid", "candidate set workOrderId mismatch");
|
|
@@ -1126,11 +1131,15 @@ function validateCandidateSet(value, workOrder, now = new Date(), options = {})
|
|
|
1126
1131
|
const releases = new Set();
|
|
1127
1132
|
for (const candidate of assertArray(slotResult.candidates, `candidateSet.${slotId}.candidates`, 100)) {
|
|
1128
1133
|
assertObject(candidate, "candidate");
|
|
1129
|
-
|
|
1134
|
+
// missingMandatory는 로컬 Core(연합) 응답에만 있는 미충족 필수 표식이다
|
|
1135
|
+
// (실측 2026-08-05, fullDossier=true에도 동봉). 원격 서버는 보내지 않는다.
|
|
1136
|
+
const candidateKeys = [
|
|
1130
1137
|
"agentDefinitionId", "agentReleaseId", "releaseVersion", "packageHash", "contentDigest",
|
|
1131
1138
|
"entityKind", "name", "communities", "fitEvidence", "qualificationEvidence", "optionalGaps",
|
|
1132
1139
|
"semanticSnapshot", "operational",
|
|
1133
|
-
]
|
|
1140
|
+
];
|
|
1141
|
+
if (Object.prototype.hasOwnProperty.call(candidate, "missingMandatory")) candidateKeys.push("missingMandatory");
|
|
1142
|
+
assertExactKeys(candidate, candidateKeys, "candidate", "candidate_set_invalid");
|
|
1134
1143
|
assertId(candidate.agentDefinitionId, "candidate.agentDefinitionId");
|
|
1135
1144
|
const releaseId = assertId(candidate.agentReleaseId, "candidate.agentReleaseId");
|
|
1136
1145
|
if (releases.has(releaseId)) fail("candidate_set_invalid", `duplicate release ${releaseId} in ${slotId}`);
|
|
@@ -1151,10 +1160,19 @@ function validateCandidateSet(value, workOrder, now = new Date(), options = {})
|
|
|
1151
1160
|
if (typeof operational.callable !== "boolean" || typeof operational.installable !== "boolean") fail("candidate_set_invalid", "candidate operational flags are invalid");
|
|
1152
1161
|
assertIds(operational.unavailableReasons || [], "candidate.operational.unavailableReasons");
|
|
1153
1162
|
const semantic = assertObject(candidate.semanticSnapshot, "candidate.semanticSnapshot");
|
|
1154
|
-
|
|
1163
|
+
// knowledge·modalities는 로컬 Core 스냅샷에만 있는 확장 어휘다(실측 2026-08-05).
|
|
1164
|
+
// 원격 서버는 보내지 않는다 — missingMandatory·projection과 같은 규칙으로
|
|
1165
|
+
// "있으면 검증하고 허용", 원격 계약의 exact-keys는 그대로 둔다.
|
|
1166
|
+
const semanticKeys = [
|
|
1155
1167
|
"summaries", "roles", "skills", "toolCapabilities", "consumes", "produces",
|
|
1156
1168
|
"authorities", "runtimes", "languages",
|
|
1157
|
-
]
|
|
1169
|
+
];
|
|
1170
|
+
for (const optional of ["knowledge", "modalities"]) {
|
|
1171
|
+
if (Object.prototype.hasOwnProperty.call(semantic, optional)) semanticKeys.push(optional);
|
|
1172
|
+
}
|
|
1173
|
+
assertExactKeys(semantic, semanticKeys, "candidate.semanticSnapshot", "candidate_set_invalid");
|
|
1174
|
+
if (semantic.knowledge !== undefined) assertIds(semantic.knowledge, "candidate.semanticSnapshot.knowledge");
|
|
1175
|
+
if (semantic.modalities !== undefined) assertStrings(semantic.modalities, "candidate.semanticSnapshot.modalities");
|
|
1158
1176
|
assertStrings(semantic.summaries, "candidate.semanticSnapshot.summaries");
|
|
1159
1177
|
assertIds(semantic.roles, "candidate.semanticSnapshot.roles");
|
|
1160
1178
|
assertLeveledConcepts(semantic.skills, "candidate.semanticSnapshot.skills");
|
|
@@ -2102,6 +2120,28 @@ function create(deps = {}) {
|
|
|
2102
2120
|
tokenLedger.push({ role: role || "stage", runtime: runtimeKind || "?", model: modelPin || null, input, output, cached });
|
|
2103
2121
|
}
|
|
2104
2122
|
|
|
2123
|
+
/*
|
|
2124
|
+
* 일시 API 오류 1회 재시도 (실측 2026-08-05: 4슬롯·2.1M 토큰 편성이 마지막
|
|
2125
|
+
* 재검증 호출의 "Connection closed mid-response" 하나로 전멸).
|
|
2126
|
+
* 재시도의 근거는 부수효과 부재가 아니라 **일시 오류의 기계 표식**이고,
|
|
2127
|
+
* 부수효과가 가능한 write 권한 단계는 표식이 있어도 재시도하지 않는다
|
|
2128
|
+
* (자동화 스케줄러와 같은 원칙). D.runModel 주입·CLI 캡처·API 백엔드
|
|
2129
|
+
* 세 경로 모두 이 관문을 지난다 — 하니스가 단위로 검증할 수 있는 이유.
|
|
2130
|
+
*/
|
|
2131
|
+
// 한도·429는 재시도 분류에 들어가야 한다 — 빠져 있으면 같은 막힌 런타임에 즉시 재도전만 한다.
|
|
2132
|
+
const TRANSIENT_MODEL_ERROR_RE = /Connection closed mid-response|"terminal_reason":"api_error"|ECONNRESET|ETIMEDOUT|socket hang up|overloaded_error|rate.?limit|quota|\b429\b|usage limit|weekly limit/i;
|
|
2133
|
+
async function withTransientModelRetry(authorityMode, invoke) {
|
|
2134
|
+
try {
|
|
2135
|
+
return await invoke();
|
|
2136
|
+
} catch (error) {
|
|
2137
|
+
const replaySafeAuthority = authorityMode === "no-authority" || authorityMode === "read-only";
|
|
2138
|
+
const transient = TRANSIENT_MODEL_ERROR_RE.test(String((error && error.message) || error));
|
|
2139
|
+
if (!replaySafeAuthority || !transient) throw error;
|
|
2140
|
+
process.stderr.write(`workforce: transient api error on a ${authorityMode} stage — retrying once\n`);
|
|
2141
|
+
return invoke();
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2105
2145
|
async function runModel(runtime, system, prompt, context) {
|
|
2106
2146
|
const invocation = stageInvocation(runtime, context);
|
|
2107
2147
|
const executionRuntime = invocation.executionRuntime;
|
|
@@ -2116,7 +2156,7 @@ function create(deps = {}) {
|
|
|
2116
2156
|
? `${system}\n\n${localContextSlice}`
|
|
2117
2157
|
: system;
|
|
2118
2158
|
if (typeof D.runModel === "function") {
|
|
2119
|
-
return normalizeModelResult(await D.runModel({
|
|
2159
|
+
return withTransientModelRetry(context.authorityMode || "no-authority", async () => normalizeModelResult(await D.runModel({
|
|
2120
2160
|
runtime: executionRuntime,
|
|
2121
2161
|
system: effectiveSystem,
|
|
2122
2162
|
prompt,
|
|
@@ -2127,7 +2167,7 @@ function create(deps = {}) {
|
|
|
2127
2167
|
modelPin: invocation.modelPin,
|
|
2128
2168
|
effortPin: invocation.effort,
|
|
2129
2169
|
},
|
|
2130
|
-
}));
|
|
2170
|
+
})));
|
|
2131
2171
|
}
|
|
2132
2172
|
if (executionRuntime.mode === "cli") {
|
|
2133
2173
|
const authorityMode = context.authorityMode || "no-authority";
|
|
@@ -2156,7 +2196,7 @@ function create(deps = {}) {
|
|
|
2156
2196
|
}
|
|
2157
2197
|
noteIsolationWeakness(executionRuntime.kind, invocation.role);
|
|
2158
2198
|
}
|
|
2159
|
-
const
|
|
2199
|
+
const captureOnce = async () => normalizeModelResult(await D.captureRuntime(executionRuntime.kind, effectiveSystem, prompt, {
|
|
2160
2200
|
cwd: context.cwd,
|
|
2161
2201
|
env: context.env,
|
|
2162
2202
|
permission: context.permission,
|
|
@@ -2169,16 +2209,17 @@ function create(deps = {}) {
|
|
|
2169
2209
|
outputLimitBytes: authorityMode === "read-only" ? 24 * 1024 * 1024 : undefined,
|
|
2170
2210
|
envelope: true,
|
|
2171
2211
|
}));
|
|
2212
|
+
const captured = await withTransientModelRetry(authorityMode, captureOnce);
|
|
2172
2213
|
recordStageTokens(invocation.role, executionRuntime.kind, invocation.modelPin, captured.usage);
|
|
2173
2214
|
return captured;
|
|
2174
2215
|
}
|
|
2175
|
-
const viaApi = normalizeModelResult(await D.runApi(
|
|
2216
|
+
const viaApi = await withTransientModelRetry(context.authorityMode || "no-authority", async () => normalizeModelResult(await D.runApi(
|
|
2176
2217
|
executionRuntime.backend,
|
|
2177
2218
|
invocation.modelPin,
|
|
2178
2219
|
effectiveSystem,
|
|
2179
2220
|
prompt,
|
|
2180
2221
|
{ effort: invocation.effort, envelope: true },
|
|
2181
|
-
));
|
|
2222
|
+
)));
|
|
2182
2223
|
recordStageTokens(invocation.role, executionRuntime.backend, invocation.modelPin, viaApi.usage);
|
|
2183
2224
|
return viaApi;
|
|
2184
2225
|
}
|
|
@@ -2393,6 +2434,16 @@ function create(deps = {}) {
|
|
|
2393
2434
|
|
|
2394
2435
|
async function workforceRun(db, rawTask, ctx = {}) {
|
|
2395
2436
|
const task = assertString(rawTask, "task", 20_000);
|
|
2437
|
+
// 이 표면이 보는 후보 메뉴의 소스 스코프. 원격 MCP 경로의 정직한 기본은 "hub"
|
|
2438
|
+
// (터미널 workforce = 공개 Hub 메뉴 스태핑). 다른 값은 로컬 Core 전송을 가진
|
|
2439
|
+
// 호출자만 넘길 수 있다 — 여기서 조용히 넓히지 않는다.
|
|
2440
|
+
const sourceScope = (() => {
|
|
2441
|
+
const value = ctx.sourceScope === undefined ? "hub" : ctx.sourceScope;
|
|
2442
|
+
if (!["network", "local", "cloud", "hub"].includes(value)) {
|
|
2443
|
+
fail("source_scope_invalid", `sourceScope must be network|local|cloud|hub, got: ${String(value)}`);
|
|
2444
|
+
}
|
|
2445
|
+
return value;
|
|
2446
|
+
})();
|
|
2396
2447
|
const ui = ctx.ui || newUi();
|
|
2397
2448
|
const runtime = ctx.runtime || D.resolveRuntime(db, ctx.runtimeOverride);
|
|
2398
2449
|
const cwd = ctx.cwd || (typeof D.projectCwd === "function" ? D.projectCwd() : process.cwd());
|
|
@@ -2744,7 +2795,9 @@ function create(deps = {}) {
|
|
|
2744
2795
|
};
|
|
2745
2796
|
|
|
2746
2797
|
const supersedeCandidateSearch = (workOrder, refinementNumber, triggerKind) => {
|
|
2747
|
-
|
|
2798
|
+
// hubStage가 저장한 requestDigest와 같은 인자 모양이어야 행을 찾는다 —
|
|
2799
|
+
// search 인자에 sourceScope가 실리므로(2026-08-05) 여기서도 함께 계산한다.
|
|
2800
|
+
const requestDigest = sha256({ workOrder, sourceScope });
|
|
2748
2801
|
for (const row of receipt.hubTools) {
|
|
2749
2802
|
if (row.tool !== "workforce.search_candidates" || row.requestDigest !== requestDigest || row.authoritativeChain !== true) continue;
|
|
2750
2803
|
row.authoritativeChain = false;
|
|
@@ -3065,7 +3118,10 @@ function create(deps = {}) {
|
|
|
3065
3118
|
|
|
3066
3119
|
let refinementsUsed = 0;
|
|
3067
3120
|
const searchCurrentWorkOrder = async () => {
|
|
3068
|
-
|
|
3121
|
+
// sourceScope는 MCP 스키마상 required다. 예전에는 싣지 않아 서버 기본값
|
|
3122
|
+
// ("hub")에 의존했다 — 기본값이 바뀌면 이 표면의 실제 스코프가 조용히
|
|
3123
|
+
// 넓어지거나 좁아진다. 이 표면이 보는 메뉴를 스스로 선언한다.
|
|
3124
|
+
const candidateRaw = await hubStage("workforce.search_candidates", { workOrder, sourceScope });
|
|
3069
3125
|
candidateSet = validateCandidateSet(
|
|
3070
3126
|
candidateRaw,
|
|
3071
3127
|
workOrder,
|
package/engine/agentlas.cjs
CHANGED
|
@@ -14,10 +14,51 @@
|
|
|
14
14
|
* ctx.out/err stdout/stderr 한 줄 출력
|
|
15
15
|
* ctx.tableExists / ctx.columnExists
|
|
16
16
|
*/
|
|
17
|
+
/*
|
|
18
|
+
* EPIPE 방어 — 발행본 1.0.29 실설치 검증에서 발견(2026-08-06):
|
|
19
|
+
* `agentlas version | head -1` 처럼 파이프 소비자가 먼저 닫히면 stdout write가
|
|
20
|
+
* EPIPE를 던져 스택트레이스로 크래시했다(실측: version.cjs → ctx.out → EPIPE).
|
|
21
|
+
* `| head` `| less` `| grep -m1` 은 CLI의 일상 사용 패턴이고, 파이프 단절은
|
|
22
|
+
* 오류가 아니라 "그만 읽겠다"는 신호다 — 조용히 성공 종료한다. 런처(bin)는
|
|
23
|
+
* 엔진을 stdio:"inherit" 로 spawn 하므로 이 방어는 엔진 프로세스에 있어야 한다.
|
|
24
|
+
*/
|
|
25
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
26
|
+
stream.on("error", (error) => {
|
|
27
|
+
if (error && error.code === "EPIPE") process.exit(0);
|
|
28
|
+
throw error;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
* 예상 못 한 크래시의 마지막 예의 (clig.dev: unexpected error에는 디버그 정보와
|
|
34
|
+
* 버그 리포트 경로를, 리포트는 미리 채워진 URL로 손쉽게).
|
|
35
|
+
* 예전에는 Node 기본 동작(원시 스택트레이스)이 그대로 사용자에게 쏟아졌다 —
|
|
36
|
+
* EPIPE 크래시(1.0.29 실측)가 정확히 그 모습이었다. 스택은 진단에 필요하므로
|
|
37
|
+
* 숨기지 않되, 한 줄 요약과 이슈 URL(제목 미리 채움)을 함께 준다.
|
|
38
|
+
* 종료 코드는 관례대로 1. 여기서 복구를 시도하지 않는다(crash-only).
|
|
39
|
+
*/
|
|
40
|
+
function reportCrash(kind, error) {
|
|
41
|
+
const message = String((error && error.stack) || error);
|
|
42
|
+
const title = encodeURIComponent(`crash: ${String((error && error.message) || error).slice(0, 100)}`);
|
|
43
|
+
process.stderr.write([
|
|
44
|
+
"",
|
|
45
|
+
`agentlas hit an unexpected error (${kind}).`,
|
|
46
|
+
message,
|
|
47
|
+
"",
|
|
48
|
+
`Report it (pre-filled): https://github.com/agentlas-ai/agentlas-terminal/issues/new?title=${title}`,
|
|
49
|
+
`Include: your command, agentlas ${(() => { try { return require("./agentlas-banner.cjs").readVersion(); } catch { return "?"; } })()}, node ${process.version}, ${process.platform}.`,
|
|
50
|
+
"",
|
|
51
|
+
].join("\n"));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
process.on("uncaughtException", (error) => reportCrash("uncaughtException", error));
|
|
55
|
+
process.on("unhandledRejection", (error) => reportCrash("unhandledRejection", error));
|
|
56
|
+
|
|
17
57
|
const { openDb, seedBuiltins, tableExists, columnExists } = require("./core/db.cjs");
|
|
18
58
|
const { userDataDir } = require("./core/paths.cjs");
|
|
19
59
|
const { loadPrefs } = require("./agentlas-config.cjs");
|
|
20
60
|
const { Ui } = require("./agentlas-ui.cjs");
|
|
61
|
+
const { parseOutputFlags, render, renderError, isRichUi } = require("./cli-output.cjs");
|
|
21
62
|
const commands = require("./commands/index.cjs");
|
|
22
63
|
|
|
23
64
|
const SUPPORTED_LANGS = new Set(["ko", "en"]);
|
|
@@ -61,6 +102,22 @@ function buildCtx() {
|
|
|
61
102
|
},
|
|
62
103
|
out: (s = "") => process.stdout.write(s + "\n"),
|
|
63
104
|
err: (s = "") => process.stderr.write(s + "\n"),
|
|
105
|
+
/*
|
|
106
|
+
* 출력 계약 — 명령은 문자열이 아니라 {데이터+스키마}를 준다(cli-output.cjs).
|
|
107
|
+
* 형식(--json/--yaml/--quiet/--no-headers/--no-color) 해석은 여기 한 곳이라,
|
|
108
|
+
* 명령마다 --json 유무가 갈리거나 에러 형식이 달라지는 일이 없다.
|
|
109
|
+
*/
|
|
110
|
+
output: { ...require("./cli-output.cjs").DEFAULT_OPTIONS },
|
|
111
|
+
emit(result) {
|
|
112
|
+
const text = render(result, this.output);
|
|
113
|
+
if (text) process.stdout.write(text + "\n");
|
|
114
|
+
},
|
|
115
|
+
fail(error) {
|
|
116
|
+
process.stderr.write(renderError(error, this.output) + "\n");
|
|
117
|
+
},
|
|
118
|
+
get richUi() {
|
|
119
|
+
return isRichUi(this.output);
|
|
120
|
+
},
|
|
64
121
|
db: () => {
|
|
65
122
|
if (_db) return _db;
|
|
66
123
|
_db = openDb();
|
|
@@ -107,7 +164,13 @@ function main() {
|
|
|
107
164
|
if (helpRequested && helpCommand) {
|
|
108
165
|
const ctx = buildCtx();
|
|
109
166
|
const command = commands.resolveCommandName(helpCommand);
|
|
110
|
-
|
|
167
|
+
// ★명령이 자기 도움말을 갖고 있으면 그것을 보여준다.
|
|
168
|
+
// 예전에는 무조건 표 한 줄을 긁어(`runForCommand`) "Usage: agentlas graph [options]"
|
|
169
|
+
// 두 줄만 나왔다 — `graph help`에는 8줄짜리 제대로 된 안내가 있는데도 `--help`로는
|
|
170
|
+
// 영원히 닿지 못했다(사용자가 가장 먼저 치는 것이 `--help`다).
|
|
171
|
+
const code = commands.SELF_HELP_COMMANDS.has(command)
|
|
172
|
+
? commands.dispatch(ctx, [command, "help"])
|
|
173
|
+
: require("./commands/help.cjs").runForCommand(ctx, command);
|
|
111
174
|
process.exit(typeof code === "number" ? code : 0);
|
|
112
175
|
}
|
|
113
176
|
// 옵션 정규화: -h/--help/-V/--version 은 하위 명령으로 변환
|
|
@@ -117,12 +180,17 @@ function main() {
|
|
|
117
180
|
return a;
|
|
118
181
|
});
|
|
119
182
|
|
|
183
|
+
// 전역 출력 플래그는 명령에 닿기 전에 한 곳에서 뜯어낸다 —
|
|
184
|
+
// 명령마다 --json 유무가 갈리던 것을 구조로 막는다.
|
|
185
|
+
const { options: outputOptions, rest: commandArgv } = parseOutputFlags(normalized);
|
|
120
186
|
const ctx = buildCtx();
|
|
187
|
+
ctx.output = outputOptions;
|
|
121
188
|
let code;
|
|
122
189
|
try {
|
|
123
|
-
code = commands.dispatch(ctx,
|
|
190
|
+
code = commands.dispatch(ctx, commandArgv);
|
|
124
191
|
} catch (e) {
|
|
125
|
-
|
|
192
|
+
// 에러도 같은 형식 규율을 따른다: --json 이면 {"error":{code,message}}.
|
|
193
|
+
ctx.fail(e);
|
|
126
194
|
process.exit(1);
|
|
127
195
|
}
|
|
128
196
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* agents/builder — 터미널 소유 로컬 에이전트 빌더 (독립).
|
|
4
|
+
*
|
|
5
|
+
* 배경(2026-08-06 오너 원칙): 데스크탑/플러그인은 산출물·설정을 공유할 뿐 선행
|
|
6
|
+
* 전제가 아니다. 그런데 `agentlas build "<req>"`는 Hephaestus 네이티브
|
|
7
|
+
* 패스스루라 "Open Claude Code or Codex with the plugin, then /hep-build"라는
|
|
8
|
+
* 스텁만 냈다 — 플러그인 강제. 터미널은 자체 런타임(claude-code/codex/gemini)이
|
|
9
|
+
* 있으므로 빌더를 로컬로 돌린다.
|
|
10
|
+
*
|
|
11
|
+
* 계약: 빌더 에이전트를 멱등 시드(installed_agents)하고, `run`과 같은 실행
|
|
12
|
+
* 인프라(Orchestrator 세션)로 돌린다. 산출물은 `import`가 읽는 로컬 폴더 형식
|
|
13
|
+
* (AGENTS.md=시스템 프롬프트, manifest.md=이름/태그라인). 빌더가 마지막 줄에
|
|
14
|
+
* `BUILT: <folder>`를 찍으면 그 폴더를 자동 import 한다. 실패해도 폴더는 남아
|
|
15
|
+
* `agentlas import <folder>`로 언제든 설치할 수 있다.
|
|
16
|
+
*/
|
|
17
|
+
const crypto = require("node:crypto");
|
|
18
|
+
const { runWriteTransaction } = require("../agentlas-sqlite-policy.cjs");
|
|
19
|
+
const { columnExists } = require("../core/db.cjs");
|
|
20
|
+
|
|
21
|
+
const BUILDER_SLUG = "agentlas-builder";
|
|
22
|
+
const BUILDER_ID = "builtin-agentlas-builder";
|
|
23
|
+
|
|
24
|
+
const BUILDER_SYSTEM_PROMPT = [
|
|
25
|
+
"You are the Agentlas local agent builder, running inside the Agentlas terminal.",
|
|
26
|
+
"Your job: turn the user's request into an installable Agentlas agent, entirely on this machine — no external plugin, no desktop app.",
|
|
27
|
+
"",
|
|
28
|
+
"Produce a folder the terminal can import. In the current working directory create a folder named after the agent (kebab-case slug), containing exactly:",
|
|
29
|
+
" - AGENTS.md — the agent's full system prompt / soul: who it is, what it does, how it behaves, its guardrails. Write it as the instructions the agent itself will run under. Be specific and production-ready, not a description of the agent.",
|
|
30
|
+
" - manifest.md — first line `# <Agent Name>`, second line a one-sentence tagline.",
|
|
31
|
+
" - README.md — a short human-facing summary of what the agent does and how to use it.",
|
|
32
|
+
"",
|
|
33
|
+
"Rules:",
|
|
34
|
+
" - Decide the agent's scope from the request; if the request is thin, choose sensible, specific defaults and state them in README.md rather than asking endless questions.",
|
|
35
|
+
" - Do not invent credentials, API keys, or secrets. If the agent needs an env var, name it in README.md as something the user provides later.",
|
|
36
|
+
" - Keep everything inside the new folder. Do not modify files outside it.",
|
|
37
|
+
" - When finished, print exactly one final line: `BUILT: <relative-folder-path>` so the terminal can install it.",
|
|
38
|
+
].join("\n");
|
|
39
|
+
|
|
40
|
+
/** 빌더 에이전트를 멱등 보장한다(installed_agents). 반환: 에이전트 행 형태. */
|
|
41
|
+
function ensureBuilderAgent(db) {
|
|
42
|
+
const now = new Date().toISOString();
|
|
43
|
+
const hasVisibility = columnExists(db, "installed_agents", "visibility");
|
|
44
|
+
runWriteTransaction(db, () => {
|
|
45
|
+
const existing = db.prepare("SELECT id FROM installed_agents WHERE id=? OR slug=?").get(BUILDER_ID, BUILDER_SLUG);
|
|
46
|
+
if (existing) {
|
|
47
|
+
db.prepare("UPDATE installed_agents SET system_prompt=?, name=?, name_en=?, tagline=?, tagline_en=? WHERE id=?")
|
|
48
|
+
.run(BUILDER_SYSTEM_PROMPT, "Agent Builder", "Agent Builder", "Builds Agentlas agents locally", "Builds Agentlas agents locally", existing.id);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (hasVisibility) {
|
|
52
|
+
db.prepare(
|
|
53
|
+
"INSERT INTO installed_agents (id, slug, name, name_en, tagline, tagline_en, system_prompt, mcp_servers_json, env_requirements_json, preferred_backend, trust_grade, installed_at, tone, builtin, role, visibility) " +
|
|
54
|
+
"VALUES (?,?,?,?,?,?,?,'[]','[]',NULL,'A',?,?,1,?,?)",
|
|
55
|
+
).run(BUILDER_ID, BUILDER_SLUG, "Agent Builder", "Agent Builder", "Builds Agentlas agents locally", "Builds Agentlas agents locally", BUILDER_SYSTEM_PROMPT, now, "blue", "orchestrator", "visible");
|
|
56
|
+
} else {
|
|
57
|
+
db.prepare(
|
|
58
|
+
"INSERT INTO installed_agents (id, slug, name, name_en, tagline, tagline_en, system_prompt, mcp_servers_json, env_requirements_json, preferred_backend, trust_grade, installed_at, tone, builtin, role) " +
|
|
59
|
+
"VALUES (?,?,?,?,?,?,?,'[]','[]',NULL,'A',?,?,1,?)",
|
|
60
|
+
).run(BUILDER_ID, BUILDER_SLUG, "Agent Builder", "Agent Builder", "Builds Agentlas agents locally", "Builds Agentlas agents locally", BUILDER_SYSTEM_PROMPT, now, "blue", "orchestrator");
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
id: BUILDER_ID,
|
|
65
|
+
slug: BUILDER_SLUG,
|
|
66
|
+
name: "Agent Builder",
|
|
67
|
+
nameEn: "Agent Builder",
|
|
68
|
+
systemPrompt: BUILDER_SYSTEM_PROMPT,
|
|
69
|
+
builtin: true,
|
|
70
|
+
role: "orchestrator",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 빌더 산출물 마지막 줄에서 `BUILT: <folder>`를 뽑는다. */
|
|
75
|
+
function parseBuiltFolder(finalText) {
|
|
76
|
+
const lines = String(finalText || "").split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
77
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
78
|
+
const m = lines[i].match(/^BUILT:\s*(.+)$/);
|
|
79
|
+
if (m) return m[1].trim();
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { ensureBuilderAgent, parseBuiltFolder, BUILDER_SLUG, BUILDER_ID, BUILDER_SYSTEM_PROMPT };
|
package/engine/agents/router.cjs
CHANGED
|
@@ -49,8 +49,14 @@ function ensureJudgeRunner(db, runtime) {
|
|
|
49
49
|
model: resolved.model || undefined,
|
|
50
50
|
signal,
|
|
51
51
|
});
|
|
52
|
-
} catch {
|
|
53
|
-
|
|
52
|
+
} catch (error) {
|
|
53
|
+
/*
|
|
54
|
+
* ★사유를 ""로 지우지 않는다 — 여기서 지우면 판정 서비스는 "no connected model
|
|
55
|
+
* reached a valid judgment"라는 거짓 문장만 남긴다(모델은 닿았고, 한도라고
|
|
56
|
+
* 말했다). 러너 계약은 문자열이므로 예외를 그대로 올려 judgeLabels가 사유를
|
|
57
|
+
* 싣게 한다.
|
|
58
|
+
*/
|
|
59
|
+
throw error;
|
|
54
60
|
}
|
|
55
61
|
});
|
|
56
62
|
return judgment;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* automation/launchd — 앱/창이 꺼져 있어도 자동화를 돌리는 macOS 영속성 (2026-08-06).
|
|
4
|
+
*
|
|
5
|
+
* 배경(오너: "터미널인데 모든 기능이 다 돼야"): 터미널 automation daemon 은 포그라운드
|
|
6
|
+
* setInterval 이라 셸 창을 닫으면 멈춘다 — "데스크탑 없이 자동화가 발동"이 실제론 안 됐다.
|
|
7
|
+
* 데스크탑 electron/launchd/agent.ts 와 같은 방식으로 ~/Library/LaunchAgents 에 plist 를 써서
|
|
8
|
+
* launchctl 로 로드한다. plist 는 coarse StartInterval(기본 300s)마다 `agentlas automation tick`
|
|
9
|
+
* (1회 due 스윕 후 종료)을 poke 한다. DB 가 스케줄 권위이고 plist 는 poke 만 하므로 자동화별
|
|
10
|
+
* plist 동기화가 필요 없다 — 데스크탑과 정확히 같은 계약.
|
|
11
|
+
*
|
|
12
|
+
* ★Label 은 데스크탑("ai.agentlas.automations")과 다르게 둔다("ai.agentlas.cli.automations").
|
|
13
|
+
* 둘 다 설치돼 있어도 공유 DB 의 lease(claimDue)가 이중 실행을 막으므로 공존은 안전하고,
|
|
14
|
+
* 서로의 plist 를 install/uninstall 로 덮지 않게 하려는 것.
|
|
15
|
+
*
|
|
16
|
+
* macOS 전용(launchd). 다른 OS 는 supported:false 로 정직하게 알린다(자동화는 포그라운드
|
|
17
|
+
* `automation daemon` 으로만 — 조용히 안 되는 척하지 않는다).
|
|
18
|
+
*/
|
|
19
|
+
const { spawnSync } = require("node:child_process");
|
|
20
|
+
const fs = require("node:fs");
|
|
21
|
+
const os = require("node:os");
|
|
22
|
+
const path = require("node:path");
|
|
23
|
+
const { userDataDir } = require("../core/paths.cjs");
|
|
24
|
+
|
|
25
|
+
const LABEL = "ai.agentlas.cli.automations";
|
|
26
|
+
|
|
27
|
+
function isSupported() { return process.platform === "darwin"; }
|
|
28
|
+
function plistPath() { return path.join(os.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`); }
|
|
29
|
+
function domainTarget() { return `gui/${os.userInfo().uid}`; }
|
|
30
|
+
|
|
31
|
+
/** launchd 가 poke 할 CLI 진입점(절대경로). 전역 설치본이든 체크아웃이든 이 파일 기준으로 해석. */
|
|
32
|
+
function cliEntry() { return path.resolve(__dirname, "..", "..", "bin", "agentlas.cjs"); }
|
|
33
|
+
|
|
34
|
+
function logPath() {
|
|
35
|
+
const dir = path.join(userDataDir(), "logs");
|
|
36
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch { /* best-effort */ }
|
|
37
|
+
return path.join(dir, "launchd-automations.log");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function plistXml(intervalSec = 300) {
|
|
41
|
+
const node = process.execPath;
|
|
42
|
+
const entry = cliEntry();
|
|
43
|
+
const log = logPath();
|
|
44
|
+
const esc = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
45
|
+
return [
|
|
46
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
47
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
48
|
+
'<plist version="1.0">',
|
|
49
|
+
"<dict>",
|
|
50
|
+
" <key>Label</key>",
|
|
51
|
+
` <string>${LABEL}</string>`,
|
|
52
|
+
" <key>ProgramArguments</key>",
|
|
53
|
+
" <array>",
|
|
54
|
+
` <string>${esc(node)}</string>`,
|
|
55
|
+
` <string>${esc(entry)}</string>`,
|
|
56
|
+
" <string>automation</string>",
|
|
57
|
+
" <string>tick</string>",
|
|
58
|
+
" </array>",
|
|
59
|
+
" <key>StartInterval</key>",
|
|
60
|
+
` <integer>${Math.max(30, Math.floor(intervalSec))}</integer>`,
|
|
61
|
+
" <key>RunAtLoad</key>",
|
|
62
|
+
" <true/>",
|
|
63
|
+
" <key>ProcessType</key>",
|
|
64
|
+
" <string>Background</string>",
|
|
65
|
+
" <key>LowPriorityIO</key>",
|
|
66
|
+
" <true/>",
|
|
67
|
+
" <key>StandardOutPath</key>",
|
|
68
|
+
` <string>${esc(log)}</string>`,
|
|
69
|
+
" <key>StandardErrorPath</key>",
|
|
70
|
+
` <string>${esc(log)}</string>`,
|
|
71
|
+
"</dict>",
|
|
72
|
+
"</plist>",
|
|
73
|
+
"",
|
|
74
|
+
].join("\n");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** launchctl 실행 — throw 하지 않고 {code, stderr} 반환(상태 함수가 판정). */
|
|
78
|
+
function launchctl(args) {
|
|
79
|
+
const res = spawnSync("launchctl", args, { encoding: "utf8" });
|
|
80
|
+
return { code: res.status ?? -1, stderr: (res.stderr || "").trim() };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isLoaded() {
|
|
84
|
+
if (!isSupported()) return false;
|
|
85
|
+
return launchctl(["print", `${domainTarget()}/${LABEL}`]).code === 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function launchdStatus() {
|
|
89
|
+
const supported = isSupported();
|
|
90
|
+
return {
|
|
91
|
+
supported,
|
|
92
|
+
installed: supported && fs.existsSync(plistPath()),
|
|
93
|
+
loaded: supported && isLoaded(),
|
|
94
|
+
plistPath: plistPath(),
|
|
95
|
+
label: LABEL,
|
|
96
|
+
entry: cliEntry(),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** plist 작성 + launchctl bootstrap 로드(멱등 — 이미 로드면 bootout 후 재로드). */
|
|
101
|
+
function enableLaunchd({ intervalSec = 300 } = {}) {
|
|
102
|
+
if (!isSupported()) return { ...launchdStatus(), error: "launchd persistence is macOS-only." };
|
|
103
|
+
const p = plistPath();
|
|
104
|
+
try {
|
|
105
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
106
|
+
fs.writeFileSync(p, plistXml(intervalSec), "utf8");
|
|
107
|
+
} catch (err) {
|
|
108
|
+
return { ...launchdStatus(), error: `failed to write plist: ${String(err)}` };
|
|
109
|
+
}
|
|
110
|
+
if (isLoaded()) launchctl(["bootout", `${domainTarget()}/${LABEL}`]);
|
|
111
|
+
const res = launchctl(["bootstrap", domainTarget(), p]);
|
|
112
|
+
if (res.code !== 0 && !isLoaded()) {
|
|
113
|
+
return { ...launchdStatus(), error: res.stderr || "launchctl bootstrap failed." };
|
|
114
|
+
}
|
|
115
|
+
return launchdStatus();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** launchctl bootout + plist 삭제. */
|
|
119
|
+
function disableLaunchd() {
|
|
120
|
+
if (!isSupported()) return launchdStatus();
|
|
121
|
+
if (isLoaded()) launchctl(["bootout", `${domainTarget()}/${LABEL}`]);
|
|
122
|
+
const p = plistPath();
|
|
123
|
+
try { if (fs.existsSync(p)) fs.rmSync(p); }
|
|
124
|
+
catch (err) { return { ...launchdStatus(), error: `failed to remove plist: ${String(err)}` }; }
|
|
125
|
+
return launchdStatus();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = {
|
|
129
|
+
LABEL, plistPath, plistXml, cliEntry, isSupported,
|
|
130
|
+
launchdStatus, enableLaunchd, disableLaunchd,
|
|
131
|
+
};
|
|
@@ -412,50 +412,12 @@ CREATE TABLE automation_runs (
|
|
|
412
412
|
, last_activity_at TEXT, occurrence_id TEXT, graph_digest TEXT, checkpoint_json TEXT, resume_of_run_id TEXT);
|
|
413
413
|
CREATE INDEX idx_automation_runs_auto
|
|
414
414
|
ON automation_runs(automation_id, started_at);
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
OR lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%twitter%'
|
|
422
|
-
OR lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%x.com%'
|
|
423
|
-
OR lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%linkedin%'
|
|
424
|
-
OR lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%facebook%'
|
|
425
|
-
OR lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%tiktok%'
|
|
426
|
-
OR (lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%browser%' AND lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%post%')
|
|
427
|
-
OR (lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%web%' AND lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%login%')
|
|
428
|
-
OR coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'') LIKE '%레딧%'
|
|
429
|
-
OR coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'') LIKE '%인스타%'
|
|
430
|
-
OR coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'') LIKE '%댓글%'
|
|
431
|
-
OR coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'') LIKE '%게시%'
|
|
432
|
-
OR coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'') LIKE '%로그인%'
|
|
433
|
-
)
|
|
434
|
-
BEGIN
|
|
435
|
-
UPDATE automations SET tool_mode = 'computer-use' WHERE id = NEW.id;
|
|
436
|
-
END;
|
|
437
|
-
CREATE TRIGGER agentlas_auto_cua_social_update
|
|
438
|
-
AFTER UPDATE OF name, prompt_template, tool_mode ON automations
|
|
439
|
-
WHEN NEW.tool_mode = 'auto' AND (
|
|
440
|
-
lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%reddit%'
|
|
441
|
-
OR lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%instagram%'
|
|
442
|
-
OR lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%threads%'
|
|
443
|
-
OR lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%twitter%'
|
|
444
|
-
OR lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%x.com%'
|
|
445
|
-
OR lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%linkedin%'
|
|
446
|
-
OR lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%facebook%'
|
|
447
|
-
OR lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%tiktok%'
|
|
448
|
-
OR (lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%browser%' AND lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%post%')
|
|
449
|
-
OR (lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%web%' AND lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%login%')
|
|
450
|
-
OR coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'') LIKE '%레딧%'
|
|
451
|
-
OR coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'') LIKE '%인스타%'
|
|
452
|
-
OR coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'') LIKE '%댓글%'
|
|
453
|
-
OR coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'') LIKE '%게시%'
|
|
454
|
-
OR coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'') LIKE '%로그인%'
|
|
455
|
-
)
|
|
456
|
-
BEGIN
|
|
457
|
-
UPDATE automations SET tool_mode = 'computer-use' WHERE id = NEW.id;
|
|
458
|
-
END;
|
|
415
|
+
-- (제거됨 2026-08-06) agentlas_auto_cua_social_insert/update 트리거가 여기 있었다.
|
|
416
|
+
-- 소셜 키워드 목록("twitter/인스타/댓글/게시/로그인"…)으로 tool_mode를 computer-use로
|
|
417
|
+
-- 강제 되돌리던 DB 차원 단어목록 판정 — 코드의 단어목록을 LLM 판정으로 대체할 때
|
|
418
|
+
-- 이 트리거만 살아남아, 코드 리뷰가 볼 수 없는 곳에서 toolMode 도출 규칙을 무효화했다
|
|
419
|
+
-- (실측: UPDATE tool_mode='auto'가 같은 연결에서 즉시 되돌아왔다). 판정을 DB 트리거로
|
|
420
|
+
-- 만들지 않는다 — 판정은 코드·게이트가 보는 곳에만 산다.
|
|
459
421
|
CREATE TABLE agent_evolution_proposals (
|
|
460
422
|
id TEXT PRIMARY KEY,
|
|
461
423
|
agent_id TEXT NOT NULL,
|