ltcai 0.1.9 → 0.1.11
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/README.md +141 -313
- package/docs/CHANGELOG.md +227 -0
- package/docs/architecture.md +121 -0
- package/docs/mcp-tools.md +116 -0
- package/docs/privacy.md +74 -0
- package/docs/public-deploy.md +137 -0
- package/docs/security-model.md +121 -0
- package/ltcai_cli.py +2 -2
- package/package.json +1 -1
- package/server.py +997 -261
- package/skills/SKILL_TEMPLATE.md +61 -29
- package/skills/code_review/SKILL.md +28 -0
- package/skills/code_review/examples.md +59 -0
- package/skills/code_review/risk.json +9 -0
- package/skills/code_review/schema.json +65 -0
- package/skills/data_analysis/SKILL.md +28 -0
- package/skills/data_analysis/examples.md +62 -0
- package/skills/data_analysis/risk.json +9 -0
- package/skills/data_analysis/schema.json +61 -0
- package/skills/file_edit/SKILL.md +33 -0
- package/skills/file_edit/examples.md +45 -0
- package/skills/file_edit/risk.json +9 -0
- package/skills/file_edit/schema.json +60 -0
- package/skills/summarize_document/SKILL.md +68 -0
- package/skills/summarize_document/examples.md +65 -0
- package/skills/summarize_document/risk.json +9 -0
- package/skills/summarize_document/schema.json +71 -0
- package/skills/web_search/SKILL.md +28 -0
- package/skills/web_search/examples.md +61 -0
- package/skills/web_search/risk.json +9 -0
- package/skills/web_search/schema.json +62 -0
- package/tests/integration/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tests/integration/__pycache__/test_api.cpython-314-pytest-9.0.3.pyc +0 -0
- package/tests/unit/__pycache__/test_tools.cpython-314-pytest-9.0.3.pyc +0 -0
- package/tests/unit/test_tools.py +194 -1
- package/tools.py +264 -4
package/server.py
CHANGED
|
@@ -29,7 +29,8 @@ try:
|
|
|
29
29
|
except Exception as e:
|
|
30
30
|
print(f"⚠️ MLX Metal context unavailable: {e}")
|
|
31
31
|
mx = None
|
|
32
|
-
from
|
|
32
|
+
from enum import Enum
|
|
33
|
+
from typing import AsyncIterator, Optional, List, Dict, TypedDict
|
|
33
34
|
|
|
34
35
|
import uvicorn
|
|
35
36
|
from fastapi import FastAPI, File, HTTPException, Request, Cookie, UploadFile
|
|
@@ -65,12 +66,14 @@ from tools import (
|
|
|
65
66
|
read_document,
|
|
66
67
|
deploy_project,
|
|
67
68
|
desktop_bridge_status,
|
|
69
|
+
edit_file,
|
|
68
70
|
ensure_agent_root,
|
|
69
71
|
execute_tool,
|
|
70
72
|
git_diff,
|
|
71
73
|
git_log,
|
|
72
74
|
git_show,
|
|
73
75
|
git_status,
|
|
76
|
+
grep,
|
|
74
77
|
inspect_html,
|
|
75
78
|
knowledge_save,
|
|
76
79
|
knowledge_search,
|
|
@@ -87,6 +90,8 @@ from tools import (
|
|
|
87
90
|
read_file,
|
|
88
91
|
run_command,
|
|
89
92
|
search_files,
|
|
93
|
+
todo_read,
|
|
94
|
+
todo_write,
|
|
90
95
|
workspace_tree,
|
|
91
96
|
write_file,
|
|
92
97
|
)
|
|
@@ -99,19 +104,15 @@ except Exception:
|
|
|
99
104
|
from datetime import datetime
|
|
100
105
|
|
|
101
106
|
def detect_language(text: str) -> str:
|
|
102
|
-
"""Detect language: 'ko' (Korean)
|
|
107
|
+
"""Detect language: 'ko' (Korean) or 'en' (English)."""
|
|
103
108
|
total = max(len(text), 1)
|
|
104
109
|
ko = sum(1 for c in text if '가' <= c <= '힣')
|
|
105
|
-
zh = sum(1 for c in text if '一' <= c <= '鿿')
|
|
106
110
|
if ko / total > 0.05:
|
|
107
111
|
return "ko"
|
|
108
|
-
if zh / total > 0.05:
|
|
109
|
-
return "zh"
|
|
110
112
|
return "en"
|
|
111
113
|
|
|
112
114
|
_LANG_HINT = {
|
|
113
115
|
"ko": "Respond in Korean (한국어로 답변하세요).",
|
|
114
|
-
"zh": "Respond in Chinese (用中文回答).",
|
|
115
116
|
"en": "Respond in English.",
|
|
116
117
|
}
|
|
117
118
|
|
|
@@ -1978,12 +1979,48 @@ class AgentRequest(BaseModel):
|
|
|
1978
1979
|
message: str
|
|
1979
1980
|
conversation_id: Optional[str] = None
|
|
1980
1981
|
source: Optional[str] = None
|
|
1981
|
-
max_steps: int =
|
|
1982
|
+
max_steps: int = 25
|
|
1982
1983
|
temperature: float = 0.1
|
|
1983
1984
|
user_email: Optional[str] = None
|
|
1984
1985
|
user_nickname: Optional[str] = None
|
|
1985
1986
|
|
|
1986
1987
|
|
|
1988
|
+
class AgentEvalRequest(BaseModel):
|
|
1989
|
+
skill: str
|
|
1990
|
+
case_id: Optional[str] = None
|
|
1991
|
+
|
|
1992
|
+
|
|
1993
|
+
class AgentState(str, Enum):
|
|
1994
|
+
IDLE = "IDLE"
|
|
1995
|
+
PLANNING = "PLANNING"
|
|
1996
|
+
WAITING_APPROVAL = "WAITING_APPROVAL"
|
|
1997
|
+
EXECUTING = "EXECUTING"
|
|
1998
|
+
VERIFYING = "VERIFYING"
|
|
1999
|
+
FAILED = "FAILED"
|
|
2000
|
+
ROLLBACK = "ROLLBACK"
|
|
2001
|
+
DONE = "DONE"
|
|
2002
|
+
|
|
2003
|
+
|
|
2004
|
+
# Terminal states — the agent loop exits when reaching one of these
|
|
2005
|
+
AGENT_TERMINAL_STATES = frozenset({AgentState.DONE, AgentState.FAILED})
|
|
2006
|
+
|
|
2007
|
+
|
|
2008
|
+
class AgentRunContext:
|
|
2009
|
+
"""Mutable state carrier passed through all agent phases."""
|
|
2010
|
+
__slots__ = ("state", "plan", "transcript", "retry_count",
|
|
2011
|
+
"state_history", "corrections", "final_message", "rollback_log")
|
|
2012
|
+
|
|
2013
|
+
def __init__(self) -> None:
|
|
2014
|
+
self.state: AgentState = AgentState.IDLE
|
|
2015
|
+
self.plan: dict = {}
|
|
2016
|
+
self.transcript: list = []
|
|
2017
|
+
self.retry_count: int = 0
|
|
2018
|
+
self.state_history: list = []
|
|
2019
|
+
self.corrections: list = []
|
|
2020
|
+
self.final_message: str = ""
|
|
2021
|
+
self.rollback_log: list = []
|
|
2022
|
+
|
|
2023
|
+
|
|
1987
2024
|
class ToolPathRequest(BaseModel):
|
|
1988
2025
|
path: str = "."
|
|
1989
2026
|
|
|
@@ -2009,6 +2046,33 @@ class ToolSearchFilesRequest(BaseModel):
|
|
|
2009
2046
|
max_results: int = 20
|
|
2010
2047
|
|
|
2011
2048
|
|
|
2049
|
+
class ToolReadFileRequest(BaseModel):
|
|
2050
|
+
path: str
|
|
2051
|
+
offset: int = 0
|
|
2052
|
+
limit: int = 0
|
|
2053
|
+
line_numbers: bool = True
|
|
2054
|
+
|
|
2055
|
+
|
|
2056
|
+
class ToolEditFileRequest(BaseModel):
|
|
2057
|
+
path: str
|
|
2058
|
+
old_string: str
|
|
2059
|
+
new_string: str
|
|
2060
|
+
replace_all: bool = False
|
|
2061
|
+
|
|
2062
|
+
|
|
2063
|
+
class ToolGrepRequest(BaseModel):
|
|
2064
|
+
pattern: str
|
|
2065
|
+
path: str = "."
|
|
2066
|
+
glob: Optional[str] = None
|
|
2067
|
+
max_results: int = 50
|
|
2068
|
+
case_insensitive: bool = False
|
|
2069
|
+
context_lines: int = 0
|
|
2070
|
+
|
|
2071
|
+
|
|
2072
|
+
class ToolTodoWriteRequest(BaseModel):
|
|
2073
|
+
todos: List[Dict] = []
|
|
2074
|
+
|
|
2075
|
+
|
|
2012
2076
|
class ToolWorkspaceTreeRequest(BaseModel):
|
|
2013
2077
|
path: str = "."
|
|
2014
2078
|
max_depth: int = 3
|
|
@@ -3078,130 +3142,446 @@ async def _stream_chat(req: ChatRequest, context: str = "", image_data: str = No
|
|
|
3078
3142
|
|
|
3079
3143
|
# ── Local Computer Agent ──────────────────────────────────────────────────────
|
|
3080
3144
|
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3145
|
+
# ── Agent Role Prompts (Planner / Executor / Critic / Memory Updater) ─────────
|
|
3146
|
+
|
|
3147
|
+
_TOOL_CATALOG_BRIEF = """
|
|
3148
|
+
FILESYSTEM : list_dir workspace_tree read_file write_file edit_file grep search_files inspect_html preview_url
|
|
3149
|
+
PLANNING : todo_read todo_write
|
|
3150
|
+
PROJECT : run_command build_project deploy_project create_web_project
|
|
3151
|
+
GIT (read) : git_status git_diff git_log git_show
|
|
3152
|
+
LOCAL FS : local_list local_read local_write read_document
|
|
3153
|
+
DOCS : create_docx create_xlsx create_pptx create_pdf
|
|
3154
|
+
KNOWLEDGE : knowledge_save knowledge_search knowledge_tree
|
|
3155
|
+
COMPUTER : computer_screenshot computer_open_app computer_open_url computer_click computer_type computer_key
|
|
3156
|
+
MISC : network_status clear_history final
|
|
3157
|
+
"""
|
|
3085
3158
|
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
- local_write: {"action":"local_write","args":{"path":"/Users/username/Desktop/output.txt","content":"..."}} — writes any local file (UI will request user permission first)
|
|
3103
|
-
- read_document: {"action":"read_document","args":{"path":"/absolute/path/to/file.pdf"}} — extract text from PDF, DOCX, XLSX, PPTX, TXT, MD, CSV
|
|
3104
|
-
- computer_screenshot: {"action":"computer_screenshot","args":{}} — capture current screen as base64 PNG
|
|
3105
|
-
- computer_open_app: {"action":"computer_open_app","args":{"app":"Google Chrome"}} — open or focus a Mac app
|
|
3106
|
-
- computer_open_url: {"action":"computer_open_url","args":{"url":"https://example.com","app":"Google Chrome"}} — open URL in app
|
|
3107
|
-
- computer_click: {"action":"computer_click","args":{"x":500,"y":300,"button":"left","double":false}}
|
|
3108
|
-
- computer_type: {"action":"computer_type","args":{"text":"hello"}}
|
|
3109
|
-
- computer_key: {"action":"computer_key","args":{"key":"command+c"}} — e.g. return, escape, tab, command+v
|
|
3110
|
-
- computer_scroll: {"action":"computer_scroll","args":{"x":500,"y":300,"direction":"down","clicks":3}}
|
|
3111
|
-
- computer_move: {"action":"computer_move","args":{"x":500,"y":300}}
|
|
3112
|
-
- computer_drag: {"action":"computer_drag","args":{"x1":100,"y1":100,"x2":500,"y2":500}}
|
|
3113
|
-
- computer_status: {"action":"computer_status","args":{}} — check if Computer Use is available
|
|
3114
|
-
- chrome_status: {"action":"chrome_status","args":{}}
|
|
3115
|
-
- computer_use_status: {"action":"computer_use_status","args":{}}
|
|
3116
|
-
- knowledge_save: {"action":"knowledge_save","args":{"folder":"30_Projects","title":"short title","content":"note"}}
|
|
3117
|
-
- knowledge_search: {"action":"knowledge_search","args":{"query":"keyword","max_results":5}}
|
|
3118
|
-
- knowledge_tree: {"action":"knowledge_tree","args":{}}
|
|
3119
|
-
- obsidian_save: {"action":"obsidian_save","args":{"folder":"30_Projects","title":"short title","content":"note"}}
|
|
3120
|
-
- obsidian_search: {"action":"obsidian_search","args":{"query":"keyword","max_results":5}}
|
|
3121
|
-
- obsidian_tree: {"action":"obsidian_tree","args":{}}
|
|
3122
|
-
- git_status: {"action":"git_status","args":{}}
|
|
3123
|
-
- git_diff: {"action":"git_diff","args":{"path":"optional/relative/path"}}
|
|
3124
|
-
- git_log: {"action":"git_log","args":{"max_count":5}}
|
|
3125
|
-
- git_show: {"action":"git_show","args":{"revision":"HEAD"}}
|
|
3126
|
-
- network_status: {"action":"network_status","args":{}} — get current local/private IP, public IP, hostname, and Wi-Fi info
|
|
3127
|
-
- run_command: {"action":"run_command","args":{"command":"python3 app.py","cwd":"."}}
|
|
3128
|
-
- build_project: {"action":"build_project","args":{"cwd":".","script":"build"}}
|
|
3129
|
-
- deploy_project: {"action":"deploy_project","args":{"cwd":".","script":"deploy"}}
|
|
3130
|
-
- final: {"action":"final","message":"short Korean summary of what you did"}
|
|
3159
|
+
PLANNER_PROMPT = """You are the PLANNER role in Lattice AI's multi-role agent harness.
|
|
3160
|
+
Your ONLY job: analyze the request and produce a structured execution plan.
|
|
3161
|
+
You do NOT call tools or write code.
|
|
3162
|
+
|
|
3163
|
+
Respond with exactly ONE JSON object (no markdown, no fences):
|
|
3164
|
+
{
|
|
3165
|
+
"action": "plan",
|
|
3166
|
+
"state": "PLANNING",
|
|
3167
|
+
"goal": "one-sentence goal in the user's language",
|
|
3168
|
+
"steps": [
|
|
3169
|
+
{"id": 1, "description": "what this step does", "action": "expected_tool", "purpose": "why needed"}
|
|
3170
|
+
],
|
|
3171
|
+
"requires_approval": true,
|
|
3172
|
+
"rollback_strategy": "git",
|
|
3173
|
+
"estimated_steps": 3
|
|
3174
|
+
}
|
|
3131
3175
|
|
|
3132
3176
|
Rules:
|
|
3133
|
-
-
|
|
3134
|
-
-
|
|
3135
|
-
-
|
|
3136
|
-
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
-
|
|
3145
|
-
-
|
|
3146
|
-
-
|
|
3147
|
-
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3177
|
+
- requires_approval = true if ANY step uses write/exec tools (edit_file, write_file, run_command, etc.)
|
|
3178
|
+
- rollback_strategy = "git" if steps modify existing files; "none" otherwise
|
|
3179
|
+
- Keep steps realistic: 2-4 for simple tasks, up to 10 for complex ones
|
|
3180
|
+
- Do NOT specify full tool args — that is the Executor's job
|
|
3181
|
+
|
|
3182
|
+
Available tools:""" + _TOOL_CATALOG_BRIEF
|
|
3183
|
+
|
|
3184
|
+
EXECUTOR_PROMPT = """You are the EXECUTOR role in Lattice AI's multi-role agent harness.
|
|
3185
|
+
You have a plan from the Planner. Execute it step by step using exactly one tool per response.
|
|
3186
|
+
|
|
3187
|
+
You think and act like a senior software engineer:
|
|
3188
|
+
- Read (read_file, grep) BEFORE editing — never guess at file contents
|
|
3189
|
+
- Prefer edit_file over write_file for existing files
|
|
3190
|
+
- Keep changes small and precise
|
|
3191
|
+
- Verify after changes with build_project or run_command
|
|
3192
|
+
|
|
3193
|
+
Respond with exactly ONE JSON object per step:
|
|
3194
|
+
{"thoughts": "what you learned / why this next action", "action": "tool_name", "args": {...}}
|
|
3195
|
+
|
|
3196
|
+
When the task is fully done AND a tool result in this run confirms it:
|
|
3197
|
+
{"thoughts": "verified", "action": "final", "message": "한국어로 무엇을 했고 어디서 검증했는지 요약"}
|
|
3198
|
+
|
|
3199
|
+
ANTI-PATTERNS (will halt the loop):
|
|
3200
|
+
- Editing without reading first → read_file + grep BEFORE edit_file
|
|
3201
|
+
- Repeating the same action+args → check the transcript
|
|
3202
|
+
- Claiming done without a verification tool result in transcript
|
|
3203
|
+
- Hallucinating imports or file paths that were never confirmed by a tool result
|
|
3204
|
+
|
|
3205
|
+
Available tools:""" + _TOOL_CATALOG_BRIEF
|
|
3206
|
+
|
|
3207
|
+
CRITIC_PROMPT = """You are the CRITIC / REVIEWER role in Lattice AI's multi-role agent harness.
|
|
3208
|
+
Review the execution transcript and determine whether the goal was achieved.
|
|
3209
|
+
|
|
3210
|
+
Respond with exactly ONE JSON object:
|
|
3211
|
+
{
|
|
3212
|
+
"action": "verdict",
|
|
3213
|
+
"state": "VERIFYING",
|
|
3214
|
+
"verdict": "PASS",
|
|
3215
|
+
"reason": "why you think it passed or failed (cite specific tool results)",
|
|
3216
|
+
"corrections": [],
|
|
3217
|
+
"confidence": 0.95,
|
|
3218
|
+
"next_state": "DONE"
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
verdict: "PASS" | "FAIL"
|
|
3222
|
+
next_state:
|
|
3223
|
+
"DONE" — task succeeded; finish
|
|
3224
|
+
"EXECUTING" — task failed but corrections can fix it (use corrections field for retry)
|
|
3225
|
+
"ROLLBACK" — task failed AND file changes should be undone
|
|
3226
|
+
|
|
3227
|
+
Criteria for PASS: a tool result in the transcript explicitly confirms success.
|
|
3228
|
+
Be strict. Claiming done without evidence = FAIL."""
|
|
3229
|
+
|
|
3230
|
+
MEMORY_UPDATER_PROMPT = """You are the MEMORY UPDATER role in Lattice AI's multi-role agent harness.
|
|
3231
|
+
After a completed task, extract reusable learnings.
|
|
3232
|
+
|
|
3233
|
+
Respond with exactly ONE JSON object:
|
|
3234
|
+
{
|
|
3235
|
+
"action": "memory",
|
|
3236
|
+
"state": "DONE",
|
|
3237
|
+
"learnings": ["one concise fact about this codebase or task"],
|
|
3238
|
+
"artifacts": ["relative/path/to/created_or_modified_file"],
|
|
3239
|
+
"save_to_knowledge": false
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
Rules:
|
|
3243
|
+
- max 5 learnings, one sentence each
|
|
3244
|
+
- save_to_knowledge = true only if learnings are genuinely useful across future sessions
|
|
3245
|
+
- artifacts = files the Executor actually created or modified (from transcript)
|
|
3151
3246
|
"""
|
|
3152
3247
|
|
|
3248
|
+
# Keep backward-compat alias used by any existing callers
|
|
3249
|
+
AGENT_SYSTEM_PROMPT = EXECUTOR_PROMPT
|
|
3250
|
+
|
|
3251
|
+
# Marker: the old monolithic prompt was replaced by 4-role prompts above.
|
|
3252
|
+
# Legacy variable kept so Telegram bot / VS Code extension still work.
|
|
3253
|
+
|
|
3254
|
+
_ORIGINAL_MONOLITHIC_PROMPT_NOTE = """You are Lattice AI Agent — a local, professional-grade coding assistant.
|
|
3255
|
+
You have full access to a sandboxed workspace and (with user approval) the wider filesystem.
|
|
3256
|
+
You think and work like a senior software engineer, not like an autocompleter.
|
|
3257
|
+
|
|
3258
|
+
================================================================================
|
|
3259
|
+
HOW A PROFESSIONAL DEVELOPER THINKS — your operating loop
|
|
3260
|
+
================================================================================
|
|
3261
|
+
Every multi-step task follows four phases. Skipping phases is the #1 cause of bad
|
|
3262
|
+
output. Do not skip them.
|
|
3263
|
+
|
|
3264
|
+
1) DISCOVER (read first, then act)
|
|
3265
|
+
- Map the territory before changing it. Use workspace_tree, list_dir, grep,
|
|
3266
|
+
and read_file BEFORE writing or editing anything.
|
|
3267
|
+
- When the user names a file/feature/function, locate it (grep) and read the
|
|
3268
|
+
surrounding code BEFORE proposing a change.
|
|
3269
|
+
- Read package.json, pyproject.toml, requirements.txt, tsconfig.json, and
|
|
3270
|
+
other config files before assuming a library/version/tool is available.
|
|
3271
|
+
- Never guess at APIs, imports, file paths, function signatures, or types.
|
|
3272
|
+
If you don't know, look it up with grep/read_file. Hallucinated code is
|
|
3273
|
+
the worst possible output.
|
|
3274
|
+
|
|
3275
|
+
2) PLAN (write the plan down)
|
|
3276
|
+
- For any task with 3+ distinct steps, call todo_write FIRST with a concrete
|
|
3277
|
+
checklist (3–10 items). Keep exactly one item in_progress at a time.
|
|
3278
|
+
- The plan should describe WHAT will change and HOW you'll verify it works,
|
|
3279
|
+
not vague intentions ("look at code", "fix bugs"). Bad plans produce bad code.
|
|
3280
|
+
- Update the todo list (todo_write again) as items complete or new ones emerge.
|
|
3281
|
+
|
|
3282
|
+
3) IMPLEMENT (small, precise diffs)
|
|
3283
|
+
- Prefer edit_file over write_file when modifying existing files. edit_file
|
|
3284
|
+
requires exact byte-level old_string match — read the file first and copy
|
|
3285
|
+
the surrounding context verbatim. This forces correctness.
|
|
3286
|
+
- Use write_file only for brand-new files or when fully rewriting a file you
|
|
3287
|
+
understand end-to-end.
|
|
3288
|
+
- Keep diffs as small as the task requires. Don't refactor "while you're
|
|
3289
|
+
there." Don't add abstractions for hypothetical future needs.
|
|
3290
|
+
- Code quality:
|
|
3291
|
+
* No new comments unless the WHY is non-obvious (a subtle invariant, a
|
|
3292
|
+
workaround for a specific bug, behavior that would surprise a reader).
|
|
3293
|
+
Never write comments that just restate what the code does.
|
|
3294
|
+
* No backward-compat shims, no dead code, no unused imports/variables.
|
|
3295
|
+
* No defensive try/except around code that can't fail. Trust internal
|
|
3296
|
+
contracts; validate only at system boundaries (user input, network).
|
|
3297
|
+
* Match the surrounding code's style (indent, quotes, naming).
|
|
3298
|
+
|
|
3299
|
+
4) VERIFY (prove it works before claiming done)
|
|
3300
|
+
- After code changes, RUN something that confirms correctness:
|
|
3301
|
+
* build_project for build/typecheck/test scripts
|
|
3302
|
+
* run_command for python/node scripts and tests
|
|
3303
|
+
* inspect_html + preview_url for generated UI
|
|
3304
|
+
- If verification fails, treat the failure as the new task. Diagnose root
|
|
3305
|
+
cause; do not paper over it (no try/except shortcuts, no --no-verify, no
|
|
3306
|
+
disabling tests). Re-enter Discover phase if needed.
|
|
3307
|
+
- Never claim a task is "complete," "saved," "fixed," "working," or
|
|
3308
|
+
"deployed" unless a tool result in this same agent run confirms it.
|
|
3309
|
+
|
|
3310
|
+
================================================================================
|
|
3311
|
+
RESPONSE FORMAT (strict)
|
|
3312
|
+
================================================================================
|
|
3313
|
+
Respond with exactly ONE JSON object per step. No markdown, no code fences, no
|
|
3314
|
+
extra prose. Include a short `thoughts` field that records your current reasoning
|
|
3315
|
+
(what you just learned, what you'll do next, why). The user does not see it
|
|
3316
|
+
directly — it exists so you can plan across steps.
|
|
3317
|
+
|
|
3318
|
+
{"thoughts": "Need to read App.tsx before editing the import. Workspace tree
|
|
3319
|
+
confirms only one App.tsx exists.",
|
|
3320
|
+
"action": "read_file",
|
|
3321
|
+
"args": {"path": "src/App.tsx"}}
|
|
3322
|
+
|
|
3323
|
+
When the task is fully complete AND verified:
|
|
3324
|
+
{"thoughts": "Build passed, file written, ready to summarize.",
|
|
3325
|
+
"action": "final",
|
|
3326
|
+
"message": "한국어로 간결하게 무엇을 만들었고 어디서 검증했는지 요약."}
|
|
3327
|
+
|
|
3328
|
+
If you cannot proceed (missing tool, blocked path, ambiguous user intent), use
|
|
3329
|
+
`final` and clearly state the blocker and the smallest next step the user can
|
|
3330
|
+
take to unblock it. Do NOT loop on the same failing action.
|
|
3331
|
+
|
|
3332
|
+
================================================================================
|
|
3333
|
+
TOOL CATALOG
|
|
3334
|
+
================================================================================
|
|
3335
|
+
Filesystem (workspace, relative paths):
|
|
3336
|
+
list_dir {"path":"."}
|
|
3337
|
+
workspace_tree {"path":".", "max_depth":3}
|
|
3338
|
+
read_file {"path":"src/App.tsx", "offset":0, "limit":0, "line_numbers":true}
|
|
3339
|
+
— returns numbered view + total_lines. Use offset/limit for big files.
|
|
3340
|
+
write_file {"path":"new_file.py", "content":"..."} — new files / full rewrites
|
|
3341
|
+
edit_file {"path":"existing.py", "old_string":"exact text", "new_string":"new text",
|
|
3342
|
+
"replace_all":false}
|
|
3343
|
+
— preferred for existing files. old_string MUST appear once
|
|
3344
|
+
(unless replace_all=true). Include enough surrounding context
|
|
3345
|
+
to make it unique.
|
|
3346
|
+
grep {"pattern":"regex", "path":".", "glob":"*.py", "max_results":50,
|
|
3347
|
+
"case_insensitive":false, "context_lines":2}
|
|
3348
|
+
— regex search across the codebase. Use this before assuming a
|
|
3349
|
+
symbol exists.
|
|
3350
|
+
search_files {"query":"substring", "path":".", "max_results":20} — legacy substring search
|
|
3351
|
+
inspect_html {"path":"index.html"}
|
|
3352
|
+
preview_url {"path":"index.html"}
|
|
3353
|
+
|
|
3354
|
+
Planning:
|
|
3355
|
+
todo_read {}
|
|
3356
|
+
todo_write {"todos":[{"id":"1","content":"...","status":"pending"}]}
|
|
3357
|
+
— status ∈ pending|in_progress|completed.
|
|
3358
|
+
Use proactively for any task with 3+ steps.
|
|
3359
|
+
|
|
3360
|
+
Project ops:
|
|
3361
|
+
run_command {"command":"python3 app.py", "cwd":"."}
|
|
3362
|
+
— allowed binaries: pwd ls find cat sed head tail wc rg python python3 node npm npx
|
|
3363
|
+
— git is NOT allowed here; use the git_* tools below (read-only).
|
|
3364
|
+
build_project {"cwd":".", "script":"build"} — also: compile, typecheck, test
|
|
3365
|
+
deploy_project {"cwd":".", "script":"deploy"} — also: preview, release, package, dist, make, build:pkg, build:exe
|
|
3366
|
+
create_web_project {"path":"my_app", "framework":"react", "template":"vite"}
|
|
3367
|
+
|
|
3368
|
+
Git (read-only):
|
|
3369
|
+
git_status, git_diff, git_log, git_show
|
|
3370
|
+
— Never commit/push/pull/fetch/clone/reset/checkout. Lattice agent does not author git history.
|
|
3371
|
+
|
|
3372
|
+
Local filesystem (outside workspace; UI prompts user for approval):
|
|
3373
|
+
local_list {"path":"/Users/.../Downloads"}
|
|
3374
|
+
local_read {"path":"/abs/path/file.txt"}
|
|
3375
|
+
local_write {"path":"/abs/path/file.txt", "content":"..."}
|
|
3376
|
+
read_document {"path":"/abs/path/report.pdf"} — PDF, DOCX, XLSX, PPTX, TXT, MD, CSV
|
|
3377
|
+
|
|
3378
|
+
Document generation (written to workspace generated_* folders):
|
|
3379
|
+
create_docx {"title":"...", "body":"...", "filename":"doc.docx"}
|
|
3380
|
+
create_xlsx {"rows":[["A","B"],[1,2]], "filename":"sheet.xlsx", "sheet_name":"Sheet1"}
|
|
3381
|
+
create_pptx {"title":"...", "slides":[{"title":"...","bullets":["..."]}], "filename":"deck.pptx"}
|
|
3382
|
+
create_pdf {"title":"...", "body":"...", "filename":"doc.pdf"}
|
|
3383
|
+
|
|
3384
|
+
Knowledge / memory (Obsidian-compatible Markdown vault):
|
|
3385
|
+
knowledge_save {"folder":"30_Projects", "title":"...", "content":"..."}
|
|
3386
|
+
knowledge_search {"query":"...", "max_results":5}
|
|
3387
|
+
knowledge_tree {}
|
|
3388
|
+
obsidian_save / obsidian_search / obsidian_tree — same as knowledge_*, with vault URIs
|
|
3389
|
+
|
|
3390
|
+
Computer use (macOS desktop control, requires Accessibility permission):
|
|
3391
|
+
computer_screenshot, computer_open_app, computer_open_url, computer_click,
|
|
3392
|
+
computer_type, computer_key, computer_scroll, computer_move, computer_drag,
|
|
3393
|
+
computer_status, chrome_status, computer_use_status
|
|
3394
|
+
— Use screenshot to ground state; click/type to interact. Verify with another screenshot.
|
|
3395
|
+
|
|
3396
|
+
Misc:
|
|
3397
|
+
network_status {}
|
|
3398
|
+
clear_history {"keep_last":0}
|
|
3399
|
+
final {"message":"..."}
|
|
3400
|
+
|
|
3401
|
+
================================================================================
|
|
3402
|
+
DOMAIN RULES (keep in mind)
|
|
3403
|
+
================================================================================
|
|
3404
|
+
- Frontend: don't assume Tailwind/framer-motion/TypeScript exist. Read
|
|
3405
|
+
package.json first. If a dependency is missing, either add it explicitly to
|
|
3406
|
+
package.json (and create the config files it needs) or pick a simpler stack
|
|
3407
|
+
that already works.
|
|
3408
|
+
- Installers (.pkg/.exe): set up the packaging config (e.g. electron-builder)
|
|
3409
|
+
with full scripts in package.json, then run deploy_project. If the current
|
|
3410
|
+
OS/toolchain can't produce the artifact, still generate complete config and
|
|
3411
|
+
state the exact missing prerequisite — do not say "I can't."
|
|
3412
|
+
- Data analysis: read the data files (read_document/local_read), compute with
|
|
3413
|
+
run_command, report concrete findings plus output artifact paths.
|
|
3414
|
+
- Document requests (docx/xlsx/pptx/pdf, 문서/엑셀/PPT/피피티/파워포인트): call
|
|
3415
|
+
the matching create_* action immediately with rich, complete content. Never
|
|
3416
|
+
say you cannot create files.
|
|
3417
|
+
- Korean/English: answer in the language the user used; default to Korean
|
|
3418
|
+
if mixed or ambiguous.
|
|
3419
|
+
|
|
3420
|
+
================================================================================
|
|
3421
|
+
ANTI-PATTERNS (will be flagged by the orchestrator)
|
|
3422
|
+
================================================================================
|
|
3423
|
+
- Editing without reading first → use read_file + grep before edit_file.
|
|
3424
|
+
- Repeating the same action with the same args → the loop will halt you.
|
|
3425
|
+
- Claiming "done" without a verification tool result in the transcript.
|
|
3426
|
+
- Adding new dependencies without updating package.json / requirements.txt.
|
|
3427
|
+
- Producing fragments when the user asked for a complete file or runnable app.
|
|
3428
|
+
- Stuffing speculative features beyond the user's actual request.
|
|
3429
|
+
- Decorative placeholder URLs / fake data when real data is available.
|
|
3430
|
+
"""
|
|
3153
3431
|
|
|
3154
|
-
|
|
3432
|
+
|
|
3433
|
+
_FILE_CREATE_ACTIONS = {"create_docx", "create_xlsx", "create_pptx", "create_pdf", "write_file", "edit_file", "create_web_project"}
|
|
3155
3434
|
|
|
3156
3435
|
# Harness risk level per tool action.
|
|
3157
3436
|
# low — read-only, no side effects
|
|
3158
3437
|
# medium — write/create files or knowledge entries
|
|
3159
3438
|
# high — execute commands, control computer, write to arbitrary FS paths
|
|
3160
|
-
|
|
3161
|
-
# read
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
#
|
|
3165
|
-
|
|
3166
|
-
#
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
#
|
|
3181
|
-
"
|
|
3182
|
-
|
|
3183
|
-
"
|
|
3184
|
-
|
|
3185
|
-
"
|
|
3186
|
-
"
|
|
3187
|
-
"
|
|
3439
|
+
class ToolPolicy(TypedDict):
|
|
3440
|
+
risk: str # "read" | "write" | "exec" | "destructive"
|
|
3441
|
+
destructive: bool # True = data loss possible, no auto-undo
|
|
3442
|
+
shell: bool # True = spawns a subprocess
|
|
3443
|
+
network: bool # True = makes external network calls
|
|
3444
|
+
auto_approve: bool# True = agent may call without human confirmation
|
|
3445
|
+
sandbox: str # "workspace" | "home" | "system"
|
|
3446
|
+
rollback: str # "none" | "backup" | "git"
|
|
3447
|
+
|
|
3448
|
+
|
|
3449
|
+
_R = lambda s, sb="workspace", ro="none": ToolPolicy(risk="read", destructive=False, shell=False, network=False, auto_approve=True, sandbox=sb, rollback=ro)
|
|
3450
|
+
_RS = lambda s, sb="workspace", ro="none": ToolPolicy(risk="read", destructive=False, shell=True, network=False, auto_approve=True, sandbox=sb, rollback=ro)
|
|
3451
|
+
_RN = lambda s, sb="system", ro="none": ToolPolicy(risk="read", destructive=False, shell=True, network=True, auto_approve=True, sandbox=sb, rollback=ro)
|
|
3452
|
+
_W = lambda s, sb="workspace", ro="none": ToolPolicy(risk="write", destructive=False, shell=False, network=False, auto_approve=False, sandbox=sb, rollback=ro)
|
|
3453
|
+
_E = lambda s, sb="workspace", ro="none": ToolPolicy(risk="exec", destructive=False, shell=True, network=False, auto_approve=False, sandbox=sb, rollback=ro)
|
|
3454
|
+
_EN = lambda s, sb="workspace", ro="none": ToolPolicy(risk="exec", destructive=False, shell=True, network=True, auto_approve=False, sandbox=sb, rollback=ro)
|
|
3455
|
+
_EC = lambda s, sb="system", ro="none": ToolPolicy(risk="exec", destructive=False, shell=False, network=False, auto_approve=False, sandbox=sb, rollback=ro)
|
|
3456
|
+
_D = lambda s, sb="workspace", ro="none": ToolPolicy(risk="destructive", destructive=True, shell=True, network=False, auto_approve=False, sandbox=sb, rollback=ro)
|
|
3457
|
+
|
|
3458
|
+
TOOL_GOVERNANCE: Dict[str, ToolPolicy] = {
|
|
3459
|
+
# ── read-only / workspace ──────────────────────────────────────────────────
|
|
3460
|
+
"list_dir": _R("list_dir"),
|
|
3461
|
+
"workspace_tree": _R("workspace_tree"),
|
|
3462
|
+
"read_file": _R("read_file"),
|
|
3463
|
+
"search_files": _R("search_files"),
|
|
3464
|
+
"grep": _R("grep"),
|
|
3465
|
+
"inspect_html": _R("inspect_html"),
|
|
3466
|
+
"todo_read": _R("todo_read"),
|
|
3467
|
+
# ── read-only / home FS ───────────────────────────────────────────────────
|
|
3468
|
+
"local_list": _R("local_list", sb="home"),
|
|
3469
|
+
"local_read": _R("local_read", sb="home"),
|
|
3470
|
+
# ── read-only / git (spawns subprocess, read-only) ───────────────────────
|
|
3471
|
+
"git_status": _RS("git_status"),
|
|
3472
|
+
"git_diff": _RS("git_diff"),
|
|
3473
|
+
"git_log": _RS("git_log"),
|
|
3474
|
+
"git_show": _RS("git_show"),
|
|
3475
|
+
# ── read-only / knowledge ─────────────────────────────────────────────────
|
|
3476
|
+
"knowledge_search": _R("knowledge_search", sb="home"),
|
|
3477
|
+
"knowledge_tree": _R("knowledge_tree", sb="home"),
|
|
3478
|
+
"obsidian_search": _R("obsidian_search", sb="home"),
|
|
3479
|
+
"obsidian_tree": _R("obsidian_tree", sb="home"),
|
|
3480
|
+
# ── read-only / system ────────────────────────────────────────────────────
|
|
3481
|
+
"computer_screenshot":_R("computer_screenshot", sb="system"),
|
|
3482
|
+
"computer_status": _R("computer_status", sb="system"),
|
|
3483
|
+
"chrome_status": _R("chrome_status", sb="system"),
|
|
3484
|
+
"computer_use_status":_R("computer_use_status", sb="system"),
|
|
3485
|
+
"network_status": _RN("network_status"),
|
|
3486
|
+
# ── write / workspace ─────────────────────────────────────────────────────
|
|
3487
|
+
"write_file": _W("write_file", ro="git"),
|
|
3488
|
+
"edit_file": _W("edit_file", ro="git"),
|
|
3489
|
+
"create_web_project": _W("create_web_project"),
|
|
3490
|
+
"create_docx": _W("create_docx"),
|
|
3491
|
+
"create_xlsx": _W("create_xlsx"),
|
|
3492
|
+
"create_pptx": _W("create_pptx"),
|
|
3493
|
+
"create_pdf": _W("create_pdf"),
|
|
3494
|
+
"preview_url": _W("preview_url"),
|
|
3495
|
+
"todo_write": _W("todo_write"),
|
|
3496
|
+
# ── write / home FS ───────────────────────────────────────────────────────
|
|
3497
|
+
"knowledge_save": _W("knowledge_save", sb="home"),
|
|
3498
|
+
"obsidian_save": _W("obsidian_save", sb="home"),
|
|
3499
|
+
"local_write": _W("local_write", sb="home"),
|
|
3500
|
+
# ── exec / workspace ──────────────────────────────────────────────────────
|
|
3501
|
+
"run_command": _E("run_command"),
|
|
3502
|
+
"build_project": _E("build_project"),
|
|
3503
|
+
# ── exec / network ────────────────────────────────────────────────────────
|
|
3504
|
+
"deploy_project": _EN("deploy_project"),
|
|
3505
|
+
# ── exec / computer use (system-level input injection) ───────────────────
|
|
3506
|
+
"computer_click": _EC("computer_click"),
|
|
3507
|
+
"computer_type": _EC("computer_type"),
|
|
3508
|
+
"computer_key": _EC("computer_key"),
|
|
3509
|
+
"computer_scroll": _EC("computer_scroll"),
|
|
3510
|
+
"computer_drag": _EC("computer_drag"),
|
|
3511
|
+
"computer_move": _EC("computer_move"),
|
|
3512
|
+
"computer_open_app": _EC("computer_open_app"),
|
|
3513
|
+
"computer_open_url": ToolPolicy(risk="exec", destructive=False, shell=False, network=True, auto_approve=False, sandbox="system", rollback="none"),
|
|
3188
3514
|
}
|
|
3189
3515
|
|
|
3190
|
-
|
|
3516
|
+
_TOOL_GOVERNANCE_DEFAULT = ToolPolicy(
|
|
3517
|
+
risk="write", destructive=False, shell=False, network=False,
|
|
3518
|
+
auto_approve=False, sandbox="workspace", rollback="none",
|
|
3519
|
+
)
|
|
3520
|
+
|
|
3521
|
+
# Paths that local_write / local_list must never target
|
|
3191
3522
|
_LOCAL_WRITE_BLOCKED_PREFIXES = (
|
|
3192
3523
|
"/etc/", "/usr/", "/bin/", "/sbin/", "/System/", "/private/etc/",
|
|
3193
3524
|
"/Library/LaunchDaemons/", "/Library/LaunchAgents/",
|
|
3194
3525
|
)
|
|
3195
3526
|
|
|
3527
|
+
# Backward-compat: map policy risk → legacy low/medium/high string
|
|
3528
|
+
_RISK_LEVEL_MAP = {"read": "low", "write": "medium", "exec": "high", "destructive": "high"}
|
|
3196
3529
|
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3530
|
+
|
|
3531
|
+
def _agent_policy(action_name: str, args: dict) -> ToolPolicy:
|
|
3532
|
+
"""Return the full governance policy for an action.
|
|
3533
|
+
|
|
3534
|
+
Upgrades local_write to destructive risk when targeting system paths.
|
|
3535
|
+
"""
|
|
3536
|
+
policy = TOOL_GOVERNANCE.get(action_name, _TOOL_GOVERNANCE_DEFAULT)
|
|
3200
3537
|
if action_name == "local_write":
|
|
3201
3538
|
path = str(args.get("path", ""))
|
|
3202
3539
|
if any(path.startswith(p) for p in _LOCAL_WRITE_BLOCKED_PREFIXES):
|
|
3203
|
-
|
|
3204
|
-
|
|
3540
|
+
policy = ToolPolicy(
|
|
3541
|
+
risk="destructive", destructive=True, shell=False, network=False,
|
|
3542
|
+
auto_approve=False, sandbox="system", rollback="none",
|
|
3543
|
+
)
|
|
3544
|
+
return policy
|
|
3545
|
+
|
|
3546
|
+
|
|
3547
|
+
def _agent_risk(action_name: str, args: dict) -> str:
|
|
3548
|
+
"""Return legacy low/medium/high risk string (kept for transcript backward-compat)."""
|
|
3549
|
+
return _RISK_LEVEL_MAP.get(_agent_policy(action_name, args)["risk"], "medium")
|
|
3550
|
+
|
|
3551
|
+
|
|
3552
|
+
# ── Tool Permission Layer ─────────────────────────────────────────────────────
|
|
3553
|
+
# A compact, public-facing view of each tool's authorization profile, derived
|
|
3554
|
+
# from TOOL_GOVERNANCE. Designed for client UIs / approval dialogs that don't
|
|
3555
|
+
# need the full 7-dimensional governance object.
|
|
3556
|
+
#
|
|
3557
|
+
# Example:
|
|
3558
|
+
# { "tool": "shell", "risk": "high", "requires_approval": true, "network": false }
|
|
3559
|
+
|
|
3560
|
+
class ToolPermission(TypedDict):
|
|
3561
|
+
tool: str
|
|
3562
|
+
risk: str # "low" | "medium" | "high"
|
|
3563
|
+
requires_approval: bool # inverse of governance.auto_approve
|
|
3564
|
+
network: bool # tool makes external network calls
|
|
3565
|
+
|
|
3566
|
+
|
|
3567
|
+
def get_tool_permission(name: str, args: Optional[dict] = None) -> ToolPermission:
|
|
3568
|
+
"""Return the simplified permission view for a tool name.
|
|
3569
|
+
|
|
3570
|
+
`args` lets path-sensitive tools (e.g. local_write to /etc) escalate risk;
|
|
3571
|
+
omit it for static catalog views.
|
|
3572
|
+
"""
|
|
3573
|
+
policy = _agent_policy(name, args or {})
|
|
3574
|
+
return ToolPermission(
|
|
3575
|
+
tool=name,
|
|
3576
|
+
risk=_RISK_LEVEL_MAP.get(policy["risk"], "medium"),
|
|
3577
|
+
requires_approval=not policy["auto_approve"],
|
|
3578
|
+
network=policy["network"],
|
|
3579
|
+
)
|
|
3580
|
+
|
|
3581
|
+
|
|
3582
|
+
def list_tool_permissions() -> list:
|
|
3583
|
+
"""Return permission views for every governed tool, sorted by tool name."""
|
|
3584
|
+
return [get_tool_permission(name) for name in sorted(TOOL_GOVERNANCE.keys())]
|
|
3205
3585
|
|
|
3206
3586
|
|
|
3207
3587
|
def _collect_created_files(transcript: list) -> list:
|
|
@@ -3250,142 +3630,420 @@ def _extract_agent_action(raw: str) -> Dict:
|
|
|
3250
3630
|
return action
|
|
3251
3631
|
|
|
3252
3632
|
|
|
3253
|
-
|
|
3254
|
-
async def agent(req: AgentRequest, request: Request):
|
|
3255
|
-
"""Natural-language local agent loop for Telegram and future clients."""
|
|
3256
|
-
current_user = require_user(request)
|
|
3257
|
-
enforce_rate_limit(current_user, "agent")
|
|
3258
|
-
if not router.current_model_id:
|
|
3259
|
-
raise HTTPException(status_code=400, detail="No model loaded. Call /models/load first.")
|
|
3633
|
+
# ── Agent State Machine — Phase Functions ─────────────────────────────────────
|
|
3260
3634
|
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3635
|
+
async def _phase_plan(
|
|
3636
|
+
ctx: AgentRunContext, req: AgentRequest, router, lang_hint: str, current_user: str,
|
|
3637
|
+
) -> None:
|
|
3638
|
+
"""PLAN: Planner role produces a structured plan JSON."""
|
|
3639
|
+
context = (
|
|
3640
|
+
f"{PLANNER_PROMPT}\n\n"
|
|
3641
|
+
f"[LANGUAGE HINT: {lang_hint}]\n"
|
|
3642
|
+
f"Workspace root: {AGENT_ROOT}\n\n"
|
|
3643
|
+
f"User request: {req.message}"
|
|
3644
|
+
)
|
|
3645
|
+
raw = await router.generate(
|
|
3646
|
+
message="Produce a JSON execution plan for this request.",
|
|
3647
|
+
context=context, max_tokens=1024, temperature=0.1,
|
|
3648
|
+
)
|
|
3649
|
+
try:
|
|
3650
|
+
plan = _extract_agent_action(str(raw))
|
|
3651
|
+
except ValueError:
|
|
3652
|
+
plan = {
|
|
3653
|
+
"action": "plan", "state": "PLAN",
|
|
3654
|
+
"goal": req.message, "steps": [],
|
|
3655
|
+
"requires_approval": False, "rollback_strategy": "none", "estimated_steps": 1,
|
|
3656
|
+
}
|
|
3657
|
+
ctx.plan = plan
|
|
3658
|
+
ctx.transcript.append({
|
|
3659
|
+
"state": AgentState.PLANNING.value,
|
|
3660
|
+
"goal": plan.get("goal", req.message),
|
|
3661
|
+
"steps": plan.get("steps", []),
|
|
3662
|
+
"requires_approval": plan.get("requires_approval", False),
|
|
3663
|
+
"rollback_strategy": plan.get("rollback_strategy", "none"),
|
|
3664
|
+
"estimated_steps": plan.get("estimated_steps", 1),
|
|
3665
|
+
})
|
|
3666
|
+
ctx.state = AgentState.WAITING_APPROVAL
|
|
3667
|
+
|
|
3668
|
+
|
|
3669
|
+
def _phase_approval(ctx: AgentRunContext, current_user: str) -> None:
|
|
3670
|
+
"""APPROVAL: Check governance, log decision, auto-approve (future: UI prompt)."""
|
|
3671
|
+
auto_approve_tools = {name for name, p in TOOL_GOVERNANCE.items() if p["auto_approve"]}
|
|
3672
|
+
steps = ctx.plan.get("steps", [])
|
|
3673
|
+
non_auto = [s.get("action") for s in steps if s.get("action") not in auto_approve_tools]
|
|
3674
|
+
requires = ctx.plan.get("requires_approval", False) or bool(non_auto)
|
|
3675
|
+
|
|
3676
|
+
ctx.transcript.append({
|
|
3677
|
+
"state": AgentState.WAITING_APPROVAL.value,
|
|
3678
|
+
"requires_approval": requires,
|
|
3679
|
+
"non_auto_approve_steps": non_auto,
|
|
3680
|
+
"decision": "auto_approved",
|
|
3681
|
+
})
|
|
3682
|
+
append_audit_event(
|
|
3683
|
+
"agent_approval", user_email=current_user,
|
|
3684
|
+
requires_approval=requires, non_auto_steps=non_auto, decision="auto_approved",
|
|
3685
|
+
)
|
|
3686
|
+
ctx.state = AgentState.EXECUTING
|
|
3687
|
+
|
|
3688
|
+
|
|
3689
|
+
async def _phase_execute(
|
|
3690
|
+
ctx: AgentRunContext, req: AgentRequest, router, lang_hint: str,
|
|
3691
|
+
current_user: str, max_steps: int,
|
|
3692
|
+
) -> None:
|
|
3693
|
+
"""EXECUTE: Executor role calls tools one at a time until final or budget exhausted."""
|
|
3694
|
+
exec_count = sum(1 for s in ctx.transcript if s.get("state") == AgentState.EXECUTING.value)
|
|
3695
|
+
budget = max(1, max_steps - exec_count)
|
|
3696
|
+
|
|
3697
|
+
for _ in range(budget):
|
|
3698
|
+
corrections_hint = (
|
|
3699
|
+
"\n\nCritic corrections from previous attempt:\n"
|
|
3700
|
+
+ "\n".join(f"- {c}" for c in ctx.corrections)
|
|
3701
|
+
) if ctx.corrections else ""
|
|
3266
3702
|
|
|
3267
|
-
for step in range(max_steps):
|
|
3268
|
-
recent_context = build_recent_chat_context(conversation_id=req.conversation_id)
|
|
3269
3703
|
context = (
|
|
3270
|
-
f"{
|
|
3271
|
-
f"[LANGUAGE: {lang_hint}]\n
|
|
3704
|
+
f"{EXECUTOR_PROMPT}\n\n"
|
|
3705
|
+
f"[LANGUAGE HINT: {lang_hint}]\n"
|
|
3272
3706
|
f"Workspace root: {AGENT_ROOT}\n\n"
|
|
3273
|
-
f"
|
|
3274
|
-
f"
|
|
3275
|
-
f"
|
|
3707
|
+
f"PLAN:\n{json.dumps(ctx.plan, ensure_ascii=False)}\n\n"
|
|
3708
|
+
f"Recent conversation:\n{build_recent_chat_context(conversation_id=req.conversation_id) or '(none)'}\n\n"
|
|
3709
|
+
f"User request: {req.message}{corrections_hint}\n\n"
|
|
3710
|
+
f"Execution transcript:\n{json.dumps(ctx.transcript, ensure_ascii=False, indent=2)}"
|
|
3276
3711
|
)
|
|
3277
3712
|
raw = await router.generate(
|
|
3278
|
-
message="
|
|
3279
|
-
context=context,
|
|
3280
|
-
max_tokens=4096,
|
|
3281
|
-
temperature=req.temperature,
|
|
3713
|
+
message="Execute the next step.",
|
|
3714
|
+
context=context, max_tokens=4096, temperature=req.temperature,
|
|
3282
3715
|
)
|
|
3283
|
-
|
|
3284
3716
|
try:
|
|
3285
3717
|
action = _extract_agent_action(str(raw))
|
|
3286
3718
|
except ValueError as exc:
|
|
3287
|
-
transcript.append({
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
"steps": transcript,
|
|
3297
|
-
"created_files": created_files,
|
|
3298
|
-
}
|
|
3719
|
+
ctx.transcript.append({
|
|
3720
|
+
"state": AgentState.EXECUTING.value, "action": "parse_error",
|
|
3721
|
+
"raw": str(raw)[:400], "error": str(exc),
|
|
3722
|
+
})
|
|
3723
|
+
break
|
|
3724
|
+
|
|
3725
|
+
name = action.get("action")
|
|
3726
|
+
thoughts = str(action.get("thoughts") or "")[:600]
|
|
3727
|
+
args = action.get("args") or {}
|
|
3299
3728
|
|
|
3300
|
-
name = action.get("action")
|
|
3301
3729
|
if name == "final":
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3730
|
+
ctx.final_message = action.get("message", "작업을 완료했습니다.")
|
|
3731
|
+
ctx.transcript.append({
|
|
3732
|
+
"state": AgentState.EXECUTING.value, "action": "final", "thoughts": thoughts,
|
|
3733
|
+
})
|
|
3734
|
+
ctx.state = AgentState.VERIFYING
|
|
3735
|
+
return
|
|
3736
|
+
|
|
3737
|
+
# Loop guard
|
|
3738
|
+
exec_steps = [s for s in ctx.transcript if s.get("state") == AgentState.EXECUTING.value]
|
|
3739
|
+
last = exec_steps[-1] if exec_steps else None
|
|
3311
3740
|
if (
|
|
3312
|
-
name in _FILE_CREATE_ACTIONS
|
|
3313
|
-
and
|
|
3314
|
-
and
|
|
3315
|
-
and
|
|
3316
|
-
and "result" in last_step
|
|
3741
|
+
name in _FILE_CREATE_ACTIONS and last
|
|
3742
|
+
and last.get("action") == name
|
|
3743
|
+
and (last.get("args") or {}) == args
|
|
3744
|
+
and "result" in last
|
|
3317
3745
|
):
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3746
|
+
ctx.transcript.append({
|
|
3747
|
+
"state": AgentState.EXECUTING.value, "action": name,
|
|
3748
|
+
"error": "LOOP_DETECTED: identical action+args repeated — halted.",
|
|
3749
|
+
})
|
|
3750
|
+
break
|
|
3323
3751
|
|
|
3324
3752
|
if name == "clear_history":
|
|
3325
|
-
result = clear_history(
|
|
3326
|
-
|
|
3327
|
-
"
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
keep_last=current_args.get("keep_last", 0),
|
|
3331
|
-
removed=result.get("removed", 0),
|
|
3332
|
-
kept=result.get("kept", 0),
|
|
3333
|
-
)
|
|
3334
|
-
transcript.append({"step": step + 1, "action": name, "args": current_args, "result": result})
|
|
3753
|
+
result = clear_history(args.get("keep_last", 0))
|
|
3754
|
+
ctx.transcript.append({
|
|
3755
|
+
"state": AgentState.EXECUTING.value, "action": name,
|
|
3756
|
+
"thoughts": thoughts, "args": args, "result": result,
|
|
3757
|
+
})
|
|
3335
3758
|
continue
|
|
3336
3759
|
|
|
3337
|
-
|
|
3760
|
+
policy = _agent_policy(name, args)
|
|
3761
|
+
risk = _RISK_LEVEL_MAP.get(policy["risk"], "medium")
|
|
3338
3762
|
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
continue
|
|
3763
|
+
if policy["risk"] == "destructive":
|
|
3764
|
+
ctx.transcript.append({
|
|
3765
|
+
"state": AgentState.EXECUTING.value, "action": name,
|
|
3766
|
+
"thoughts": thoughts, "args": args, "risk": risk,
|
|
3767
|
+
"governance": dict(policy),
|
|
3768
|
+
"error": f"BLOCKED: destructive action '{name}' not permitted in agent mode.",
|
|
3769
|
+
})
|
|
3770
|
+
append_audit_event(
|
|
3771
|
+
"agent_blocked", user_email=current_user, source=req.source or "agent",
|
|
3772
|
+
action=name, reason="destructive", governance=dict(policy),
|
|
3773
|
+
)
|
|
3774
|
+
continue
|
|
3352
3775
|
|
|
3353
|
-
|
|
3354
|
-
if risk in ("medium", "high"):
|
|
3776
|
+
if not policy["auto_approve"]:
|
|
3355
3777
|
append_audit_event(
|
|
3356
3778
|
"agent_exec", user_email=current_user, source=req.source or "agent",
|
|
3357
|
-
|
|
3358
|
-
|
|
3779
|
+
state=AgentState.EXECUTING.value, action=name, risk=risk,
|
|
3780
|
+
shell=policy["shell"], network=policy["network"],
|
|
3781
|
+
destructive=policy["destructive"], sandbox=policy["sandbox"],
|
|
3782
|
+
rollback=policy["rollback"],
|
|
3783
|
+
args={k: v for k, v in args.items() if k != "content"},
|
|
3359
3784
|
)
|
|
3360
3785
|
|
|
3361
3786
|
try:
|
|
3362
|
-
result = execute_tool(name,
|
|
3363
|
-
transcript.append({
|
|
3787
|
+
result = execute_tool(name, args)
|
|
3788
|
+
ctx.transcript.append({
|
|
3789
|
+
"state": AgentState.EXECUTING.value, "action": name,
|
|
3790
|
+
"thoughts": thoughts, "args": args,
|
|
3791
|
+
"risk": risk, "governance": dict(policy), "result": result,
|
|
3792
|
+
})
|
|
3364
3793
|
except (ToolError, KeyError, TypeError) as exc:
|
|
3365
|
-
transcript.append({
|
|
3794
|
+
ctx.transcript.append({
|
|
3795
|
+
"state": AgentState.EXECUTING.value, "action": name,
|
|
3796
|
+
"thoughts": thoughts, "args": args,
|
|
3797
|
+
"risk": risk, "governance": dict(policy), "error": str(exc),
|
|
3798
|
+
})
|
|
3366
3799
|
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3800
|
+
ctx.state = AgentState.VERIFYING
|
|
3801
|
+
|
|
3802
|
+
|
|
3803
|
+
async def _phase_verify(
|
|
3804
|
+
ctx: AgentRunContext, req: AgentRequest, router, lang_hint: str, current_user: str,
|
|
3805
|
+
max_retry: int = 3,
|
|
3806
|
+
) -> None:
|
|
3807
|
+
"""VERIFYING: Critic role evaluates transcript → DONE / EXECUTING (retry) / ROLLBACK / FAILED."""
|
|
3808
|
+
context = (
|
|
3809
|
+
f"{CRITIC_PROMPT}\n\n"
|
|
3810
|
+
f"[LANGUAGE HINT: {lang_hint}]\n\n"
|
|
3811
|
+
f"Original request: {req.message}\n"
|
|
3812
|
+
f"Plan goal: {ctx.plan.get('goal', req.message)}\n\n"
|
|
3813
|
+
f"Full transcript:\n{json.dumps(ctx.transcript, ensure_ascii=False, indent=2)}"
|
|
3372
3814
|
)
|
|
3373
|
-
|
|
3374
|
-
message=
|
|
3375
|
-
context=
|
|
3376
|
-
max_tokens=1024,
|
|
3377
|
-
temperature=0.1,
|
|
3815
|
+
raw = await router.generate(
|
|
3816
|
+
message="Review the execution transcript and return your verdict JSON.",
|
|
3817
|
+
context=context, max_tokens=512, temperature=0.1,
|
|
3378
3818
|
)
|
|
3379
3819
|
try:
|
|
3380
|
-
|
|
3381
|
-
message = final_action.get("message", str(summary))
|
|
3820
|
+
verdict = _extract_agent_action(str(raw))
|
|
3382
3821
|
except ValueError:
|
|
3383
|
-
|
|
3822
|
+
verdict = {"action": "verdict", "verdict": "PASS", "next_state": "DONE",
|
|
3823
|
+
"reason": "Critic parse failed — assuming pass.", "corrections": [], "confidence": 0.7}
|
|
3824
|
+
|
|
3825
|
+
ctx.corrections = verdict.get("corrections", [])
|
|
3826
|
+
# Normalize legacy verdict next_state strings to current AgentState names
|
|
3827
|
+
raw_next = verdict.get("next_state", "DONE")
|
|
3828
|
+
next_s = {"COMPLETE": "DONE", "RETRY": "EXECUTING"}.get(raw_next, raw_next)
|
|
3829
|
+
|
|
3830
|
+
ctx.transcript.append({
|
|
3831
|
+
"state": AgentState.VERIFYING.value,
|
|
3832
|
+
"verdict": verdict.get("verdict", "PASS"),
|
|
3833
|
+
"reason": verdict.get("reason", ""),
|
|
3834
|
+
"corrections": ctx.corrections,
|
|
3835
|
+
"confidence": verdict.get("confidence", 0.9),
|
|
3836
|
+
"next_state": next_s,
|
|
3837
|
+
})
|
|
3838
|
+
|
|
3839
|
+
if verdict.get("verdict") == "PASS" or next_s == "DONE":
|
|
3840
|
+
if not ctx.final_message:
|
|
3841
|
+
ctx.final_message = verdict.get("reason", "작업이 완료되었습니다.")
|
|
3842
|
+
ctx.state = AgentState.DONE
|
|
3843
|
+
elif next_s == "ROLLBACK":
|
|
3844
|
+
ctx.state = AgentState.ROLLBACK
|
|
3845
|
+
elif next_s == "EXECUTING":
|
|
3846
|
+
if ctx.retry_count >= max_retry:
|
|
3847
|
+
ctx.final_message = (
|
|
3848
|
+
f"최대 재시도({max_retry}회) 초과로 작업을 종료했습니다. "
|
|
3849
|
+
f"마지막 비판: {verdict.get('reason', '(없음)')}"
|
|
3850
|
+
)
|
|
3851
|
+
ctx.state = AgentState.FAILED
|
|
3852
|
+
else:
|
|
3853
|
+
ctx.retry_count += 1
|
|
3854
|
+
ctx.transcript.append({
|
|
3855
|
+
"state": AgentState.EXECUTING.value,
|
|
3856
|
+
"retry_attempt": ctx.retry_count,
|
|
3857
|
+
"corrections": ctx.corrections,
|
|
3858
|
+
})
|
|
3859
|
+
ctx.state = AgentState.EXECUTING
|
|
3860
|
+
else:
|
|
3861
|
+
ctx.final_message = verdict.get("reason", "검증자가 인식되지 않은 다음 상태를 반환했습니다.")
|
|
3862
|
+
ctx.state = AgentState.FAILED
|
|
3863
|
+
|
|
3864
|
+
|
|
3865
|
+
def _phase_rollback(ctx: AgentRunContext, current_user: str) -> None:
|
|
3866
|
+
"""ROLLBACK: attempt git checkout for each edited file, then COMPLETE."""
|
|
3867
|
+
import subprocess as _sp
|
|
3868
|
+
rolled: list = []
|
|
3869
|
+
for step in ctx.transcript:
|
|
3870
|
+
if step.get("state") != AgentState.EXECUTING.value:
|
|
3871
|
+
continue
|
|
3872
|
+
gov = step.get("governance", {})
|
|
3873
|
+
if gov.get("rollback") != "git":
|
|
3874
|
+
continue
|
|
3875
|
+
result = step.get("result", {})
|
|
3876
|
+
if not (isinstance(result, dict) and result.get("success")):
|
|
3877
|
+
continue
|
|
3878
|
+
path = result.get("path") or (step.get("args") or {}).get("path", "")
|
|
3879
|
+
if not path:
|
|
3880
|
+
continue
|
|
3881
|
+
try:
|
|
3882
|
+
r = _sp.run(
|
|
3883
|
+
["git", "checkout", "--", path], cwd=str(AGENT_ROOT),
|
|
3884
|
+
capture_output=True, text=True, timeout=10,
|
|
3885
|
+
)
|
|
3886
|
+
rolled.append({"path": path, "ok": r.returncode == 0, "stderr": r.stderr[:200]})
|
|
3887
|
+
except Exception as exc:
|
|
3888
|
+
rolled.append({"path": path, "ok": False, "error": str(exc)})
|
|
3889
|
+
|
|
3890
|
+
ctx.transcript.append({"state": AgentState.ROLLBACK.value, "rolled_back": rolled})
|
|
3891
|
+
recovered = [r["path"] for r in rolled if r.get("ok")]
|
|
3892
|
+
ctx.final_message = (
|
|
3893
|
+
f"실행 실패로 롤백했습니다. 복구 파일: {recovered}"
|
|
3894
|
+
if recovered
|
|
3895
|
+
else "롤백을 시도했으나 복구할 파일이 없거나 git이 초기화되지 않았습니다."
|
|
3896
|
+
)
|
|
3897
|
+
append_audit_event("agent_rollback", user_email=current_user, rolled_back=rolled)
|
|
3898
|
+
# Rollback is a recovery from a failed verification — terminal state is FAILED
|
|
3899
|
+
ctx.state = AgentState.FAILED
|
|
3900
|
+
|
|
3901
|
+
|
|
3902
|
+
async def _phase_memory_update(
|
|
3903
|
+
ctx: AgentRunContext, req: AgentRequest, router, current_user: str,
|
|
3904
|
+
) -> None:
|
|
3905
|
+
"""Background: Memory Updater role extracts learnings after COMPLETE."""
|
|
3906
|
+
context = (
|
|
3907
|
+
f"{MEMORY_UPDATER_PROMPT}\n\n"
|
|
3908
|
+
f"Completed task: {req.message}\n\n"
|
|
3909
|
+
f"Last 5 transcript steps:\n{json.dumps(ctx.transcript[-5:], ensure_ascii=False)}"
|
|
3910
|
+
)
|
|
3911
|
+
try:
|
|
3912
|
+
raw = await router.generate(
|
|
3913
|
+
message="Extract learnings from this completed task.",
|
|
3914
|
+
context=context, max_tokens=256, temperature=0.1,
|
|
3915
|
+
)
|
|
3916
|
+
mem = _extract_agent_action(str(raw))
|
|
3917
|
+
if mem.get("save_to_knowledge") and mem.get("learnings"):
|
|
3918
|
+
from tools import knowledge_save
|
|
3919
|
+
knowledge_save(
|
|
3920
|
+
"\n".join(mem["learnings"]),
|
|
3921
|
+
folder="30_Projects",
|
|
3922
|
+
title=f"Agent: {req.message[:60]}",
|
|
3923
|
+
)
|
|
3924
|
+
except Exception:
|
|
3925
|
+
pass
|
|
3926
|
+
|
|
3927
|
+
|
|
3928
|
+
# ── Eval harness ──────────────────────────────────────────────────────────────
|
|
3929
|
+
|
|
3930
|
+
@app.post("/agent/eval")
|
|
3931
|
+
async def agent_eval(req: AgentEvalRequest, request: Request):
|
|
3932
|
+
"""Run a skill's eval cases from schema.json and return pass/fail per case."""
|
|
3933
|
+
require_user(request)
|
|
3934
|
+
skill_dir = Path(__file__).resolve().parent / "skills" / req.skill
|
|
3935
|
+
schema_path = skill_dir / "schema.json"
|
|
3936
|
+
if not schema_path.exists():
|
|
3937
|
+
raise HTTPException(404, detail=f"Skill '{req.skill}' not found or missing schema.json")
|
|
3938
|
+
|
|
3939
|
+
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
3940
|
+
eval_cases = schema.get("evals", [])
|
|
3941
|
+
if req.case_id:
|
|
3942
|
+
eval_cases = [c for c in eval_cases if c.get("id") == req.case_id]
|
|
3943
|
+
if not eval_cases:
|
|
3944
|
+
return {"skill": req.skill, "total": 0, "passed": 0, "failed": 0, "results": [],
|
|
3945
|
+
"message": "No eval cases defined in schema.json"}
|
|
3946
|
+
|
|
3947
|
+
action_name = schema.get("action", req.skill)
|
|
3948
|
+
results = []
|
|
3949
|
+
for case in eval_cases:
|
|
3950
|
+
case_id = case.get("id", "?")
|
|
3951
|
+
try:
|
|
3952
|
+
result = execute_tool(action_name, case.get("input", {}))
|
|
3953
|
+
criteria = case.get("pass_criteria", "")
|
|
3954
|
+
if "success == true" in criteria:
|
|
3955
|
+
passed = result.get("success") is True
|
|
3956
|
+
elif "success == false" in criteria:
|
|
3957
|
+
passed = result.get("success") is False
|
|
3958
|
+
else:
|
|
3959
|
+
passed = True # manual review required
|
|
3960
|
+
results.append({"id": case_id, "description": case.get("description", ""),
|
|
3961
|
+
"passed": passed, "result": result, "pass_criteria": criteria})
|
|
3962
|
+
except Exception as exc:
|
|
3963
|
+
results.append({"id": case_id, "description": case.get("description", ""),
|
|
3964
|
+
"passed": False, "error": str(exc),
|
|
3965
|
+
"pass_criteria": case.get("pass_criteria", "")})
|
|
3966
|
+
|
|
3967
|
+
n_passed = sum(1 for r in results if r.get("passed") is True)
|
|
3968
|
+
return {
|
|
3969
|
+
"skill": req.skill, "action": action_name,
|
|
3970
|
+
"total": len(results), "passed": n_passed, "failed": len(results) - n_passed,
|
|
3971
|
+
"results": results,
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3974
|
+
|
|
3975
|
+
@app.post("/agent")
|
|
3976
|
+
async def agent(req: AgentRequest, request: Request):
|
|
3977
|
+
"""Natural-language local agent.
|
|
3978
|
+
|
|
3979
|
+
State machine:
|
|
3980
|
+
IDLE → PLANNING → WAITING_APPROVAL → EXECUTING → VERIFYING
|
|
3981
|
+
↓ ↓
|
|
3982
|
+
FAILED DONE | EXECUTING(retry) | ROLLBACK
|
|
3983
|
+
↓
|
|
3984
|
+
FAILED
|
|
3985
|
+
"""
|
|
3986
|
+
current_user = require_user(request)
|
|
3987
|
+
enforce_rate_limit(current_user, "agent")
|
|
3988
|
+
if not router.current_model_id:
|
|
3989
|
+
raise HTTPException(status_code=400, detail="No model loaded. Call /models/load first.")
|
|
3990
|
+
|
|
3991
|
+
ensure_agent_root()
|
|
3992
|
+
lang = detect_language(req.message)
|
|
3993
|
+
lang_hint = _LANG_HINT[lang]
|
|
3994
|
+
max_steps = max(1, min(req.max_steps, 50))
|
|
3995
|
+
max_retry = 3
|
|
3384
3996
|
|
|
3997
|
+
ctx = AgentRunContext()
|
|
3998
|
+
|
|
3999
|
+
while ctx.state not in AGENT_TERMINAL_STATES:
|
|
4000
|
+
ctx.state_history.append(ctx.state.value)
|
|
4001
|
+
# Hard guard against infinite state loops
|
|
4002
|
+
if len(ctx.state_history) > 200:
|
|
4003
|
+
ctx.final_message = "에이전트 상태 머신이 최대 반복(200)에 도달해 중단했습니다."
|
|
4004
|
+
ctx.state = AgentState.FAILED
|
|
4005
|
+
break
|
|
4006
|
+
|
|
4007
|
+
if ctx.state == AgentState.IDLE:
|
|
4008
|
+
ctx.state = AgentState.PLANNING
|
|
4009
|
+
|
|
4010
|
+
elif ctx.state == AgentState.PLANNING:
|
|
4011
|
+
await _phase_plan(ctx, req, router, lang_hint, current_user)
|
|
4012
|
+
|
|
4013
|
+
elif ctx.state == AgentState.WAITING_APPROVAL:
|
|
4014
|
+
_phase_approval(ctx, current_user)
|
|
4015
|
+
|
|
4016
|
+
elif ctx.state == AgentState.EXECUTING:
|
|
4017
|
+
await _phase_execute(ctx, req, router, lang_hint, current_user, max_steps)
|
|
4018
|
+
|
|
4019
|
+
elif ctx.state == AgentState.VERIFYING:
|
|
4020
|
+
await _phase_verify(ctx, req, router, lang_hint, current_user, max_retry)
|
|
4021
|
+
|
|
4022
|
+
elif ctx.state == AgentState.ROLLBACK:
|
|
4023
|
+
_phase_rollback(ctx, current_user)
|
|
4024
|
+
|
|
4025
|
+
else:
|
|
4026
|
+
ctx.state = AgentState.FAILED
|
|
4027
|
+
|
|
4028
|
+
# Record terminal state in history for clients
|
|
4029
|
+
ctx.state_history.append(ctx.state.value)
|
|
4030
|
+
|
|
4031
|
+
# Fire-and-forget memory update — does not block the response
|
|
4032
|
+
asyncio.create_task(_phase_memory_update(ctx, req, router, current_user))
|
|
4033
|
+
|
|
4034
|
+
message = ctx.final_message or "작업을 완료했습니다."
|
|
3385
4035
|
save_to_history("user", req.message, source=req.source or "web", conversation_id=req.conversation_id)
|
|
3386
4036
|
save_to_history("assistant", message, source=req.source or "web", conversation_id=req.conversation_id)
|
|
3387
|
-
created_files = _collect_created_files(transcript)
|
|
3388
|
-
return {
|
|
4037
|
+
created_files = _collect_created_files(ctx.transcript)
|
|
4038
|
+
return {
|
|
4039
|
+
"status": "ok" if ctx.state == AgentState.DONE else "failed",
|
|
4040
|
+
"response": message,
|
|
4041
|
+
"workspace": str(AGENT_ROOT),
|
|
4042
|
+
"steps": ctx.transcript,
|
|
4043
|
+
"state_history": ctx.state_history,
|
|
4044
|
+
"final_state": ctx.state.value,
|
|
4045
|
+
"created_files": created_files,
|
|
4046
|
+
}
|
|
3389
4047
|
|
|
3390
4048
|
|
|
3391
4049
|
# ── Direct Tool API ───────────────────────────────────────────────────────────
|
|
@@ -3410,9 +4068,13 @@ async def tools_workspace_tree(req: ToolWorkspaceTreeRequest, request: Request):
|
|
|
3410
4068
|
|
|
3411
4069
|
|
|
3412
4070
|
@app.post("/tools/read_file")
|
|
3413
|
-
async def tools_read_file(req:
|
|
4071
|
+
async def tools_read_file(req: ToolReadFileRequest, request: Request):
|
|
3414
4072
|
require_user(request)
|
|
3415
|
-
|
|
4073
|
+
try:
|
|
4074
|
+
return {"status": "ok", "workspace": str(AGENT_ROOT),
|
|
4075
|
+
"result": read_file(req.path, offset=req.offset, limit=req.limit, line_numbers=req.line_numbers)}
|
|
4076
|
+
except ToolError as exc:
|
|
4077
|
+
raise HTTPException(status_code=400, detail=str(exc))
|
|
3416
4078
|
|
|
3417
4079
|
|
|
3418
4080
|
@app.post("/tools/write_file")
|
|
@@ -3421,12 +4083,51 @@ async def tools_write_file(req: ToolWriteFileRequest, request: Request):
|
|
|
3421
4083
|
return _tool_response(write_file, req.path, req.content)
|
|
3422
4084
|
|
|
3423
4085
|
|
|
4086
|
+
@app.post("/tools/edit_file")
|
|
4087
|
+
async def tools_edit_file(req: ToolEditFileRequest, request: Request):
|
|
4088
|
+
require_user(request)
|
|
4089
|
+
try:
|
|
4090
|
+
return {"status": "ok", "workspace": str(AGENT_ROOT),
|
|
4091
|
+
"result": edit_file(req.path, req.old_string, req.new_string, replace_all=req.replace_all)}
|
|
4092
|
+
except ToolError as exc:
|
|
4093
|
+
raise HTTPException(status_code=400, detail=str(exc))
|
|
4094
|
+
|
|
4095
|
+
|
|
3424
4096
|
@app.post("/tools/search_files")
|
|
3425
4097
|
async def tools_search_files(req: ToolSearchFilesRequest, request: Request):
|
|
3426
4098
|
require_user(request)
|
|
3427
4099
|
return _tool_response(search_files, req.query, req.path, req.max_results)
|
|
3428
4100
|
|
|
3429
4101
|
|
|
4102
|
+
@app.post("/tools/grep")
|
|
4103
|
+
async def tools_grep(req: ToolGrepRequest, request: Request):
|
|
4104
|
+
require_user(request)
|
|
4105
|
+
try:
|
|
4106
|
+
return {"status": "ok", "workspace": str(AGENT_ROOT),
|
|
4107
|
+
"result": grep(
|
|
4108
|
+
req.pattern,
|
|
4109
|
+
path=req.path,
|
|
4110
|
+
glob=req.glob,
|
|
4111
|
+
max_results=req.max_results,
|
|
4112
|
+
case_insensitive=req.case_insensitive,
|
|
4113
|
+
context_lines=req.context_lines,
|
|
4114
|
+
)}
|
|
4115
|
+
except ToolError as exc:
|
|
4116
|
+
raise HTTPException(status_code=400, detail=str(exc))
|
|
4117
|
+
|
|
4118
|
+
|
|
4119
|
+
@app.post("/tools/todo_read")
|
|
4120
|
+
async def tools_todo_read(request: Request):
|
|
4121
|
+
require_user(request)
|
|
4122
|
+
return _tool_response(todo_read)
|
|
4123
|
+
|
|
4124
|
+
|
|
4125
|
+
@app.post("/tools/todo_write")
|
|
4126
|
+
async def tools_todo_write(req: ToolTodoWriteRequest, request: Request):
|
|
4127
|
+
require_user(request)
|
|
4128
|
+
return _tool_response(todo_write, req.todos)
|
|
4129
|
+
|
|
4130
|
+
|
|
3430
4131
|
@app.post("/tools/clear_history")
|
|
3431
4132
|
async def tools_clear_history(req: ToolClearHistoryRequest, request: Request):
|
|
3432
4133
|
current_user = require_user(request)
|
|
@@ -4003,61 +4704,96 @@ async def tools_deploy_project(req: ToolScriptRequest, request: Request):
|
|
|
4003
4704
|
return _tool_response(deploy_project, req.cwd, req.script)
|
|
4004
4705
|
|
|
4005
4706
|
|
|
4707
|
+
_MCP_TOOL_DESCRIPTIONS: Dict[str, str] = {
|
|
4708
|
+
"list_dir": "List files in the agent workspace.",
|
|
4709
|
+
"workspace_tree": "Return a recursive workspace tree.",
|
|
4710
|
+
"read_file": "Read a UTF-8 file from the workspace with optional line numbers and offset/limit slicing.",
|
|
4711
|
+
"write_file": "Write a UTF-8 file inside the workspace (new files / full rewrites).",
|
|
4712
|
+
"edit_file": "Precise diff-style edit: replace exact old_string with new_string. Requires unique match unless replace_all=true.",
|
|
4713
|
+
"search_files": "Substring search in text files (legacy).",
|
|
4714
|
+
"grep": "Regex search across the workspace with line numbers and optional context.",
|
|
4715
|
+
"todo_read": "Read the agent's persistent TODO list for the current workspace.",
|
|
4716
|
+
"todo_write": "Replace the agent's TODO list (id, content, status: pending/in_progress/completed).",
|
|
4717
|
+
"clear_history": "Clear chat history to reduce context and speed up responses.",
|
|
4718
|
+
"inspect_html": "Inspect local HTML structure and assets.",
|
|
4719
|
+
"preview_url": "Return a server URL for a workspace file.",
|
|
4720
|
+
"create_docx": "Create a Word DOCX document in the agent workspace.",
|
|
4721
|
+
"create_xlsx": "Create an XLSX spreadsheet in the agent workspace.",
|
|
4722
|
+
"create_pptx": "Create a PPTX presentation deck in the agent workspace.",
|
|
4723
|
+
"create_pdf": "Create a PDF document in the agent workspace.",
|
|
4724
|
+
"local_list": "List any local folder (requires user permission via UI).",
|
|
4725
|
+
"local_read": "Read any local file (requires user permission via UI).",
|
|
4726
|
+
"local_write": "Write any local file (requires user permission via UI).",
|
|
4727
|
+
"read_document": "Extract text from PDF, DOCX, XLSX, PPTX, TXT, MD, CSV files.",
|
|
4728
|
+
"computer_screenshot": "Capture the current Mac screen as base64 PNG.",
|
|
4729
|
+
"computer_open_app": "Open or focus a Mac app, e.g. Google Chrome.",
|
|
4730
|
+
"computer_open_url": "Open a URL in a Mac app, e.g. Google Chrome.",
|
|
4731
|
+
"computer_click": "Click at screen coordinates (x, y).",
|
|
4732
|
+
"computer_type": "Type text at the current focus position.",
|
|
4733
|
+
"computer_key": "Press a keyboard key or shortcut (e.g. 'command+c').",
|
|
4734
|
+
"computer_scroll": "Scroll at screen coordinates.",
|
|
4735
|
+
"computer_move": "Move the mouse to screen coordinates.",
|
|
4736
|
+
"computer_drag": "Drag from (x1,y1) to (x2,y2).",
|
|
4737
|
+
"computer_status": "Check if Mac Computer Use (pyautogui) is available.",
|
|
4738
|
+
"chrome_status": "Report Chrome desktop bridge availability.",
|
|
4739
|
+
"computer_use_status": "Report Mac Computer Use bridge availability.",
|
|
4740
|
+
"knowledge_save": "Save a note into the local knowledge garden.",
|
|
4741
|
+
"knowledge_search": "Search the local knowledge garden.",
|
|
4742
|
+
"knowledge_tree": "List local knowledge garden markdown files.",
|
|
4743
|
+
"knowledge_graph_ingest":"Ingest a message, AI answer, or connector event into the SQLite knowledge graph.",
|
|
4744
|
+
"knowledge_graph_search":"Search graph nodes, summaries, and JSON metadata.",
|
|
4745
|
+
"knowledge_graph_graph": "Return Obsidian-style graph nodes and edges.",
|
|
4746
|
+
"knowledge_graph_context":"Return compact graph-backed RAG context for a prompt.",
|
|
4747
|
+
"obsidian_save": "Save a note into the Obsidian-compatible memory vault.",
|
|
4748
|
+
"obsidian_search": "Search the Obsidian-compatible memory vault.",
|
|
4749
|
+
"obsidian_tree": "List Obsidian memory vault markdown files.",
|
|
4750
|
+
"git_status": "Read-only local git status inside the workspace.",
|
|
4751
|
+
"git_diff": "Read-only local git diff inside the workspace.",
|
|
4752
|
+
"git_log": "Read-only local git log inside the workspace.",
|
|
4753
|
+
"git_show": "Read-only local git show --stat inside the workspace.",
|
|
4754
|
+
"network_status": "Get current local/private IP, public IP, hostname, and Wi-Fi info.",
|
|
4755
|
+
"run_command": "Run an allowlisted local command inside the workspace.",
|
|
4756
|
+
"build_project": "Run an allowlisted package.json build/compile/typecheck/test script to verify changes actually work.",
|
|
4757
|
+
"deploy_project": "Run an allowlisted package.json deploy/preview/release/package installer script (pkg/exe).",
|
|
4758
|
+
}
|
|
4759
|
+
|
|
4760
|
+
|
|
4761
|
+
@app.get("/tools/permissions")
|
|
4762
|
+
async def tools_permissions(request: Request):
|
|
4763
|
+
"""Compact tool permission view (tool / risk / requires_approval / network).
|
|
4764
|
+
|
|
4765
|
+
A simpler authorization-layer summary derived from TOOL_GOVERNANCE.
|
|
4766
|
+
Use /mcp/tools for the full 7-dimensional governance object.
|
|
4767
|
+
"""
|
|
4768
|
+
require_user(request)
|
|
4769
|
+
return {"status": "ok", "permissions": list_tool_permissions()}
|
|
4770
|
+
|
|
4771
|
+
|
|
4006
4772
|
@app.get("/mcp/tools")
|
|
4007
4773
|
async def mcp_tools():
|
|
4008
4774
|
installed = load_mcp_installs().get("installed", {})
|
|
4775
|
+
tools = []
|
|
4776
|
+
for name, description in _MCP_TOOL_DESCRIPTIONS.items():
|
|
4777
|
+
policy = TOOL_GOVERNANCE.get(name, _TOOL_GOVERNANCE_DEFAULT)
|
|
4778
|
+
tools.append({
|
|
4779
|
+
"name": name,
|
|
4780
|
+
"description": description,
|
|
4781
|
+
"permission": get_tool_permission(name),
|
|
4782
|
+
"governance": {
|
|
4783
|
+
"risk": policy["risk"],
|
|
4784
|
+
"destructive": policy["destructive"],
|
|
4785
|
+
"shell": policy["shell"],
|
|
4786
|
+
"network": policy["network"],
|
|
4787
|
+
"auto_approve": policy["auto_approve"],
|
|
4788
|
+
"sandbox": policy["sandbox"],
|
|
4789
|
+
"rollback": policy["rollback"],
|
|
4790
|
+
},
|
|
4791
|
+
})
|
|
4009
4792
|
return {
|
|
4010
4793
|
"status": "ok",
|
|
4011
4794
|
"workspace": str(AGENT_ROOT),
|
|
4012
4795
|
"installed_mcps": [mcp_public_item(item, installed) for item in MCP_REGISTRY],
|
|
4013
|
-
"tools":
|
|
4014
|
-
{"name": "list_dir", "description": "List files in the agent workspace."},
|
|
4015
|
-
{"name": "workspace_tree", "description": "Return a recursive workspace tree."},
|
|
4016
|
-
{"name": "read_file", "description": "Read a UTF-8 file from the workspace."},
|
|
4017
|
-
{"name": "write_file", "description": "Write a UTF-8 file inside the workspace."},
|
|
4018
|
-
{"name": "search_files", "description": "Search text files inside the workspace."},
|
|
4019
|
-
{"name": "clear_history", "description": "Clear chat history to reduce context and speed up responses."},
|
|
4020
|
-
{"name": "inspect_html", "description": "Inspect local HTML structure and assets."},
|
|
4021
|
-
{"name": "preview_url", "description": "Return a server URL for a workspace file."},
|
|
4022
|
-
{"name": "create_docx", "description": "Create a Word DOCX document in the agent workspace."},
|
|
4023
|
-
{"name": "create_xlsx", "description": "Create an XLSX spreadsheet in the agent workspace."},
|
|
4024
|
-
{"name": "create_pptx", "description": "Create a PPTX presentation deck in the agent workspace."},
|
|
4025
|
-
{"name": "create_pdf", "description": "Create a PDF document in the agent workspace."},
|
|
4026
|
-
{"name": "local_list", "description": "List any local folder (requires user permission via UI)."},
|
|
4027
|
-
{"name": "local_read", "description": "Read any local file (requires user permission via UI)."},
|
|
4028
|
-
{"name": "local_write", "description": "Write any local file (requires user permission via UI)."},
|
|
4029
|
-
{"name": "read_document", "description": "Extract text from PDF, DOCX, XLSX, PPTX, TXT, MD, CSV files."},
|
|
4030
|
-
{"name": "computer_screenshot", "description": "Capture the current Mac screen as base64 PNG."},
|
|
4031
|
-
{"name": "computer_open_app", "description": "Open or focus a Mac app, e.g. Google Chrome."},
|
|
4032
|
-
{"name": "computer_open_url", "description": "Open a URL in a Mac app, e.g. Google Chrome."},
|
|
4033
|
-
{"name": "computer_click", "description": "Click at screen coordinates (x, y)."},
|
|
4034
|
-
{"name": "computer_type", "description": "Type text at the current focus position."},
|
|
4035
|
-
{"name": "computer_key", "description": "Press a keyboard key or shortcut (e.g. 'command+c')."},
|
|
4036
|
-
{"name": "computer_scroll", "description": "Scroll at screen coordinates."},
|
|
4037
|
-
{"name": "computer_move", "description": "Move the mouse to screen coordinates."},
|
|
4038
|
-
{"name": "computer_drag", "description": "Drag from (x1,y1) to (x2,y2)."},
|
|
4039
|
-
{"name": "computer_status", "description": "Check if Mac Computer Use (pyautogui) is available."},
|
|
4040
|
-
{"name": "chrome_status", "description": "Report Chrome desktop bridge availability."},
|
|
4041
|
-
{"name": "computer_use_status", "description": "Report Mac Computer Use bridge availability."},
|
|
4042
|
-
{"name": "knowledge_save", "description": "Save a note into the local knowledge garden."},
|
|
4043
|
-
{"name": "knowledge_search", "description": "Search the local knowledge garden."},
|
|
4044
|
-
{"name": "knowledge_tree", "description": "List local knowledge garden markdown files."},
|
|
4045
|
-
{"name": "knowledge_graph_ingest", "description": "Ingest a message, AI answer, or connector event into the SQLite knowledge graph."},
|
|
4046
|
-
{"name": "knowledge_graph_search", "description": "Search graph nodes, summaries, and JSON metadata."},
|
|
4047
|
-
{"name": "knowledge_graph_graph", "description": "Return Obsidian-style graph nodes and edges."},
|
|
4048
|
-
{"name": "knowledge_graph_context", "description": "Return compact graph-backed RAG context for a prompt."},
|
|
4049
|
-
{"name": "obsidian_save", "description": "Save a note into the Obsidian-compatible memory vault."},
|
|
4050
|
-
{"name": "obsidian_search", "description": "Search the Obsidian-compatible memory vault."},
|
|
4051
|
-
{"name": "obsidian_tree", "description": "List Obsidian memory vault markdown files."},
|
|
4052
|
-
{"name": "git_status", "description": "Read-only local git status inside the workspace."},
|
|
4053
|
-
{"name": "git_diff", "description": "Read-only local git diff inside the workspace."},
|
|
4054
|
-
{"name": "git_log", "description": "Read-only local git log inside the workspace."},
|
|
4055
|
-
{"name": "git_show", "description": "Read-only local git show --stat inside the workspace."},
|
|
4056
|
-
{"name": "network_status", "description": "Get current local/private IP, public IP, hostname, and Wi-Fi info."},
|
|
4057
|
-
{"name": "run_command", "description": "Run an allowlisted local command inside the workspace."},
|
|
4058
|
-
{"name": "build_project", "description": "Run an allowlisted package.json build/compile/typecheck/test script."},
|
|
4059
|
-
{"name": "deploy_project", "description": "Run an allowlisted package.json deploy/preview/release/package installer script (pkg/exe)."},
|
|
4060
|
-
],
|
|
4796
|
+
"tools": tools,
|
|
4061
4797
|
}
|
|
4062
4798
|
|
|
4063
4799
|
|