Merge branch 'main' of https://git.eroomai.com/jhogyu/Liti-agent-Development
# Conflicts: # .serena/project.yml # Case_02_Comparison_Research/.serena/project.yml
This commit is contained in:
@@ -0,0 +1 @@
|
||||
GITEA_TOKEN=
|
||||
@@ -0,0 +1,131 @@
|
||||
# 지정한 Agent YAML 을 AgentBackend API 로 실행하는 Gitea Actions 워크플로우.
|
||||
#
|
||||
# 사용법: Gitea 저장소 → Actions 탭 → "Run Agent YAML via API" → Run workflow
|
||||
# yaml_path 에 repo 상대경로 입력, 예:
|
||||
# Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.5/Stage_1_Part_4.yml
|
||||
#
|
||||
# 요구사항:
|
||||
# - 저장소 설정에서 Actions 활성화 + act_runner 등록 (runs-on: ubuntu-latest 라벨)
|
||||
# - 러너가 백엔드에 내부 주소로 접근 가능해야 함 (아래 api_base 참고)
|
||||
#
|
||||
# 인증 (중요):
|
||||
# 공개 URL(https://legalpoc.eroomai.com)은 Google OAuth 프록시(auth.eroomai.com)
|
||||
# 뒤에 있어서 CI 에서는 302 로 로그인 페이지로 튕긴다. localhost:8800 도 job 이
|
||||
# 컨테이너 안에서 돌면 컨테이너 자신을 가리켜 못 닿는다.
|
||||
# → 해결: act_runner 의 job 컨테이너를 AgentBackend 와 같은 도커 네트워크에 붙이고
|
||||
# 내부 컨테이너 주소로 직접 호출한다(프록시 우회). 내부 백엔드는 /api 접두사 없이
|
||||
# 루트로 API 를 제공한다(/upload-agent, /sse/agent/...).
|
||||
# 러너 설정 (서버, /opt/gitea-runner-scale/):
|
||||
# config.yaml 의 container.network 를 백엔드 네트워크 이름으로 지정
|
||||
# (docker network ls 로 확인, 보통 agentbackend_agent-network).
|
||||
# 그러면 api_base = http://agent-backend:8000 (container_name, 내부 포트 8000).
|
||||
#
|
||||
# 실행 흐름 (SKILL.md §0.6 경로 A):
|
||||
# workspaces/lookup(이름→UUID) → upload-agent 등록 → SSE start →
|
||||
# stage_complete 자동 confirm → execution_complete
|
||||
# stage_error 발생 시 세션을 cancel 하고 실패 처리한다.
|
||||
# 전체 이벤트 로그 / final_output / summary 는 아티팩트로 저장된다.
|
||||
# ※ workspace_name(작업실 이름) 또는 workspace_id(UUID) 중 하나는 반드시 지정.
|
||||
#
|
||||
# 실행 시간 상한 (주의):
|
||||
# Gitea 의 [actions] ENDLESS_TASK_TIMEOUT 과 act_runner 의 runner.timeout 기본값이
|
||||
# 각각 3h 라서, timeout-minutes 를 아무리 크게 줘도 3h 에서 강제 종료된다.
|
||||
# 강제 종료되면 if: always() 아티팩트 업로드도 건너뛰므로,
|
||||
# 스크립트의 --max-runtime(기본 9000s=150m) 이 먼저 세션을 정리하고 종료하도록
|
||||
# max_runtime < timeout-minutes(175m) < 3h 순서를 유지한다.
|
||||
# 3h 이상 돌려야 하면: app.ini 의 ENDLESS_TASK_TIMEOUT, act_runner config 의
|
||||
# runner.timeout, 아래 timeout-minutes, max_runtime 입력을 모두 함께 올릴 것.
|
||||
|
||||
name: Run Agent YAML via API
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
yaml_path:
|
||||
description: '실행할 Agent YAML 경로 (repo 상대경로)'
|
||||
required: true
|
||||
type: string
|
||||
user_id:
|
||||
description: 'AgentBackend user_id'
|
||||
required: false
|
||||
default: 'jsahn'
|
||||
type: string
|
||||
workspace_name:
|
||||
description: '작업실 이름 (예: 팬아웃 테스트). GET /workspaces/lookup 으로 UUID 자동 해석'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
workspace_id:
|
||||
description: '작업실 UUID 직접 지정(override). 비우면 workspace_name 으로 해석'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
user_input:
|
||||
description: 'Agent 에 전달할 user_input'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
start_stage_index:
|
||||
description: '시작 stage 인덱스 (0-based)'
|
||||
required: false
|
||||
default: '0'
|
||||
type: string
|
||||
api_base:
|
||||
description: 'AgentBackend API base URL (내부 컨테이너 주소, /api 접두사 없음)'
|
||||
required: false
|
||||
default: 'http://agent-backend:8000'
|
||||
type: string
|
||||
max_runtime_seconds:
|
||||
description: '총 실행 시간 상한(초). Gitea/act_runner 3h 상한보다 짧게'
|
||||
required: false
|
||||
default: '9000'
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
run-agent:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 175
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install dependencies
|
||||
run: python -m pip install --quiet httpx httpx-sse
|
||||
|
||||
- name: Run agent via API
|
||||
env:
|
||||
YAML_PATH: ${{ inputs.yaml_path }}
|
||||
API_BASE: ${{ inputs.api_base }}
|
||||
AGENT_USER_ID: ${{ inputs.user_id }}
|
||||
AGENT_WORKSPACE_NAME: ${{ inputs.workspace_name }}
|
||||
AGENT_WORKSPACE_ID: ${{ inputs.workspace_id }}
|
||||
AGENT_USER_INPUT: ${{ inputs.user_input }}
|
||||
START_STAGE_INDEX: ${{ inputs.start_stage_index }}
|
||||
MAX_RUNTIME_SECONDS: ${{ inputs.max_runtime_seconds }}
|
||||
# '=' 형식 필수: 값이 '-' 로 시작하는 자유 텍스트여도 argparse 가 값으로 인식
|
||||
run: |
|
||||
python scripts/run_agent_api.py \
|
||||
--yaml-path="$YAML_PATH" \
|
||||
--api-base="$API_BASE" \
|
||||
--user-id="$AGENT_USER_ID" \
|
||||
--workspace-name="$AGENT_WORKSPACE_NAME" \
|
||||
--workspace-id="$AGENT_WORKSPACE_ID" \
|
||||
--user-input="$AGENT_USER_INPUT" \
|
||||
--start-stage-index="$START_STAGE_INDEX" \
|
||||
--max-runtime="$MAX_RUNTIME_SECONDS" \
|
||||
--output-dir=agent_run_output
|
||||
|
||||
# Gitea Actions 는 아티팩트 v3 프로토콜만 지원한다. upload-artifact@v4 는
|
||||
# @actions/artifact v2 API 를 써서 Gitea(GHES 로 식별됨)에서 거부되므로 v3 사용.
|
||||
- name: Upload run artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: agent-run-${{ github.run_number }}
|
||||
path: agent_run_output/
|
||||
if-no-files-found: warn
|
||||
+11
@@ -8,3 +8,14 @@ wheels/
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
|
||||
# Serena
|
||||
.serena/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Secrets / local env (never commit tokens)
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
# the name by which the project can be referenced within Serena/when chatting with the LLM.
|
||||
project_name: "prompt_updates_sequential"
|
||||
|
||||
# list of languages for which language servers are started (LSP backend only); choose from:
|
||||
# ada al angular ansible bash
|
||||
# bsl clojure cpp cpp_ccls crystal
|
||||
# csharp csharp_omnisharp cue dart elixir
|
||||
# elm erlang fortran fsharp gdscript
|
||||
# go groovy haskell haxe hlsl
|
||||
# html java json julia kotlin
|
||||
# latex lean4 lua luau markdown
|
||||
# matlab msl nix ocaml pascal
|
||||
# perl php php_phpactor php_phpantom powershell
|
||||
# python python_jedi python_pyrefly python_ty r
|
||||
# rego ruby ruby_solargraph rust scala
|
||||
# scss solidity svelte swift systemverilog
|
||||
# terraform toml typescript typescript_vts vue
|
||||
# yaml zig
|
||||
# (This list may be outdated; generated with scripts/print_language_list.py;
|
||||
# For the current list, see values of Language enum here:
|
||||
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py)
|
||||
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
|
||||
# Note:
|
||||
# - For C, use cpp
|
||||
# - For JavaScript, use typescript
|
||||
# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root)
|
||||
# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm)
|
||||
# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three)
|
||||
# - For Free Pascal/Lazarus, use pascal
|
||||
# Special requirements:
|
||||
# Some languages require additional setup/installations.
|
||||
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
|
||||
# When using multiple languages, the first language server that supports a given file will be used for that file.
|
||||
# The first language is the default language and the respective language server will be used as a fallback.
|
||||
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
||||
languages:
|
||||
- python
|
||||
|
||||
# the encoding used by text files in the project
|
||||
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||
encoding: "utf-8"
|
||||
|
||||
# line ending convention to use when writing source files.
|
||||
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
|
||||
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
|
||||
line_ending:
|
||||
|
||||
# The language backend to use for this project.
|
||||
# If not set, the global setting from serena_config.yml is used.
|
||||
# Valid values: LSP, JetBrains
|
||||
# Note: the backend is fixed at startup. If a project with a different backend
|
||||
# is activated post-init, an error will be returned.
|
||||
language_backend:
|
||||
|
||||
# whether to use project's .gitignore files to ignore files
|
||||
ignore_all_files_in_gitignore: true
|
||||
|
||||
# advanced configuration option allowing to configure language server-specific options.
|
||||
# Maps the language key to the options.
|
||||
# The settings are considered only if the project is trusted (see global configuration to define trusted projects).
|
||||
# See https://oraios.github.io/serena/02-usage/050_configuration.html#language-server-specific-settings
|
||||
ls_specific_settings: {}
|
||||
|
||||
# list of additional paths to ignore in this project.
|
||||
# Same syntax as gitignore, so you can use * and **.
|
||||
# Note: global ignored_paths from serena_config.yml are also applied additively.
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||
# Added on 2025-04-18
|
||||
read_only: false
|
||||
|
||||
# list of tool names to exclude.
|
||||
# This extends the existing exclusions (e.g. from the global configuration)
|
||||
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
|
||||
excluded_tools: []
|
||||
|
||||
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default).
|
||||
# This extends the existing inclusions (e.g. from the global configuration).
|
||||
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
|
||||
included_optional_tools: []
|
||||
|
||||
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
|
||||
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
|
||||
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
|
||||
fixed_tools: []
|
||||
|
||||
# list of mode names to that are always to be included in the set of active modes
|
||||
# The full set of modes to be activated is base_modes + default_modes.
|
||||
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
|
||||
# Otherwise, this setting overrides the global configuration.
|
||||
# Set this to [] to disable base modes for this project.
|
||||
# Set this to a list of mode names to always include the respective modes for this project.
|
||||
base_modes:
|
||||
|
||||
# list of mode names that are to be activated by default, overriding the setting in the global configuration.
|
||||
# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
|
||||
# If the setting is undefined/empty, the default_modes from the global configuration (serena_config.yml) apply.
|
||||
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
|
||||
# Therefore, you can set this to [] if you do not want the default modes defined in the global config to apply
|
||||
# for this project.
|
||||
# This setting can, in turn, be overridden by CLI parameters (--mode).
|
||||
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
|
||||
default_modes:
|
||||
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: ""
|
||||
|
||||
# time budget (seconds) per tool call for the retrieval of additional symbol information
|
||||
# such as docstrings or parameter information.
|
||||
# This overrides the corresponding setting in the global configuration; see the documentation there.
|
||||
# If null or missing, use the setting from the global configuration.
|
||||
symbol_info_budget:
|
||||
|
||||
# list of regex patterns which, when matched, mark a memory entry as read‑only.
|
||||
# Extends the list from the global configuration, merging the two lists.
|
||||
read_only_memory_patterns: []
|
||||
|
||||
# list of regex patterns for memories to completely ignore.
|
||||
# Matching memories will not appear in list_memories or activate_project output
|
||||
# and cannot be accessed via read_memory or write_memory.
|
||||
# To access ignored memory files, use the read_file tool on the raw file path.
|
||||
# Extends the list from the global configuration, merging the two lists.
|
||||
# Example: ["_archive/.*", "_episodes/.*"]
|
||||
ignored_memory_patterns: []
|
||||
|
||||
# list of mode names to be activated additionally for this project, e.g. ["query-projects"]
|
||||
# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
|
||||
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
|
||||
added_modes:
|
||||
|
||||
# list of additional workspace folder paths for cross-package reference support.
|
||||
# Paths can be absolute or relative to the project root.
|
||||
# Each folder is registered as an LSP workspace folder, enabling language servers to discover
|
||||
# symbols and references across package boundaries, but these folders are not indexed by Serena,
|
||||
# i.e. the respective symbols will not be found using Serena's symbol search tools.
|
||||
# Example:
|
||||
# additional_workspace_folders:
|
||||
# - ../sibling-package
|
||||
# - ../shared-lib
|
||||
ls_additional_workspace_folders: []
|
||||
|
||||
# list of workspace folder paths (LSP backend only).
|
||||
# These folders will be used to build up Serena's symbol index.
|
||||
# Paths must be within the project root and should thus be relative to the project root.
|
||||
# Furthermore, the paths should not be filtered by ignore settings.
|
||||
# Default setting: The entire project root folder (".") is considered.
|
||||
# In (large) monorepos, this can be used to index only subfolders of the project root, e.g.
|
||||
# ls_workspace_folders:
|
||||
# - "./subproject1"
|
||||
# - "./subproject2"
|
||||
ls_workspace_folders:
|
||||
- .
|
||||
|
||||
# optional shell command to run before the language backend (LSP or JetBrains) is initialised.
|
||||
# the command runs in the project root directory and is only executed if the project is trusted
|
||||
# (see trusted_project_path_patterns in the global configuration).
|
||||
# serena waits for the command to exit: a non-zero exit code is logged as an error but does not
|
||||
# abort activation. a per-project timeout (activation_command_timeout, default 180s) is the safety
|
||||
# backstop for non-terminating commands; on expiry the process is killed and activation continues.
|
||||
# example: activation_command: "npx nx run-many -t build"
|
||||
activation_command:
|
||||
|
||||
# maximum time in seconds to wait for activation_command to complete before killing it (default 180s).
|
||||
# must be a positive number.
|
||||
activation_command_timeout: 180.0
|
||||
@@ -1,167 +0,0 @@
|
||||
# the name by which the project can be referenced within Serena/when chatting with the LLM.
|
||||
project_name: "Case_02_Comparison_Research"
|
||||
|
||||
# list of languages for which language servers are started (LSP backend only); choose from:
|
||||
# ada al angular ansible bash
|
||||
# bsl clojure cpp cpp_ccls crystal
|
||||
# csharp csharp_omnisharp cue dart elixir
|
||||
# elm erlang fortran fsharp gdscript
|
||||
# go groovy haskell haxe hlsl
|
||||
# html java json julia kotlin
|
||||
# latex lean4 lua luau markdown
|
||||
# matlab msl nix ocaml pascal
|
||||
# perl php php_phpactor php_phpantom powershell
|
||||
# python python_jedi python_pyrefly python_ty r
|
||||
# rego ruby ruby_solargraph rust scala
|
||||
# scss solidity svelte swift systemverilog
|
||||
# terraform toml typescript typescript_vts vue
|
||||
# yaml zig
|
||||
# (This list may be outdated; generated with scripts/print_language_list.py;
|
||||
# For the current list, see values of Language enum here:
|
||||
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py)
|
||||
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
|
||||
# Note:
|
||||
# - For C, use cpp
|
||||
# - For JavaScript, use typescript
|
||||
# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root)
|
||||
# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm)
|
||||
# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three)
|
||||
# - For Free Pascal/Lazarus, use pascal
|
||||
# Special requirements:
|
||||
# Some languages require additional setup/installations.
|
||||
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
|
||||
# When using multiple languages, the first language server that supports a given file will be used for that file.
|
||||
# The first language is the default language and the respective language server will be used as a fallback.
|
||||
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
||||
languages: []
|
||||
|
||||
# the encoding used by text files in the project
|
||||
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||
encoding: "utf-8"
|
||||
|
||||
# line ending convention to use when writing source files.
|
||||
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
|
||||
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
|
||||
line_ending:
|
||||
|
||||
# The language backend to use for this project.
|
||||
# If not set, the global setting from serena_config.yml is used.
|
||||
# Valid values: LSP, JetBrains
|
||||
# Note: the backend is fixed at startup. If a project with a different backend
|
||||
# is activated post-init, an error will be returned.
|
||||
language_backend:
|
||||
|
||||
# whether to use project's .gitignore files to ignore files
|
||||
ignore_all_files_in_gitignore: true
|
||||
|
||||
# advanced configuration option allowing to configure language server-specific options.
|
||||
# Maps the language key to the options.
|
||||
# The settings are considered only if the project is trusted (see global configuration to define trusted projects).
|
||||
# See https://oraios.github.io/serena/02-usage/050_configuration.html#language-server-specific-settings
|
||||
ls_specific_settings: {}
|
||||
|
||||
# list of additional paths to ignore in this project.
|
||||
# Same syntax as gitignore, so you can use * and **.
|
||||
# Note: global ignored_paths from serena_config.yml are also applied additively.
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||
# Added on 2025-04-18
|
||||
read_only: false
|
||||
|
||||
# list of tool names to exclude.
|
||||
# This extends the existing exclusions (e.g. from the global configuration)
|
||||
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
|
||||
excluded_tools: []
|
||||
|
||||
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default).
|
||||
# This extends the existing inclusions (e.g. from the global configuration).
|
||||
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
|
||||
included_optional_tools: []
|
||||
|
||||
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
|
||||
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
|
||||
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
|
||||
fixed_tools: []
|
||||
|
||||
# list of mode names to that are always to be included in the set of active modes
|
||||
# The full set of modes to be activated is base_modes + default_modes.
|
||||
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
|
||||
# Otherwise, this setting overrides the global configuration.
|
||||
# Set this to [] to disable base modes for this project.
|
||||
# Set this to a list of mode names to always include the respective modes for this project.
|
||||
base_modes:
|
||||
|
||||
# list of mode names that are to be activated by default, overriding the setting in the global configuration.
|
||||
# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
|
||||
# If the setting is undefined/empty, the default_modes from the global configuration (serena_config.yml) apply.
|
||||
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
|
||||
# Therefore, you can set this to [] if you do not want the default modes defined in the global config to apply
|
||||
# for this project.
|
||||
# This setting can, in turn, be overridden by CLI parameters (--mode).
|
||||
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
|
||||
default_modes:
|
||||
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: ""
|
||||
|
||||
# time budget (seconds) per tool call for the retrieval of additional symbol information
|
||||
# such as docstrings or parameter information.
|
||||
# This overrides the corresponding setting in the global configuration; see the documentation there.
|
||||
# If null or missing, use the setting from the global configuration.
|
||||
symbol_info_budget:
|
||||
|
||||
# list of regex patterns which, when matched, mark a memory entry as read‑only.
|
||||
# Extends the list from the global configuration, merging the two lists.
|
||||
read_only_memory_patterns: []
|
||||
|
||||
# list of regex patterns for memories to completely ignore.
|
||||
# Matching memories will not appear in list_memories or activate_project output
|
||||
# and cannot be accessed via read_memory or write_memory.
|
||||
# To access ignored memory files, use the read_file tool on the raw file path.
|
||||
# Extends the list from the global configuration, merging the two lists.
|
||||
# Example: ["_archive/.*", "_episodes/.*"]
|
||||
ignored_memory_patterns: []
|
||||
|
||||
# list of mode names to be activated additionally for this project, e.g. ["query-projects"]
|
||||
# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
|
||||
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
|
||||
added_modes:
|
||||
|
||||
# optional shell command to run before the language backend (LSP or JetBrains) is initialised.
|
||||
# the command runs in the project root directory and is only executed if the project is trusted
|
||||
# (see trusted_project_path_patterns in the global configuration).
|
||||
# serena waits for the command to exit: a non-zero exit code is logged as an error but does not
|
||||
# abort activation. a per-project timeout (activation_command_timeout, default 180s) is the safety
|
||||
# backstop for non-terminating commands; on expiry the process is killed and activation continues.
|
||||
# example: activation_command: "npx nx run-many -t build"
|
||||
activation_command:
|
||||
|
||||
# maximum time in seconds to wait for activation_command to complete before killing it (default 180s).
|
||||
# must be a positive number.
|
||||
activation_command_timeout: 180.0
|
||||
|
||||
# list of additional workspace folder paths for cross-package reference support.
|
||||
# Paths can be absolute or relative to the project root.
|
||||
# Each folder is registered as an LSP workspace folder, enabling language servers to discover
|
||||
# symbols and references across package boundaries, but these folders are not indexed by Serena,
|
||||
# i.e. the respective symbols will not be found using Serena's symbol search tools.
|
||||
# Example:
|
||||
# additional_workspace_folders:
|
||||
# - ../sibling-package
|
||||
# - ../shared-lib
|
||||
ls_additional_workspace_folders: []
|
||||
|
||||
# list of workspace folder paths (LSP backend only).
|
||||
# These folders will be used to build up Serena's symbol index.
|
||||
# Paths must be within the project root and should thus be relative to the project root.
|
||||
# Furthermore, the paths should not be filtered by ignore settings.
|
||||
# Default setting: The entire project root folder (".") is considered.
|
||||
# In (large) monorepos, this can be used to index only subfolders of the project root, e.g.
|
||||
# ls_workspace_folders:
|
||||
# - "./subproject1"
|
||||
# - "./subproject2"
|
||||
ls_workspace_folders:
|
||||
- .
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,144 @@
|
||||
# Agent YAML 실행 워크플로우 — 테스트 방법
|
||||
|
||||
지정한 Agent YAML 을 AgentBackend API 로 실행하는 Gitea Actions 워크플로우
|
||||
([.gitea/workflows/run-agent.yml](.gitea/workflows/run-agent.yml) +
|
||||
[scripts/run_agent_api.py](scripts/run_agent_api.py)) 를 돌리는 방법 정리.
|
||||
|
||||
실행 흐름 (SKILL.md §0.6 경로 A):
|
||||
`workspaces/lookup`(이름→UUID) → `upload-agent` 등록 → SSE `start` →
|
||||
`stage_complete` 자동 confirm → `execution_complete`.
|
||||
전체 이벤트/`final_output`/`summary` 는 아티팩트로 저장된다.
|
||||
|
||||
---
|
||||
|
||||
## 0. `.env` 설정 (토큰)
|
||||
|
||||
> ⚠️ **`.env` 는 절대 커밋하지 말 것.** `.gitignore` 에 등록되어 있다
|
||||
> (`git check-ignore .env` 로 확인). 토큰 값은 이 문서에도 적지 않는다.
|
||||
|
||||
프로젝트 루트에 `.env` 파일을 만들고 아래 형식으로 채운다 (값은 실제 토큰):
|
||||
|
||||
```dotenv
|
||||
GITEA_TOKEN=<Gitea Personal Access Token>
|
||||
```
|
||||
|
||||
- 토큰 발급: Gitea → Settings → Applications → Generate New Token
|
||||
- 필요한 스코프: **`write:repository`**
|
||||
- 사용 후에는 **폐기(revoke)** 권장. 만료를 짧게 설정할 것.
|
||||
- 템플릿: [.env.example](.env.example) 참고 (빈 값, 커밋되어도 안전).
|
||||
|
||||
---
|
||||
|
||||
## 방법 A — Gitea API 로 트리거 (설계 의도: 러너가 실행)
|
||||
|
||||
`.env` 의 토큰을 읽어 `workflow_dispatch` 를 호출한다. **토큰을 화면에 출력하지 말 것.**
|
||||
|
||||
```bash
|
||||
cd /Users/jsahn/Works/Liti-agent-Development
|
||||
set -a; . ./.env; set +a # .env 로드 ($GITEA_TOKEN)
|
||||
|
||||
curl -s -X POST \
|
||||
"https://git.eroomai.com/api/v1/repos/jhogyu/Liti-agent-Development/actions/workflows/run-agent.yml/dispatches" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"ref": "main",
|
||||
"inputs": {
|
||||
"yaml_path": "Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.5/Stage_1_Part_1.yml",
|
||||
"workspace_name": "팬아웃 테스트",
|
||||
"user_id": "jsahn"
|
||||
}
|
||||
}'
|
||||
# 성공 시 HTTP 204 (본문 없음)
|
||||
```
|
||||
|
||||
### 실행 상태 폴링
|
||||
|
||||
```bash
|
||||
# 최근 run 목록
|
||||
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||
"https://git.eroomai.com/api/v1/repos/jhogyu/Liti-agent-Development/actions/tasks?limit=5" \
|
||||
| python3 -m json.tool
|
||||
```
|
||||
|
||||
또는 웹 UI: `https://git.eroomai.com/jhogyu/Liti-agent-Development/actions` 의
|
||||
run 상세 페이지에서 로그 확인. 완료 후 하단 **Artifacts** 에서
|
||||
`agent-run-<번호>` (events.jsonl / final_output.txt / summary.md) 다운로드.
|
||||
|
||||
---
|
||||
|
||||
## 방법 B — Gitea 웹 UI 에서 실행
|
||||
|
||||
1. Actions 탭 → **Run Agent YAML via API** 선택
|
||||
2. **Run workflow** 클릭 (⚠️ 실패한 run 의 **Re-run 은 금지** — 옛 커밋의 워크플로우가 실행됨)
|
||||
3. 입력 채우기:
|
||||
- `yaml_path`: 실행할 YAML 의 repo 상대경로
|
||||
- `workspace_name`: 작업실 이름 (예: `팬아웃 테스트`) — UUID 자동 해석
|
||||
- (또는 `workspace_id` 에 UUID 직접 입력 — override)
|
||||
4. 실행 → run 페이지에서 로그/아티팩트 확인
|
||||
|
||||
---
|
||||
|
||||
## 방법 C — 로컬에서 스크립트 직접 실행 (CI 안 거침)
|
||||
|
||||
backend 에 네트워크로 직접 붙어 실행. `httpx`, `httpx-sse` 필요.
|
||||
|
||||
```bash
|
||||
pip install httpx httpx-sse
|
||||
python scripts/run_agent_api.py \
|
||||
--yaml-path="Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.5/Stage_1_Part_1.yml" \
|
||||
--workspace-name="팬아웃 테스트" \
|
||||
--user-id="jsahn" \
|
||||
--api-base="http://100.93.221.71:8800" \
|
||||
--output-dir=agent_run_output
|
||||
```
|
||||
|
||||
- 러너에서 돌 때의 `api_base` 는 `http://agent-backend:8000` (컨테이너 네트워크).
|
||||
- 이 머신(Tailscale) 에서 직접 검증할 때는 `http://100.93.221.71:8800`
|
||||
(= `eroomaiserver.tailabbfd5.ts.net`, 실제 데이터가 있는 backend).
|
||||
|
||||
---
|
||||
|
||||
## 워크플로우 입력 레퍼런스
|
||||
|
||||
| 입력 | 필수 | 기본값 | 설명 |
|
||||
|------|------|--------|------|
|
||||
| `yaml_path` | ✅ | — | 실행할 Agent YAML 의 repo 상대경로 |
|
||||
| `workspace_name` | △ | `""` | 작업실 이름 → `GET /workspaces/lookup` 으로 UUID 해석 |
|
||||
| `workspace_id` | △ | `""` | 작업실 UUID 직접 지정(override). 비우면 name 으로 해석 |
|
||||
| `user_id` | | `jsahn` | AgentBackend user_id |
|
||||
| `user_input` | | `""` | Agent 에 전달할 user_input |
|
||||
| `start_stage_index` | | `0` | 시작 stage 인덱스 |
|
||||
| `api_base` | | `http://agent-backend:8000` | 내부 컨테이너 주소 (/api 접두사 없음) |
|
||||
| `max_runtime_seconds` | | `9000` | 총 실행 상한(초). 3h infra 상한보다 짧게 |
|
||||
|
||||
> `workspace_name` 또는 `workspace_id` **중 하나는 반드시** 지정 (둘 다 비우면 실패).
|
||||
|
||||
작업실 이름↔UUID 목록 확인:
|
||||
```bash
|
||||
curl -s "http://100.93.221.71:8800/workspaces/lookup?user_id=jsahn" | python3 -m json.tool
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 전제 조건 (서버 인프라)
|
||||
|
||||
| 항목 | 설정 | 이유 |
|
||||
|------|------|------|
|
||||
| act_runner 네트워크 | `config.yaml` 의 `container.network` = 백엔드 도커 네트워크 | job 컨테이너가 `agent-backend:8000` 을 DNS 로 찾으려면 같은 네트워크여야 함 |
|
||||
| 동시 실행 | `config.yaml` 의 `runner.capacity` 상향 (예: 20) | agent job 은 SSE 대기(I/O 바운드)라 슬롯을 오래 점유 → capacity 낮으면 큐 적체 |
|
||||
| 아티팩트 | `actions/upload-artifact@v3` (v4 아님) | Gitea 는 아티팩트 v3 프로토콜만 지원 (v4 는 GHESNotSupportedError) |
|
||||
| 실행 시간 | `max_runtime`(150m) < `timeout-minutes`(175m) < 3h | Gitea/act_runner 3h 강제종료 전에 스스로 정리 |
|
||||
|
||||
---
|
||||
|
||||
## 트러블슈팅 (겪었던 이슈)
|
||||
|
||||
| 증상 | 원인 | 해결 |
|
||||
|------|------|------|
|
||||
| `HTTP 302` → Google OAuth 로 리다이렉트 | 공개 URL(legalpoc.eroomai.com)이 OAuth 프록시 뒤 | `api_base` 를 내부 주소로 |
|
||||
| 고쳤는데도 계속 302 | 실패 run 을 **Re-run** (옛 커밋 실행) | **Run workflow** 로 새로 실행 |
|
||||
| `Name or service not known` | job 컨테이너가 백엔드 네트워크 밖 | act_runner `container.network` 설정 |
|
||||
| job 이 pending 에서 대기 | `capacity=1` 로 슬롯 부족 | `runner.capacity` 상향 |
|
||||
| `GHESNotSupportedError` (아티팩트) | Gitea 는 v4 미지원 | `upload-artifact@v3` |
|
||||
| `/workspaces` 빈 배열 | `eroom-server`(다른 머신, 빈 DB) 로 요청 | `eroomaiserver`(100.93.221.71) 로, 경로는 `/workspaces/lookup` |
|
||||
@@ -0,0 +1,479 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Upload an Agent YAML to AgentBackend and execute it via SSE.
|
||||
|
||||
Flow (SKILL.md §0.6 — 경로 A):
|
||||
0. GET {api_base}/workspaces/lookup?user_id=... (workspace_name → UUID 해석)
|
||||
1. POST {api_base}/upload-agent?user_id=... (multipart YAML 등록)
|
||||
2. POST {api_base}/sse/agent/{name}/start (SSE 실행)
|
||||
3. stage_complete 이벤트 수신 시 자동 confirm (CI 무인 실행, 재시도 포함)
|
||||
4. 연결 끊김 시 /sse/agent/{name}/reconnect/{sid} (재연결)
|
||||
5. stage_error / --max-runtime 초과 / SIGTERM 시 세션 cancel 후 종료
|
||||
|
||||
Exit code: 0 = execution_complete / 1 = 그 외 (stage_error, stopped, timeout, ...)
|
||||
|
||||
--output-dir 에 저장되는 파일:
|
||||
events.jsonl — 수신한 모든 SSE 이벤트 (한 줄당 1개)
|
||||
final_output.txt — execution_complete 이벤트의 final_outputs (stage 별 출력 dict)
|
||||
summary.md — 실행 요약 (GITHUB_STEP_SUMMARY 에도 기록)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
import unicodedata
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
|
||||
import httpx
|
||||
from httpx_sse import SSEError, connect_sse
|
||||
|
||||
PRINT_TRUNCATE = 500
|
||||
|
||||
|
||||
class RunTimeout(Exception):
|
||||
"""--max-runtime 초과."""
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return datetime.now(timezone.utc).strftime("%H:%M:%S")
|
||||
|
||||
|
||||
def log(msg: str) -> None:
|
||||
print(f"[{_now()}] {msg}", flush=True)
|
||||
|
||||
|
||||
def truncate(value: object, limit: int = PRINT_TRUNCATE) -> str:
|
||||
text = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False)
|
||||
return text if len(text) <= limit else text[:limit] + f"... (+{len(text) - limit} chars)"
|
||||
|
||||
|
||||
class AgentRunner:
|
||||
def __init__(self, args: argparse.Namespace) -> None:
|
||||
self.api_base = args.api_base.rstrip("/")
|
||||
self.yaml_path = Path(args.yaml_path)
|
||||
self.user_id = args.user_id
|
||||
self.workspace_id = args.workspace_id
|
||||
self.workspace_name = args.workspace_name
|
||||
self.user_input = args.user_input
|
||||
self.start_stage_index = args.start_stage_index
|
||||
self.read_timeout = args.read_timeout
|
||||
self.max_reconnects = args.max_reconnects
|
||||
self.max_runtime = args.max_runtime
|
||||
self.output_dir = Path(args.output_dir)
|
||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self.agent_name: str = ""
|
||||
self.session_id: str = ""
|
||||
self.final_status: str = "unknown"
|
||||
self.final_error: str = ""
|
||||
self.event_counts: dict[str, int] = {}
|
||||
self.stages_confirmed = 0
|
||||
self.started_at = time.monotonic()
|
||||
self.deadline = self.started_at + self.max_runtime
|
||||
|
||||
self.sse_client = httpx.Client(timeout=httpx.Timeout(30.0))
|
||||
# action/cancel 등 단발 요청은 SSE 스트림과 분리된 클라이언트로 보낸다
|
||||
self.api_client = httpx.Client(timeout=60.0)
|
||||
|
||||
self.events_file = (self.output_dir / "events.jsonl").open("a", encoding="utf-8")
|
||||
|
||||
def _remaining(self) -> float:
|
||||
return self.deadline - time.monotonic()
|
||||
|
||||
# ------------------------------------------------------------- workspace
|
||||
|
||||
def resolve_workspace(self) -> None:
|
||||
"""workspace_id 를 확정한다.
|
||||
|
||||
- workspace_id 가 주어지면(override) 그대로 사용.
|
||||
- 아니면 workspace_name 을 GET /workspaces/lookup 으로 UUID 해석.
|
||||
둘 다 없으면 SystemExit.
|
||||
"""
|
||||
if self.workspace_id:
|
||||
log(f"Using explicit workspace_id: {self.workspace_id}")
|
||||
return
|
||||
if not self.workspace_name:
|
||||
raise SystemExit("ERROR: workspace_name 또는 workspace_id 중 하나를 지정해야 합니다.")
|
||||
|
||||
log(f"Resolving workspace by name: {self.workspace_name!r} (user_id={self.user_id})")
|
||||
try:
|
||||
resp = self.api_client.get(
|
||||
f"{self.api_base}/workspaces/lookup",
|
||||
params={"user_id": self.user_id},
|
||||
)
|
||||
except httpx.HTTPError as exc:
|
||||
raise SystemExit(f"ERROR: workspaces/lookup 요청 실패: {exc}")
|
||||
if resp.status_code != 200:
|
||||
raise SystemExit(f"ERROR: workspaces/lookup HTTP {resp.status_code}: {resp.text[:500]}")
|
||||
try:
|
||||
items = resp.json().get("workspaces", [])
|
||||
except json.JSONDecodeError:
|
||||
raise SystemExit(f"ERROR: workspaces/lookup 응답이 JSON 이 아님: {resp.text[:300]}")
|
||||
|
||||
def norm(s: object) -> str:
|
||||
return unicodedata.normalize("NFC", (s if isinstance(s, str) else "").strip())
|
||||
|
||||
def field(w: dict, *keys: str) -> str:
|
||||
for k in keys:
|
||||
v = w.get(k)
|
||||
if v:
|
||||
return v
|
||||
return ""
|
||||
|
||||
target = norm(self.workspace_name)
|
||||
names, exact, ci = [], [], []
|
||||
for w in items:
|
||||
name = field(w, "name", "workspace_name", "title")
|
||||
wid = field(w, "workspace_id", "id")
|
||||
names.append(name)
|
||||
if not wid:
|
||||
continue
|
||||
if norm(name) == target:
|
||||
exact.append(wid)
|
||||
elif norm(name).lower() == target.lower():
|
||||
ci.append(wid)
|
||||
|
||||
chosen = exact if exact else ci
|
||||
if len(chosen) == 1:
|
||||
self.workspace_id = chosen[0]
|
||||
log(f"Resolved workspace {self.workspace_name!r} -> {self.workspace_id}")
|
||||
elif len(chosen) > 1:
|
||||
raise SystemExit(
|
||||
f"ERROR: 워크스페이스 이름 {self.workspace_name!r} 이 여러 개 매칭됩니다: {chosen}"
|
||||
)
|
||||
else:
|
||||
available = ", ".join(repr(n) for n in names) or "(없음)"
|
||||
raise SystemExit(
|
||||
f"ERROR: {self.workspace_name!r} 에 해당하는 워크스페이스를 찾지 못했습니다. "
|
||||
f"사용 가능: {available}"
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ upload
|
||||
|
||||
def upload(self) -> None:
|
||||
if not self.yaml_path.is_file():
|
||||
raise SystemExit(f"ERROR: YAML not found: {self.yaml_path}")
|
||||
|
||||
log(f"Uploading agent YAML: {self.yaml_path}")
|
||||
with self.yaml_path.open("rb") as f:
|
||||
resp = self.api_client.post(
|
||||
f"{self.api_base}/upload-agent",
|
||||
params={"user_id": self.user_id},
|
||||
files={"file": (self.yaml_path.name, f, "application/x-yaml")},
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
raise SystemExit(f"ERROR: upload-agent failed HTTP {resp.status_code}: {resp.text[:1000]}")
|
||||
|
||||
data = resp.json()
|
||||
if not data.get("success") or not data.get("agent_name"):
|
||||
raise SystemExit(f"ERROR: upload-agent rejected: {json.dumps(data, ensure_ascii=False)[:1000]}")
|
||||
|
||||
self.agent_name = data["agent_name"]
|
||||
log(f"Agent registered: {self.agent_name} (stages={data.get('stages')})")
|
||||
|
||||
# ------------------------------------------------------------------ actions
|
||||
|
||||
def cancel_session(self) -> None:
|
||||
if not (self.agent_name and self.session_id):
|
||||
return
|
||||
try:
|
||||
self.api_client.post(
|
||||
f"{self.api_base}/sse/agent/{quote(self.agent_name, safe='')}"
|
||||
f"/cancel/{self.session_id}"
|
||||
)
|
||||
log(f"Cancel requested for session {self.session_id}")
|
||||
except httpx.HTTPError as exc:
|
||||
log(f"WARNING: cancel request failed: {exc}")
|
||||
|
||||
def _send_action(self, action: str, attempts: int = 5) -> bool:
|
||||
"""Send a stage action; retry transient failures inside the server's 600s wait window."""
|
||||
url = (
|
||||
f"{self.api_base}/sse/agent/{quote(self.agent_name, safe='')}"
|
||||
f"/action/{self.session_id}"
|
||||
)
|
||||
for i in range(1, attempts + 1):
|
||||
try:
|
||||
resp = self.api_client.post(url, json={"action": action})
|
||||
if resp.status_code == 200:
|
||||
log(f" -> action '{action}' sent")
|
||||
return True
|
||||
if resp.status_code in (400, 404):
|
||||
# 세션 소멸/거부 — 재시도 무의미
|
||||
log(f" -> action '{action}' failed HTTP {resp.status_code}: {resp.text[:300]}")
|
||||
return False
|
||||
log(f" -> action '{action}' attempt {i}/{attempts} HTTP {resp.status_code}: {resp.text[:300]}")
|
||||
except httpx.HTTPError as exc:
|
||||
log(f" -> action '{action}' attempt {i}/{attempts} request error: {exc}")
|
||||
if i < attempts:
|
||||
time.sleep(min(2 ** i, 60))
|
||||
return False
|
||||
|
||||
# ------------------------------------------------------------------ events
|
||||
|
||||
def _record(self, event: dict) -> None:
|
||||
event["_received_at"] = datetime.now(timezone.utc).isoformat()
|
||||
self.events_file.write(json.dumps(event, ensure_ascii=False) + "\n")
|
||||
self.events_file.flush()
|
||||
etype = event.get("type", "unknown")
|
||||
self.event_counts[etype] = self.event_counts.get(etype, 0) + 1
|
||||
|
||||
def _handle(self, event: dict) -> bool:
|
||||
"""Returns True when execution reached a terminal state."""
|
||||
self._record(event)
|
||||
etype = event.get("type", "unknown")
|
||||
|
||||
if etype == "session_started":
|
||||
self.session_id = event.get("session_id", self.session_id)
|
||||
log(
|
||||
f"Session started: {self.session_id} "
|
||||
f"(total_stages={event.get('total_stages')}, start_index={event.get('start_stage_index')})"
|
||||
)
|
||||
elif etype == "stage_start":
|
||||
log(f"=== Stage start: index={event.get('stage_index')} {event.get('stage_name', '')}")
|
||||
elif etype == "task_start":
|
||||
log(f" task start: {event.get('task_name', '?')}")
|
||||
elif etype == "task_iteration":
|
||||
log(f" task iter : {event.get('task_name', '?')} #{event.get('iteration', '?')}")
|
||||
elif etype == "task_complete":
|
||||
log(f" task done : {event.get('task_name', '?')}")
|
||||
elif etype == "stage_complete":
|
||||
log(f"=== Stage complete: index={event.get('stage_index')} — auto-confirming")
|
||||
if self._send_action("confirm"):
|
||||
self.stages_confirmed += 1
|
||||
else:
|
||||
# confirm 미전달 상태로 방치하면 서버 600s action-timeout 까지 세션이 잠긴다
|
||||
self.final_status = "error"
|
||||
self.final_error = "confirm action could not be delivered — cancelling session"
|
||||
log(f"ERROR: {self.final_error}")
|
||||
self.cancel_session()
|
||||
return True
|
||||
elif etype == "stage_error":
|
||||
# 서버는 stage_error 후 클라이언트 개입 없이는 같은 stage 를 재실행하거나
|
||||
# 실패 stage 를 건너뛰므로, CI 에서는 즉시 세션을 취소하고 실패 처리한다.
|
||||
self.final_status = "stage_error"
|
||||
self.final_error = str(event.get("error", ""))
|
||||
log(f"=== Stage ERROR: index={event.get('stage_index')} — {truncate(self.final_error)}")
|
||||
self.cancel_session()
|
||||
return True
|
||||
elif etype == "execution_complete":
|
||||
self.final_status = "completed"
|
||||
# 현행 백엔드는 final_outputs(stage 별 dict), 구버전 문서는 final_output
|
||||
final_output = event.get("final_outputs") or event.get("final_output", "")
|
||||
if not final_output:
|
||||
log("WARNING: execution_complete has neither final_outputs nor final_output")
|
||||
out_path = self.output_dir / "final_output.txt"
|
||||
out_path.write_text(
|
||||
final_output if isinstance(final_output, str)
|
||||
else json.dumps(final_output, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
log(f"Execution complete. final_output saved to {out_path}")
|
||||
return True
|
||||
elif etype == "execution_stopped":
|
||||
self.final_status = "stopped"
|
||||
self.final_error = str(event.get("reason") or event.get("message") or "")
|
||||
log(f"Execution STOPPED: {self.final_error}")
|
||||
return True
|
||||
elif etype == "error":
|
||||
self.final_status = "error"
|
||||
self.final_error = str(event.get("error", ""))
|
||||
log(f"Execution ERROR: {truncate(self.final_error)}")
|
||||
return True
|
||||
else:
|
||||
log(f" event [{etype}]: {truncate({k: v for k, v in event.items() if k != 'type'})}")
|
||||
|
||||
return False
|
||||
|
||||
# ------------------------------------------------------------------ stream
|
||||
|
||||
def _stream(self, mode: str) -> bool:
|
||||
"""Open one SSE connection and consume events. Returns True on terminal event."""
|
||||
remaining = self._remaining()
|
||||
if remaining <= 0:
|
||||
raise RunTimeout()
|
||||
|
||||
if mode == "start":
|
||||
url = f"{self.api_base}/sse/agent/{quote(self.agent_name, safe='')}/start"
|
||||
body = {
|
||||
"user_id": self.user_id,
|
||||
"workspace_id": self.workspace_id,
|
||||
"user_input": self.user_input,
|
||||
"start_stage_index": self.start_stage_index,
|
||||
}
|
||||
else:
|
||||
url = (
|
||||
f"{self.api_base}/sse/agent/{quote(self.agent_name, safe='')}"
|
||||
f"/reconnect/{self.session_id}"
|
||||
)
|
||||
body = {}
|
||||
|
||||
# read timeout: 이벤트 간 무응답 상한이며 남은 실행 예산을 넘지 않게 잡는다
|
||||
timeout = httpx.Timeout(
|
||||
connect=30.0,
|
||||
read=min(self.read_timeout, max(remaining, 30.0)),
|
||||
write=30.0,
|
||||
pool=30.0,
|
||||
)
|
||||
|
||||
log(f"SSE {mode}: {url}")
|
||||
with connect_sse(self.sse_client, "POST", url, json=body, timeout=timeout) as event_source:
|
||||
for sse in event_source.iter_sse():
|
||||
if self._remaining() <= 0:
|
||||
raise RunTimeout()
|
||||
if not sse.data:
|
||||
continue
|
||||
try:
|
||||
event = json.loads(sse.data)
|
||||
except json.JSONDecodeError:
|
||||
log(f" (unparseable SSE data) {truncate(sse.data)}")
|
||||
continue
|
||||
if self._handle(event):
|
||||
return True
|
||||
return False
|
||||
|
||||
def run(self) -> int:
|
||||
try:
|
||||
self.resolve_workspace()
|
||||
self.upload()
|
||||
|
||||
mode = "start"
|
||||
reconnects = 0
|
||||
while True:
|
||||
try:
|
||||
if self._stream(mode):
|
||||
break
|
||||
# 스트림이 종료 이벤트 없이 닫힘 → 재연결 시도
|
||||
raise ConnectionError("SSE stream ended without a terminal event")
|
||||
except RunTimeout:
|
||||
self.final_status = "timeout"
|
||||
self.final_error = (
|
||||
f"--max-runtime {int(self.max_runtime)}s exceeded — cancelling session"
|
||||
)
|
||||
log(f"ERROR: {self.final_error}")
|
||||
self.cancel_session()
|
||||
break
|
||||
except SSEError as exc:
|
||||
# 서버가 SSE 가 아닌 응답을 반환 (404/500 등)
|
||||
self.final_status = "error"
|
||||
self.final_error = f"SSE handshake failed ({mode}): {exc}"
|
||||
log(f"ERROR: {self.final_error}")
|
||||
break
|
||||
except (httpx.HTTPError, ConnectionError) as exc:
|
||||
if self.final_status != "unknown":
|
||||
break
|
||||
reconnects += 1
|
||||
if not self.session_id or reconnects > self.max_reconnects:
|
||||
self.final_status = "error"
|
||||
self.final_error = (
|
||||
f"connection lost ({exc}); reconnect attempts exhausted "
|
||||
f"({reconnects - 1}/{self.max_reconnects}). "
|
||||
f"과거 세션 조회: GET {self.api_base}/history/sessions"
|
||||
f"?user_id={self.user_id}&agent_name={self.agent_name}"
|
||||
)
|
||||
log(f"ERROR: {self.final_error}")
|
||||
self.cancel_session()
|
||||
break
|
||||
wait = min(10 * reconnects, 60)
|
||||
log(f"Connection lost ({exc}); reconnect {reconnects}/{self.max_reconnects} in {wait}s")
|
||||
time.sleep(wait)
|
||||
mode = "reconnect"
|
||||
except KeyboardInterrupt:
|
||||
# SIGINT / SIGTERM(핸들러가 KeyboardInterrupt 로 변환) — 세션 정리 후 종료
|
||||
self.final_status = "interrupted"
|
||||
self.final_error = "interrupted by SIGINT/SIGTERM — session cancelled"
|
||||
log(self.final_error)
|
||||
self.cancel_session()
|
||||
except SystemExit as exc:
|
||||
# upload() 의 ERROR 경로 — summary 에 남기고 실패 처리
|
||||
self.final_status = "error"
|
||||
self.final_error = str(exc)
|
||||
log(self.final_error)
|
||||
except Exception as exc: # noqa: BLE001 — 어떤 실패든 summary 를 남긴다
|
||||
self.final_status = "error"
|
||||
self.final_error = f"unexpected {type(exc).__name__}: {exc}"
|
||||
log(f"ERROR: {self.final_error}")
|
||||
finally:
|
||||
try:
|
||||
self._write_summary()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
log(f"WARNING: summary write failed: {exc}")
|
||||
self.events_file.close()
|
||||
|
||||
return 0 if self.final_status == "completed" else 1
|
||||
|
||||
# ------------------------------------------------------------------ report
|
||||
|
||||
def _write_summary(self) -> None:
|
||||
elapsed = int(time.monotonic() - self.started_at)
|
||||
status_icon = "✅" if self.final_status == "completed" else "❌"
|
||||
lines = [
|
||||
"# Agent Run Summary",
|
||||
"",
|
||||
"| 항목 | 값 |",
|
||||
"|------|-----|",
|
||||
f"| Agent | `{self.agent_name or '-'}` |",
|
||||
f"| YAML | `{self.yaml_path}` |",
|
||||
f"| Session | `{self.session_id or '-'}` |",
|
||||
f"| workspace | `{self.workspace_name or '-'}` (`{self.workspace_id or '-'}`) |",
|
||||
f"| user_id | `{self.user_id}` |",
|
||||
f"| 최종 상태 | {status_icon} `{self.final_status}` |",
|
||||
f"| 소요 시간 | {elapsed // 60}m {elapsed % 60}s (상한 {int(self.max_runtime) // 60}m) |",
|
||||
f"| 확인(confirm)한 stage 수 | {self.stages_confirmed} |",
|
||||
]
|
||||
if self.final_error:
|
||||
lines += ["", f"**오류/중단 사유:** {self.final_error}"]
|
||||
lines += ["", "## 이벤트 수신 통계", "", "| type | count |", "|------|-------|"]
|
||||
for etype, count in sorted(self.event_counts.items()):
|
||||
lines.append(f"| {etype} | {count} |")
|
||||
summary = "\n".join(lines) + "\n"
|
||||
|
||||
(self.output_dir / "summary.md").write_text(summary, encoding="utf-8")
|
||||
step_summary = os.environ.get("GITHUB_STEP_SUMMARY")
|
||||
if step_summary:
|
||||
with open(step_summary, "a", encoding="utf-8") as f:
|
||||
f.write(summary)
|
||||
log(f"Summary written ({self.final_status}, {elapsed}s)")
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
p.add_argument("--yaml-path", required=True, help="실행할 Agent YAML 경로 (repo 상대/절대)")
|
||||
p.add_argument("--api-base", default="https://legalpoc.eroomai.com/api")
|
||||
p.add_argument("--user-id", default="jsahn")
|
||||
p.add_argument("--workspace-name", default="",
|
||||
help="작업실 이름. GET /workspaces/lookup 으로 UUID 해석 (workspace-id 미지정 시)")
|
||||
p.add_argument("--workspace-id", default="",
|
||||
help="작업실 UUID 직접 지정(override). 지정 시 workspace-name 무시")
|
||||
p.add_argument("--user-input", default="")
|
||||
p.add_argument("--start-stage-index", type=int, default=0)
|
||||
p.add_argument("--output-dir", default="agent_run_output")
|
||||
p.add_argument("--read-timeout", type=float, default=1800.0,
|
||||
help="SSE 이벤트 간 최대 대기 초 (초과 시 재연결)")
|
||||
p.add_argument("--max-reconnects", type=int, default=5)
|
||||
p.add_argument("--max-runtime", type=float, default=9000.0,
|
||||
help="총 실행 시간 상한 초. 초과 시 세션 cancel 후 exit 1. "
|
||||
"Gitea/act_runner 의 3h 태스크 상한 및 job timeout-minutes 보다 짧게 잡을 것")
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
def _raise_interrupt(signum, frame): # noqa: ARG001
|
||||
raise KeyboardInterrupt
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
# job timeout/취소 시 SIGTERM 이 오므로 KeyboardInterrupt 경로로 합류시켜 세션을 정리한다
|
||||
signal.signal(signal.SIGTERM, _raise_interrupt)
|
||||
runner = AgentRunner(args)
|
||||
sys.exit(runner.run())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user