@triflux/core 10.1.0 → 10.26.0
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/hooks/agent-route-guard.mjs +14 -10
- package/hooks/cross-review-tracker.mjs +21 -8
- package/hooks/error-context.mjs +20 -8
- package/hooks/hook-adaptive-collector.mjs +18 -16
- package/hooks/hook-manager.mjs +93 -32
- package/hooks/hook-orchestrator.mjs +267 -35
- package/hooks/hook-registry.json +343 -256
- package/hooks/hooks.json +49 -1
- package/hooks/keyword-rules.json +191 -31
- package/hooks/lib/resolve-root.mjs +1 -1
- package/hooks/mcp-config-watcher.mjs +6 -2
- package/hooks/permission-safe-allow.mjs +233 -0
- package/hooks/pipeline-stop.mjs +369 -22
- package/hooks/post-tool-tips.mjs +306 -0
- package/hooks/pre-compact-snapshot.mjs +181 -0
- package/hooks/safety-guard.mjs +262 -34
- package/hooks/session-end-cleanup.mjs +345 -0
- package/hooks/session-start-fast.mjs +214 -0
- package/hooks/subagent-tracker.mjs +267 -0
- package/hooks/subagent-verifier.mjs +9 -4
- package/hub/account-broker.mjs +1036 -66
- package/hub/adaptive-diagnostic.mjs +75 -48
- package/hub/adaptive-inject.mjs +96 -58
- package/hub/adaptive-memory.mjs +156 -42
- package/hub/adaptive.mjs +60 -31
- package/hub/assign-callbacks.mjs +163 -126
- package/hub/bridge.mjs +1051 -290
- package/hub/cli-adapter-base.mjs +321 -97
- package/hub/codex-adapter.mjs +179 -93
- package/hub/codex-compat.mjs +4 -4
- package/hub/codex-preflight.mjs +113 -37
- package/hub/delegator/contracts.mjs +22 -21
- package/hub/delegator/index.mjs +14 -14
- package/hub/delegator/schema/delegator-tools.schema.json +1 -1
- package/hub/delegator/service.mjs +93 -64
- package/hub/delegator/tool-definitions.mjs +35 -35
- package/hub/dynamic-routing-engine.mjs +511 -0
- package/hub/fullcycle.mjs +34 -18
- package/hub/gemini-adapter.mjs +82 -108
- package/hub/hitl.mjs +202 -143
- package/hub/intent.mjs +167 -38
- package/hub/lib/bash-path.mjs +73 -0
- package/hub/lib/cache-guard.mjs +43 -17
- package/hub/lib/env-detect.mjs +145 -0
- package/hub/lib/hosts-compat.mjs +319 -0
- package/hub/lib/mcp-response-cache.mjs +239 -0
- package/hub/lib/memory-store.mjs +285 -111
- package/hub/lib/path-utils.mjs +165 -0
- package/hub/lib/phase-manager.mjs +493 -0
- package/hub/lib/process-utils.mjs +797 -104
- package/hub/lib/prompt-tmp.mjs +33 -0
- package/hub/lib/risk-tier.mjs +53 -0
- package/hub/lib/spawn-trace.mjs +561 -0
- package/hub/lib/ssh-command.mjs +44 -24
- package/hub/lib/ssh-retry.mjs +5 -1
- package/hub/lib/staged-file-detect.mjs +43 -0
- package/hub/lib/state-snapshot.mjs +293 -0
- package/hub/lib/tfx-route-args.mjs +309 -0
- package/hub/lib/timeline-adapter.mjs +190 -0
- package/hub/lib/trace-recorder.mjs +153 -0
- package/hub/lib/uuidv7.mjs +4 -3
- package/hub/memory-doctor.mjs +266 -106
- package/hub/middleware/quota-middleware.mjs +70 -0
- package/hub/middleware/request-logger.mjs +61 -34
- package/hub/paths.mjs +9 -9
- package/hub/pipeline/gates/confidence.mjs +34 -15
- package/hub/pipeline/gates/consensus.mjs +27 -15
- package/hub/pipeline/gates/index.mjs +7 -3
- package/hub/pipeline/gates/selfcheck.mjs +57 -19
- package/hub/pipeline/index.mjs +77 -42
- package/hub/pipeline/state.mjs +10 -10
- package/hub/pipeline/transitions.mjs +40 -23
- package/hub/platform.mjs +65 -48
- package/hub/quality/deslop.mjs +70 -49
- package/hub/reflexion.mjs +87 -13
- package/hub/research.mjs +33 -25
- package/hub/router.mjs +941 -791
- package/hub/routing/complexity.mjs +132 -29
- package/hub/routing/index.mjs +17 -12
- package/hub/routing/q-learning.mjs +78 -30
- package/hub/routing-snapshot.mjs +261 -0
- package/hub/session-fingerprint.mjs +127 -61
- package/hub/state.mjs +86 -44
- package/hub/team/claude-daemon-control.mjs +1130 -0
- package/hub/team/retry-state-machine.mjs +336 -0
- package/hub/token-mode.mjs +72 -71
- package/hub/workers/worker-utils.mjs +23 -17
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +190 -27
- package/hud/context-monitor.mjs +133 -44
- package/hud/hud-qos-status.mjs +157 -50
- package/hud/mission-board.mjs +56 -0
- package/hud/providers/claude.mjs +398 -114
- package/hud/providers/codex.mjs +96 -21
- package/hud/providers/gemini.mjs +210 -52
- package/hud/renderers.mjs +440 -127
- package/hud/terminal.mjs +101 -31
- package/hud/utils.mjs +81 -39
- package/mesh/index.mjs +69 -0
- package/mesh/mesh-budget.mjs +137 -0
- package/mesh/mesh-heartbeat.mjs +100 -0
- package/mesh/mesh-protocol.mjs +96 -0
- package/mesh/mesh-queue.mjs +163 -0
- package/mesh/mesh-registry.mjs +78 -0
- package/mesh/mesh-router.mjs +77 -0
- package/package.json +5 -1
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +6 -1
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +37 -0
- package/scripts/lib/codex-recovery.sh +50 -0
- package/scripts/lib/context.mjs +3 -3
- package/scripts/lib/cross-review-utils.mjs +13 -5
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +58 -30
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/gemini-profiles.mjs +44 -10
- package/scripts/lib/handoff.mjs +33 -17
- package/scripts/lib/hook-utils.mjs +8 -6
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +49 -20
- package/scripts/lib/logger.mjs +24 -24
- package/scripts/lib/mcp-filter.mjs +391 -247
- package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
- package/scripts/lib/mcp-guard-engine.mjs +1281 -115
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +24 -14
- package/scripts/lib/mcp-server-catalog.mjs +355 -118
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +117 -12
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +44 -14
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +32 -7
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- package/scripts/lib/toml.mjs +113 -0
package/hub/gemini-adapter.mjs
CHANGED
|
@@ -1,41 +1,45 @@
|
|
|
1
|
-
// hub/gemini-adapter.mjs — Gemini
|
|
2
|
-
//
|
|
1
|
+
// hub/gemini-adapter.mjs — legacy Gemini adapter compatibility layer
|
|
2
|
+
// The "gemini" route is retained as an alias, but execution is Antigravity/agy.
|
|
3
3
|
|
|
4
|
-
import { mkdirSync } from
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
import { withRetry } from './workers/worker-utils.mjs';
|
|
9
|
-
import { whichCommandAsync } from './platform.mjs';
|
|
4
|
+
import { mkdirSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
10
7
|
import {
|
|
11
|
-
|
|
12
|
-
createResult,
|
|
13
|
-
appendWarnings,
|
|
8
|
+
executeWithCircuitBroker,
|
|
14
9
|
normalizePathForShell,
|
|
15
|
-
shellQuote,
|
|
16
10
|
runProcess,
|
|
17
|
-
} from
|
|
11
|
+
} from "./cli-adapter-base.mjs";
|
|
12
|
+
import { whichCommandAsync } from "./platform.mjs";
|
|
18
13
|
|
|
19
|
-
|
|
14
|
+
function shellSingleQuote(value) {
|
|
15
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
16
|
+
}
|
|
20
17
|
|
|
21
|
-
// ──
|
|
18
|
+
// ── Antigravity stall inference ─────────────────────────────────
|
|
22
19
|
|
|
23
20
|
function inferStallMode(stdout, stderr) {
|
|
24
21
|
const text = `${stdout}\n${stderr}`.toLowerCase();
|
|
25
|
-
if (/(rate.?limit|quota|resource.?exhaust|429)/u.test(text))
|
|
26
|
-
|
|
27
|
-
if (
|
|
28
|
-
|
|
22
|
+
if (/(rate.?limit|quota|resource.?exhaust|429)/u.test(text))
|
|
23
|
+
return "rate_limited";
|
|
24
|
+
if (
|
|
25
|
+
/(unauthorized|forbidden|auth|login|token|credential|api.?key)/u.test(text)
|
|
26
|
+
)
|
|
27
|
+
return "auth_stall";
|
|
28
|
+
if (/\bmcp\b|playwright|tavily|brave|sequential|server/u.test(text))
|
|
29
|
+
return "mcp_stall";
|
|
30
|
+
return "timeout";
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
// ── Preflight ───────────────────────────────────────────────────
|
|
32
34
|
|
|
33
35
|
async function runPreflight(opts = {}) {
|
|
34
|
-
const
|
|
35
|
-
if (!
|
|
36
|
+
const agyPath = await whichCommandAsync("agy");
|
|
37
|
+
if (!agyPath) {
|
|
36
38
|
return {
|
|
37
|
-
|
|
38
|
-
warnings: [
|
|
39
|
+
agyPath: null,
|
|
40
|
+
warnings: [
|
|
41
|
+
"Antigravity CLI not found. Install Antigravity and ensure `agy` is available on PATH.",
|
|
42
|
+
],
|
|
39
43
|
excludeMcpServers: [],
|
|
40
44
|
ok: false,
|
|
41
45
|
};
|
|
@@ -45,38 +49,32 @@ async function runPreflight(opts = {}) {
|
|
|
45
49
|
const excludeMcpServers = [];
|
|
46
50
|
|
|
47
51
|
for (const name of Array.isArray(opts.mcpServers) ? opts.mcpServers : []) {
|
|
48
|
-
const server = String(name ??
|
|
52
|
+
const server = String(name ?? "").trim();
|
|
49
53
|
if (!server) continue;
|
|
50
|
-
//
|
|
51
|
-
// 사전 probe는 수행하지 않음 (gemini가 자체적으로 graceful degrade)
|
|
54
|
+
// Antigravity MCP health는 best-effort: 실행 시점 probe는 수행하지 않음.
|
|
52
55
|
}
|
|
53
56
|
|
|
54
|
-
return {
|
|
57
|
+
return { agyPath, warnings, excludeMcpServers, ok: true };
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
// ── Command building ────────────────────────────────────────────
|
|
58
61
|
|
|
59
62
|
function buildGeminiCommand(prompt, resultFile, opts = {}) {
|
|
60
|
-
const parts = [
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
parts.push('--allowed-mcp-server-names', ...filtered.map((n) => shellQuote(n)));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
parts.push('--prompt', shellQuote(prompt));
|
|
73
|
-
parts.push('--output-format', 'text');
|
|
63
|
+
const parts = [
|
|
64
|
+
"printf",
|
|
65
|
+
"%s",
|
|
66
|
+
shellSingleQuote(prompt),
|
|
67
|
+
"|",
|
|
68
|
+
"agy",
|
|
69
|
+
"--print",
|
|
70
|
+
"--dangerously-skip-permissions",
|
|
71
|
+
];
|
|
74
72
|
|
|
75
73
|
if (resultFile) {
|
|
76
|
-
return `${parts.join(
|
|
74
|
+
return `${parts.join(" ")} > ${shellSingleQuote(normalizePathForShell(resultFile))} 2>${shellSingleQuote(normalizePathForShell(resultFile + ".err"))}`;
|
|
77
75
|
}
|
|
78
76
|
|
|
79
|
-
return parts.join(
|
|
77
|
+
return parts.join(" ");
|
|
80
78
|
}
|
|
81
79
|
|
|
82
80
|
function buildAttempts(opts, preflight) {
|
|
@@ -84,20 +82,19 @@ function buildAttempts(opts, preflight) {
|
|
|
84
82
|
const base = {
|
|
85
83
|
timeout,
|
|
86
84
|
model: opts.model,
|
|
87
|
-
allowedMcpServers: Array.isArray(opts.mcpServers)
|
|
85
|
+
allowedMcpServers: Array.isArray(opts.mcpServers)
|
|
86
|
+
? [...opts.mcpServers]
|
|
87
|
+
: [],
|
|
88
88
|
excludeMcpServers: [...(preflight.excludeMcpServers || [])],
|
|
89
89
|
};
|
|
90
90
|
if (opts.retryOnFail === false) return [base];
|
|
91
|
-
return [
|
|
92
|
-
base,
|
|
93
|
-
{ ...base, timeout: timeout * 2, allowedMcpServers: [] },
|
|
94
|
-
];
|
|
91
|
+
return [base, { ...base, timeout: timeout * 2, allowedMcpServers: [] }];
|
|
95
92
|
}
|
|
96
93
|
|
|
97
94
|
// ── Public: buildExecArgs ───────────────────────────────────────
|
|
98
95
|
|
|
99
96
|
export function buildExecArgs(opts = {}) {
|
|
100
|
-
const prompt = typeof opts.prompt ===
|
|
97
|
+
const prompt = typeof opts.prompt === "string" ? opts.prompt : "";
|
|
101
98
|
return buildGeminiCommand(prompt, opts.resultFile || null, {
|
|
102
99
|
model: opts.model,
|
|
103
100
|
allowedMcpServers: opts.mcpServers,
|
|
@@ -106,75 +103,52 @@ export function buildExecArgs(opts = {}) {
|
|
|
106
103
|
|
|
107
104
|
// ── Execution ───────────────────────────────────────────────────
|
|
108
105
|
|
|
109
|
-
async function runGemini(prompt, workdir, preflight, attempt) {
|
|
110
|
-
const dir = join(tmpdir(),
|
|
106
|
+
async function runGemini(prompt, workdir, preflight, attempt, lease) {
|
|
107
|
+
const dir = join(tmpdir(), "triflux-antigravity-exec");
|
|
111
108
|
mkdirSync(dir, { recursive: true });
|
|
112
|
-
const resultFile = join(
|
|
109
|
+
const resultFile = join(
|
|
110
|
+
dir,
|
|
111
|
+
`agy-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`,
|
|
112
|
+
);
|
|
113
113
|
const command = buildGeminiCommand(prompt, resultFile, {
|
|
114
114
|
model: attempt.model,
|
|
115
115
|
allowedMcpServers: attempt.allowedMcpServers,
|
|
116
116
|
excludeMcpServers: attempt.excludeMcpServers,
|
|
117
117
|
});
|
|
118
|
-
|
|
118
|
+
// PRD A1: lease.env 가 있으면 spawn env 에 적용 (예: GOOGLE_API_KEY 등 account-specific
|
|
119
|
+
// 환경변수). lease 가 null 이면 default 동작 유지 (부모 env inherit).
|
|
120
|
+
const spawnEnv =
|
|
121
|
+
lease?.env && typeof lease.env === "object"
|
|
122
|
+
? { ...process.env, ...lease.env }
|
|
123
|
+
: undefined;
|
|
124
|
+
return runProcess(command, workdir, attempt.timeout, {
|
|
125
|
+
resultFile,
|
|
126
|
+
inferStallMode,
|
|
127
|
+
spawnEnv,
|
|
128
|
+
});
|
|
119
129
|
}
|
|
120
130
|
|
|
121
131
|
// ── Public API ──────────────────────────────────────────────────
|
|
122
132
|
|
|
123
|
-
export function getCircuitState(
|
|
124
|
-
|
|
133
|
+
export async function getCircuitState() {
|
|
134
|
+
const brokerMod = await import("./account-broker.mjs");
|
|
135
|
+
if (!brokerMod.broker) return { state: "closed", failures: [] };
|
|
136
|
+
const snap = brokerMod.broker
|
|
137
|
+
.snapshot()
|
|
138
|
+
.filter((a) => a.provider === "gemini");
|
|
139
|
+
return snap.length
|
|
140
|
+
? { state: snap[0].circuitState, accounts: snap }
|
|
141
|
+
: { state: "closed", failures: [] };
|
|
125
142
|
}
|
|
126
143
|
|
|
127
|
-
export
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return createResult(false, {
|
|
138
|
-
stderr: appendWarnings('', preflight.warnings),
|
|
139
|
-
fellBack: opts.fallbackToClaude !== false,
|
|
140
|
-
failureMode: 'crash',
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const attempts = buildAttempts(opts, preflight);
|
|
145
|
-
let attemptIndex = 0;
|
|
146
|
-
let lastResult = createResult(false);
|
|
147
|
-
|
|
148
|
-
try {
|
|
149
|
-
lastResult = await withRetry(async () => {
|
|
150
|
-
const result = await runGemini(opts.prompt || '', opts.workdir || process.cwd(), preflight, attempts[attemptIndex]);
|
|
151
|
-
const current = { ...result, stderr: appendWarnings(result.stderr, preflight.warnings), retried: attemptIndex > 0 };
|
|
152
|
-
const canRetry = !current.ok && attemptIndex < attempts.length - 1;
|
|
153
|
-
attemptIndex += 1;
|
|
154
|
-
if (!canRetry) return current;
|
|
155
|
-
const error = new Error('retry');
|
|
156
|
-
error.retryable = true;
|
|
157
|
-
error.result = current;
|
|
158
|
-
throw error;
|
|
159
|
-
}, {
|
|
160
|
-
maxAttempts: attempts.length,
|
|
161
|
-
baseDelayMs: 250,
|
|
162
|
-
maxDelayMs: 750,
|
|
163
|
-
shouldRetry: (error) => error?.retryable === true,
|
|
164
|
-
});
|
|
165
|
-
} catch (error) {
|
|
166
|
-
lastResult = error?.result || createResult(false, { stderr: String(error?.message || error) });
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (lastResult.ok) {
|
|
170
|
-
breaker.reset();
|
|
171
|
-
return lastResult;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
breaker.recordFailure(entry.halfOpen);
|
|
175
|
-
return {
|
|
176
|
-
...lastResult,
|
|
177
|
-
retried: attempts.length > 1,
|
|
178
|
-
fellBack: opts.fallbackToClaude !== false,
|
|
179
|
-
};
|
|
144
|
+
export function execute(opts = {}) {
|
|
145
|
+
return executeWithCircuitBroker({
|
|
146
|
+
// Account-broker still stores this compatibility bucket as "gemini";
|
|
147
|
+
// execution itself is routed to agy above.
|
|
148
|
+
provider: "gemini",
|
|
149
|
+
runFn: runGemini,
|
|
150
|
+
preflightFn: (o) => runPreflight({ mcpServers: o.mcpServers }),
|
|
151
|
+
buildAttemptsFn: buildAttempts,
|
|
152
|
+
opts,
|
|
153
|
+
});
|
|
180
154
|
}
|
package/hub/hitl.mjs
CHANGED
|
@@ -1,143 +1,202 @@
|
|
|
1
|
-
// hub/hitl.mjs — Human-in-the-Loop 매니저
|
|
2
|
-
// 사용자 입력 요청/응답, 타임아웃 자동 처리
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* HITL 매니저 생성
|
|
6
|
-
* @param {object} store — createStore() 반환 객체
|
|
7
|
-
* @param {object} router — createRouter() 반환 객체
|
|
8
|
-
*/
|
|
9
|
-
export function createHitlManager(store, router = null) {
|
|
10
|
-
function forwardHumanResponse({
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
correlation_id
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
1
|
+
// hub/hitl.mjs — Human-in-the-Loop 매니저
|
|
2
|
+
// 사용자 입력 요청/응답, 타임아웃 자동 처리
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* HITL 매니저 생성
|
|
6
|
+
* @param {object} store — createStore() 반환 객체
|
|
7
|
+
* @param {object} router — createRouter() 반환 객체
|
|
8
|
+
*/
|
|
9
|
+
export function createHitlManager(store, router = null) {
|
|
10
|
+
function forwardHumanResponse({
|
|
11
|
+
requesterAgent,
|
|
12
|
+
requestId,
|
|
13
|
+
action,
|
|
14
|
+
content,
|
|
15
|
+
submittedBy,
|
|
16
|
+
correlationId,
|
|
17
|
+
traceId,
|
|
18
|
+
priority,
|
|
19
|
+
}) {
|
|
20
|
+
if (!router?.handlePublish) {
|
|
21
|
+
throw new Error("router.handlePublish is required for HITL forwarding");
|
|
22
|
+
}
|
|
23
|
+
return router.handlePublish({
|
|
24
|
+
from: "hub:hitl",
|
|
25
|
+
to: requesterAgent,
|
|
26
|
+
topic: "human.response",
|
|
27
|
+
priority,
|
|
28
|
+
ttl_ms: 300000,
|
|
29
|
+
payload: {
|
|
30
|
+
request_id: requestId,
|
|
31
|
+
action,
|
|
32
|
+
content,
|
|
33
|
+
submitted_by: submittedBy,
|
|
34
|
+
},
|
|
35
|
+
correlation_id: correlationId,
|
|
36
|
+
trace_id: traceId,
|
|
37
|
+
message_type: "human_response",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
/**
|
|
43
|
+
* 사용자에게 입력 요청 생성
|
|
44
|
+
* 터미널에 알림 출력 후 pending 상태로 저장
|
|
45
|
+
*/
|
|
46
|
+
requestHumanInput({
|
|
47
|
+
requester_agent,
|
|
48
|
+
kind,
|
|
49
|
+
prompt,
|
|
50
|
+
requested_schema = {},
|
|
51
|
+
deadline_ms,
|
|
52
|
+
default_action,
|
|
53
|
+
channel_preference = "terminal",
|
|
54
|
+
correlation_id,
|
|
55
|
+
trace_id,
|
|
56
|
+
}) {
|
|
57
|
+
const result = store.insertHumanRequest({
|
|
58
|
+
requester_agent,
|
|
59
|
+
kind,
|
|
60
|
+
prompt,
|
|
61
|
+
requested_schema,
|
|
62
|
+
deadline_ms,
|
|
63
|
+
default_action,
|
|
64
|
+
correlation_id,
|
|
65
|
+
trace_id,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// 터미널 알림 (stderr — stdout은 MCP 용)
|
|
69
|
+
const kindLabel = {
|
|
70
|
+
captcha: "CAPTCHA",
|
|
71
|
+
approval: "승인",
|
|
72
|
+
credential: "자격증명",
|
|
73
|
+
choice: "선택",
|
|
74
|
+
text: "텍스트",
|
|
75
|
+
};
|
|
76
|
+
process.stderr.write(
|
|
77
|
+
`\n[tfx-hub] 사용자 입력 요청 (${kindLabel[kind] || kind})\n` +
|
|
78
|
+
` 요청자: ${requester_agent}\n` +
|
|
79
|
+
` 내용: ${prompt}\n` +
|
|
80
|
+
` ID: ${result.request_id}\n` +
|
|
81
|
+
` 제한: ${Math.round(deadline_ms / 1000)}초\n\n`,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return { ok: true, data: result };
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 사용자 입력 응답 제출
|
|
89
|
+
* 유효성 검증 → 상태 업데이트 → 요청자에게 응답 메시지 전달
|
|
90
|
+
*/
|
|
91
|
+
submitHumanInput({
|
|
92
|
+
request_id,
|
|
93
|
+
action,
|
|
94
|
+
content = null,
|
|
95
|
+
submitted_by = "human",
|
|
96
|
+
}) {
|
|
97
|
+
// 요청 조회
|
|
98
|
+
const hr = store.getHumanRequest(request_id);
|
|
99
|
+
if (!hr) {
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
error: { code: "NOT_FOUND", message: `요청 없음: ${request_id}` },
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (hr.state !== "pending") {
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
error: {
|
|
109
|
+
code: "ALREADY_HANDLED",
|
|
110
|
+
message: `이미 처리됨: ${hr.state}`,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 상태 매핑
|
|
116
|
+
const stateMap = {
|
|
117
|
+
accept: "accepted",
|
|
118
|
+
decline: "declined",
|
|
119
|
+
cancel: "cancelled",
|
|
120
|
+
};
|
|
121
|
+
const newState = stateMap[action];
|
|
122
|
+
if (!newState) {
|
|
123
|
+
return {
|
|
124
|
+
ok: false,
|
|
125
|
+
error: {
|
|
126
|
+
code: "INVALID_ACTION",
|
|
127
|
+
message: `잘못된 action: ${action}`,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// DB 업데이트
|
|
133
|
+
store.updateHumanRequest(request_id, newState, content);
|
|
134
|
+
|
|
135
|
+
// 요청자에게 응답 메시지 전달
|
|
136
|
+
let forwardedMessageId = null;
|
|
137
|
+
if (action === "accept" || action === "decline") {
|
|
138
|
+
const published = forwardHumanResponse({
|
|
139
|
+
requesterAgent: hr.requester_agent,
|
|
140
|
+
requestId: request_id,
|
|
141
|
+
action,
|
|
142
|
+
content,
|
|
143
|
+
submittedBy: submitted_by,
|
|
144
|
+
correlationId: hr.correlation_id,
|
|
145
|
+
traceId: hr.trace_id,
|
|
146
|
+
priority: 7,
|
|
147
|
+
});
|
|
148
|
+
forwardedMessageId = published.data?.message_id || null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
ok: true,
|
|
153
|
+
data: {
|
|
154
|
+
request_id,
|
|
155
|
+
new_state: newState,
|
|
156
|
+
forwarded_message_id: forwardedMessageId,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 만료된 요청 자동 처리
|
|
163
|
+
* deadline 초과 시 default_action 적용
|
|
164
|
+
*/
|
|
165
|
+
checkTimeouts() {
|
|
166
|
+
const pending = store.getPendingHumanRequests();
|
|
167
|
+
const now = Date.now();
|
|
168
|
+
const expired = pending.filter((hr) => hr.deadline_ms <= now);
|
|
169
|
+
if (!expired.length) return 0;
|
|
170
|
+
|
|
171
|
+
const expireRequests = () => {
|
|
172
|
+
for (const hr of expired) {
|
|
173
|
+
store.updateHumanRequest(hr.request_id, "timed_out", null);
|
|
174
|
+
if (hr.default_action === "timeout_continue") {
|
|
175
|
+
forwardHumanResponse({
|
|
176
|
+
requesterAgent: hr.requester_agent,
|
|
177
|
+
requestId: hr.request_id,
|
|
178
|
+
action: "timeout_continue",
|
|
179
|
+
content: null,
|
|
180
|
+
submittedBy: "system",
|
|
181
|
+
correlationId: hr.correlation_id,
|
|
182
|
+
traceId: hr.trace_id,
|
|
183
|
+
priority: 5,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return expired.length;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const processExpired = store.db?.transaction
|
|
191
|
+
? store.db.transaction(expireRequests)
|
|
192
|
+
: expireRequests;
|
|
193
|
+
|
|
194
|
+
return processExpired();
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
/** 대기 중인 요청 목록 */
|
|
198
|
+
getPendingRequests() {
|
|
199
|
+
return store.getPendingHumanRequests();
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|