process-gpt-agent-sdk 0.3.10__tar.gz → 0.3.12__tar.gz
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.
Potentially problematic release.
This version of process-gpt-agent-sdk might be problematic. Click here for more details.
- process_gpt_agent_sdk-0.3.12/PKG-INFO +410 -0
- process_gpt_agent_sdk-0.3.12/README.md +386 -0
- process_gpt_agent_sdk-0.3.12/process_gpt_agent_sdk.egg-info/PKG-INFO +410 -0
- {process_gpt_agent_sdk-0.3.10 → process_gpt_agent_sdk-0.3.12}/process_gpt_agent_sdk.egg-info/SOURCES.txt +1 -0
- process_gpt_agent_sdk-0.3.12/processgpt_agent_sdk/database.py +537 -0
- {process_gpt_agent_sdk-0.3.10 → process_gpt_agent_sdk-0.3.12}/processgpt_agent_sdk/processgpt_agent_framework.py +1 -1
- {process_gpt_agent_sdk-0.3.10 → process_gpt_agent_sdk-0.3.12}/pyproject.toml +1 -1
- process_gpt_agent_sdk-0.3.10/PKG-INFO +0 -336
- process_gpt_agent_sdk-0.3.10/README.md +0 -312
- process_gpt_agent_sdk-0.3.10/process_gpt_agent_sdk.egg-info/PKG-INFO +0 -336
- {process_gpt_agent_sdk-0.3.10 → process_gpt_agent_sdk-0.3.12}/process_gpt_agent_sdk.egg-info/dependency_links.txt +0 -0
- {process_gpt_agent_sdk-0.3.10 → process_gpt_agent_sdk-0.3.12}/process_gpt_agent_sdk.egg-info/requires.txt +0 -0
- {process_gpt_agent_sdk-0.3.10 → process_gpt_agent_sdk-0.3.12}/process_gpt_agent_sdk.egg-info/top_level.txt +0 -0
- {process_gpt_agent_sdk-0.3.10 → process_gpt_agent_sdk-0.3.12}/setup.cfg +0 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: process-gpt-agent-sdk
|
|
3
|
+
Version: 0.3.12
|
|
4
|
+
Summary: Supabase 기반 이벤트/작업 폴링으로 A2A AgentExecutor를 실행하는 SDK
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/your-org/process-gpt-agent-sdk
|
|
7
|
+
Project-URL: Issues, https://github.com/your-org/process-gpt-agent-sdk/issues
|
|
8
|
+
Keywords: agent,a2a,supabase,workflow,sdk,processgpt
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: supabase>=2.0.0
|
|
16
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
17
|
+
Requires-Dist: click>=8.0.0
|
|
18
|
+
Requires-Dist: asyncio-mqtt>=0.13.0
|
|
19
|
+
Requires-Dist: jsonschema>=4.0.0
|
|
20
|
+
Requires-Dist: structlog>=23.0.0
|
|
21
|
+
Requires-Dist: typing-extensions>=4.0.0
|
|
22
|
+
Requires-Dist: python-dateutil>=2.8.0
|
|
23
|
+
Requires-Dist: a2a-sdk==0.3.0
|
|
24
|
+
|
|
25
|
+
# ProcessGPT Agent Framework
|
|
26
|
+
## A2A SDK 연동을 위한 경량 에이전트 서버 프레임워크
|
|
27
|
+
|
|
28
|
+
Supabase 기반의 프로세스 작업(Todolist)을 폴링하고, A2A 규격 이벤트를 통해 작업 상태/결과를 기록하는 **경량 에이전트 서버 프레임워크**입니다.
|
|
29
|
+
|
|
30
|
+
### 📋 요구사항
|
|
31
|
+
- **런타임**: Python 3.9+ (권장: Python 3.11)
|
|
32
|
+
- **데이터베이스**: Supabase (PostgreSQL) + 제공된 RPC/테이블
|
|
33
|
+
- **이벤트 규격**: A2A `TaskStatusUpdateEvent` / `TaskArtifactUpdateEvent`
|
|
34
|
+
|
|
35
|
+
## 📊 이벤트 타입별 저장 테이블 및 특징
|
|
36
|
+
|
|
37
|
+
### 1. TaskStatusUpdateEvent (작업 상태 이벤트)
|
|
38
|
+
- **저장 테이블**: `events`
|
|
39
|
+
- **용도**: 작업 진행 상황, 사용자 입력 요청, 에러 알림 등
|
|
40
|
+
- **저장 데이터**: 메시지 래퍼를 제거한 순수 payload만 `data` 컬럼에 JSON으로 저장
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
# 예시 코드
|
|
44
|
+
event_queue.enqueue_event(
|
|
45
|
+
TaskStatusUpdateEvent(
|
|
46
|
+
status={
|
|
47
|
+
"state": TaskState.working, # working, input_required, completed 등
|
|
48
|
+
"message": new_agent_text_message("진행 중입니다", context_id, task_id),
|
|
49
|
+
},
|
|
50
|
+
final=False,
|
|
51
|
+
contextId=context_id,
|
|
52
|
+
taskId=task_id,
|
|
53
|
+
metadata={"event_type": "task_started"} # events.event_type에 저장
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**특별 규칙**:
|
|
59
|
+
- `state=input_required`일 때는 자동으로 `event_type=human_asked`로 저장됨
|
|
60
|
+
- 메시지는 `new_agent_text_message()` 유틸 함수로 생성
|
|
61
|
+
|
|
62
|
+
### 2. TaskArtifactUpdateEvent (작업 결과 이벤트)
|
|
63
|
+
- **저장 테이블**: `todolist` (output 컬럼)
|
|
64
|
+
- **용도**: 최종 작업 결과물 전송
|
|
65
|
+
- **저장 데이터**: 아티팩트 래퍼를 제거한 순수 payload만 `output` 컬럼에 JSON으로 저장
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
# 예시 코드
|
|
69
|
+
artifact = new_text_artifact(
|
|
70
|
+
name="처리결과",
|
|
71
|
+
description="작업 완료 결과",
|
|
72
|
+
text="실제 결과 데이터"
|
|
73
|
+
)
|
|
74
|
+
event_queue.enqueue_event(
|
|
75
|
+
TaskArtifactUpdateEvent(
|
|
76
|
+
artifact=artifact,
|
|
77
|
+
lastChunk=True, # 최종 결과면 True
|
|
78
|
+
contextId=context_id,
|
|
79
|
+
taskId=task_id,
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**특별 규칙**:
|
|
85
|
+
- `lastChunk=True` 또는 `final=True`일 때만 최종 저장됨 (`p_final=true`)
|
|
86
|
+
- 아티팩트는 `new_text_artifact()` 유틸 함수로 생성
|
|
87
|
+
|
|
88
|
+
## 🔄 데이터 흐름과 값 전달 방식
|
|
89
|
+
|
|
90
|
+
### 전체 흐름
|
|
91
|
+
1. **작업 폴링**: 서버가 Supabase `todolist` 테이블에서 새 작업을 가져옴
|
|
92
|
+
2. **컨텍스트 준비**: `RequestContext`에 작업 정보와 사용자 입력을 담음
|
|
93
|
+
3. **익스큐터 실행**: 사용자가 구현한 `AgentExecutor.execute()` 메서드 호출
|
|
94
|
+
4. **이벤트 전송**: 익스큐터에서 진행 상황과 결과를 이벤트로 전송
|
|
95
|
+
5. **데이터 저장**: 이벤트 타입에 따라 적절한 테이블에 저장
|
|
96
|
+
|
|
97
|
+
### 값 전달 과정
|
|
98
|
+
```python
|
|
99
|
+
# 1. 서버에서 작업 정보 가져오기
|
|
100
|
+
row = context.get_context_data()["row"] # todolist 테이블의 한 행
|
|
101
|
+
context_id = row.get("root_proc_inst_id") or row.get("proc_inst_id") # 프로세스 ID
|
|
102
|
+
task_id = row.get("id") # 작업 ID
|
|
103
|
+
user_input = context.get_user_input() # 사용자가 입력한 내용
|
|
104
|
+
|
|
105
|
+
# 2. 메시지/아티팩트 생성시 JSON 문자열로 변환
|
|
106
|
+
payload = {"result": "처리 완료"}
|
|
107
|
+
message_text = json.dumps(payload, ensure_ascii=False) # 중요: JSON 문자열로!
|
|
108
|
+
|
|
109
|
+
# 3. 서버가 자동으로 래퍼 제거 후 순수 payload만 저장
|
|
110
|
+
# events.data 또는 todolist.output에 {"result": "처리 완료"}만 저장됨
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## 🚀 빠른 시작 가이드
|
|
114
|
+
|
|
115
|
+
### 1단계: 설치
|
|
116
|
+
```bash
|
|
117
|
+
# 패키지 설치
|
|
118
|
+
pip install -e .
|
|
119
|
+
|
|
120
|
+
# 또는 requirements.txt 사용
|
|
121
|
+
pip install -r requirements.txt
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 2단계: 환경 설정
|
|
125
|
+
`.env` 파일 생성:
|
|
126
|
+
```env
|
|
127
|
+
SUPABASE_URL=your_supabase_project_url
|
|
128
|
+
SUPABASE_KEY=your_supabase_anon_key
|
|
129
|
+
ENV=dev
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 3단계: 서버 구현 방법
|
|
133
|
+
서버는 이렇게 만드세요:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
# my_server.py
|
|
137
|
+
import asyncio
|
|
138
|
+
from dotenv import load_dotenv
|
|
139
|
+
from processgpt_agent_sdk.processgpt_agent_framework import ProcessGPTAgentServer
|
|
140
|
+
from my_executor import MyExecutor # 아래에서 구현할 익스큐터
|
|
141
|
+
|
|
142
|
+
async def main():
|
|
143
|
+
load_dotenv()
|
|
144
|
+
|
|
145
|
+
server = ProcessGPTAgentServer(
|
|
146
|
+
agent_executor=MyExecutor(), # 여러분이 구현할 익스큐터
|
|
147
|
+
agent_type="my-agent" # Supabase todolist.agent_orch와 매칭되어야 함
|
|
148
|
+
)
|
|
149
|
+
server.polling_interval = 3 # 3초마다 새 작업 확인
|
|
150
|
+
|
|
151
|
+
print("서버 시작!")
|
|
152
|
+
await server.run()
|
|
153
|
+
|
|
154
|
+
if __name__ == "__main__":
|
|
155
|
+
try:
|
|
156
|
+
asyncio.run(main())
|
|
157
|
+
except KeyboardInterrupt:
|
|
158
|
+
print("서버 종료")
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 4단계: 익스큐터 구현 방법
|
|
162
|
+
익스큐터는 이렇게 만드세요:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
# my_executor.py
|
|
166
|
+
import asyncio
|
|
167
|
+
import json
|
|
168
|
+
from typing_extensions import override
|
|
169
|
+
from a2a.server.agent_execution import AgentExecutor, RequestContext
|
|
170
|
+
from a2a.server.events import EventQueue
|
|
171
|
+
from a2a.types import TaskStatusUpdateEvent, TaskState, TaskArtifactUpdateEvent
|
|
172
|
+
from a2a.utils import new_agent_text_message, new_text_artifact
|
|
173
|
+
|
|
174
|
+
class MyExecutor(AgentExecutor):
|
|
175
|
+
@override
|
|
176
|
+
async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
|
|
177
|
+
# 1. 작업 정보 가져오기
|
|
178
|
+
row = context.get_context_data()["row"]
|
|
179
|
+
context_id = row.get("root_proc_inst_id") or row.get("proc_inst_id")
|
|
180
|
+
task_id = row.get("id")
|
|
181
|
+
user_input = context.get_user_input() # 사용자가 입력한 내용
|
|
182
|
+
|
|
183
|
+
print(f"처리할 작업: {user_input}")
|
|
184
|
+
|
|
185
|
+
# 2. 작업 시작 알림 (events 테이블에 저장됨)
|
|
186
|
+
event_queue.enqueue_event(
|
|
187
|
+
TaskStatusUpdateEvent(
|
|
188
|
+
status={
|
|
189
|
+
"state": TaskState.working,
|
|
190
|
+
"message": new_agent_text_message("작업 시작", context_id, task_id),
|
|
191
|
+
},
|
|
192
|
+
final=False,
|
|
193
|
+
contextId=context_id,
|
|
194
|
+
taskId=task_id,
|
|
195
|
+
metadata={"event_type": "task_started"}
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# 3. 실제 작업 수행 (여기에 여러분의 로직 작성)
|
|
200
|
+
await asyncio.sleep(2)
|
|
201
|
+
result_data = {"status": "완료", "input": user_input, "output": "처리 결과"}
|
|
202
|
+
|
|
203
|
+
# 4. 최종 결과 전송 (todolist.output에 저장됨)
|
|
204
|
+
artifact = new_text_artifact(
|
|
205
|
+
name="처리결과",
|
|
206
|
+
description="작업 완료 결과",
|
|
207
|
+
text=json.dumps(result_data, ensure_ascii=False) # JSON 문자열로!
|
|
208
|
+
)
|
|
209
|
+
event_queue.enqueue_event(
|
|
210
|
+
TaskArtifactUpdateEvent(
|
|
211
|
+
artifact=artifact,
|
|
212
|
+
lastChunk=True, # 중요: 최종 결과면 True
|
|
213
|
+
contextId=context_id,
|
|
214
|
+
taskId=task_id,
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
@override
|
|
219
|
+
async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
|
|
220
|
+
pass # 취소 로직 (필요시 구현)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### 5단계: 실행
|
|
224
|
+
```bash
|
|
225
|
+
python my_server.py
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## 🤝 Human-in-the-Loop (사용자 입력 요청) 패턴
|
|
229
|
+
|
|
230
|
+
사용자 입력이 필요할 때:
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
# 사용자 입력 요청
|
|
234
|
+
question_data = {
|
|
235
|
+
"question": "어떤 방식으로 처리할까요?",
|
|
236
|
+
"options": ["방식A", "방식B", "방식C"]
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
event_queue.enqueue_event(
|
|
240
|
+
TaskStatusUpdateEvent(
|
|
241
|
+
status={
|
|
242
|
+
"state": TaskState.input_required, # 이 상태가 중요!
|
|
243
|
+
"message": new_agent_text_message(
|
|
244
|
+
json.dumps(question_data, ensure_ascii=False),
|
|
245
|
+
context_id, task_id
|
|
246
|
+
),
|
|
247
|
+
},
|
|
248
|
+
final=True,
|
|
249
|
+
contextId=context_id,
|
|
250
|
+
taskId=task_id,
|
|
251
|
+
metadata={"job_id": f"job-{task_id}"} # job_id 필수
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
# 자동으로 events 테이블에 event_type=human_asked로 저장됨
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## 📋 체크리스트 (실패 없는 통합을 위한)
|
|
258
|
+
|
|
259
|
+
### 필수 설정
|
|
260
|
+
- [ ] `.env`에 `SUPABASE_URL`, `SUPABASE_KEY` 설정
|
|
261
|
+
- [ ] `requirements.txt` 설치 완료
|
|
262
|
+
- [ ] Supabase에서 제공 SQL(`database_schema.sql`, `function.sql`) 적용
|
|
263
|
+
|
|
264
|
+
### 코드 구현
|
|
265
|
+
- [ ] 서버에서 `agent_type`이 Supabase `todolist.agent_orch`와 매칭됨
|
|
266
|
+
- [ ] 익스큐터에서 `contextId`, `taskId`를 올바르게 설정
|
|
267
|
+
- [ ] 상태 이벤트는 `new_agent_text_message()`로 생성
|
|
268
|
+
- [ ] 최종 결과는 `new_text_artifact()` + `lastChunk=True`로 전송
|
|
269
|
+
- [ ] HITL 요청시 `TaskState.input_required` 사용
|
|
270
|
+
|
|
271
|
+
## 🚨 자주 발생하는 문제
|
|
272
|
+
|
|
273
|
+
### 1. 설치 문제
|
|
274
|
+
**증상**: `ModuleNotFoundError`
|
|
275
|
+
```bash
|
|
276
|
+
# 해결
|
|
277
|
+
pip install -e .
|
|
278
|
+
pip install a2a-sdk==0.3.0 --force-reinstall
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### 2. 작업이 폴링되지 않음
|
|
282
|
+
**원인**: Supabase 연결 문제
|
|
283
|
+
**해결**:
|
|
284
|
+
- `.env` 파일 위치 확인 (프로젝트 루트)
|
|
285
|
+
- URL/Key 재확인
|
|
286
|
+
- `agent_type`이 todolist.agent_orch와 매칭되는지 확인
|
|
287
|
+
|
|
288
|
+
### 3. 이벤트가 저장되지 않음
|
|
289
|
+
**원인**: 테이블/함수 누락
|
|
290
|
+
**해결**:
|
|
291
|
+
- `database_schema.sql`, `function.sql` 실행 확인
|
|
292
|
+
- Supabase 테이블 권한 확인
|
|
293
|
+
|
|
294
|
+
### 4. 결과가 래퍼와 함께 저장됨
|
|
295
|
+
**원인**: JSON 문자열 변환 누락
|
|
296
|
+
```python
|
|
297
|
+
# 올바른 방법
|
|
298
|
+
text=json.dumps(data, ensure_ascii=False) # JSON 문자열로!
|
|
299
|
+
|
|
300
|
+
# 잘못된 방법
|
|
301
|
+
text=data # 딕셔너리 직접 전달 (X)
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## 📚 샘플 코드 (간단 버전)
|
|
305
|
+
|
|
306
|
+
### 기본 서버
|
|
307
|
+
```python
|
|
308
|
+
# sample_server/minimal_server.py
|
|
309
|
+
import asyncio
|
|
310
|
+
from dotenv import load_dotenv
|
|
311
|
+
from processgpt_agent_sdk.processgpt_agent_framework import ProcessGPTAgentServer
|
|
312
|
+
from sample_server.minimal_executor import MinimalExecutor
|
|
313
|
+
|
|
314
|
+
async def main():
|
|
315
|
+
load_dotenv()
|
|
316
|
+
server = ProcessGPTAgentServer(
|
|
317
|
+
agent_executor=MinimalExecutor(),
|
|
318
|
+
agent_type="crewai-action"
|
|
319
|
+
)
|
|
320
|
+
server.polling_interval = 3
|
|
321
|
+
await server.run()
|
|
322
|
+
|
|
323
|
+
if __name__ == "__main__":
|
|
324
|
+
try:
|
|
325
|
+
asyncio.run(main())
|
|
326
|
+
except KeyboardInterrupt:
|
|
327
|
+
pass
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### 기본 익스큐터
|
|
331
|
+
```python
|
|
332
|
+
# sample_server/minimal_executor.py
|
|
333
|
+
import asyncio
|
|
334
|
+
import json
|
|
335
|
+
from typing_extensions import override
|
|
336
|
+
from a2a.server.agent_execution import AgentExecutor, RequestContext
|
|
337
|
+
from a2a.server.events import EventQueue
|
|
338
|
+
from a2a.types import TaskStatusUpdateEvent, TaskState, TaskArtifactUpdateEvent
|
|
339
|
+
from a2a.utils import new_agent_text_message, new_text_artifact
|
|
340
|
+
|
|
341
|
+
class MinimalExecutor(AgentExecutor):
|
|
342
|
+
@override
|
|
343
|
+
async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
|
|
344
|
+
row = context.get_context_data()["row"]
|
|
345
|
+
context_id = row.get("root_proc_inst_id") or row.get("proc_inst_id")
|
|
346
|
+
task_id = row.get("id")
|
|
347
|
+
user_input = context.get_user_input()
|
|
348
|
+
|
|
349
|
+
# 진행 상태
|
|
350
|
+
event_queue.enqueue_event(
|
|
351
|
+
TaskStatusUpdateEvent(
|
|
352
|
+
status={
|
|
353
|
+
"state": TaskState.working,
|
|
354
|
+
"message": new_agent_text_message("처리중", context_id, task_id),
|
|
355
|
+
},
|
|
356
|
+
final=False,
|
|
357
|
+
contextId=context_id,
|
|
358
|
+
taskId=task_id,
|
|
359
|
+
metadata={"event_type": "task_started"}
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
await asyncio.sleep(1)
|
|
364
|
+
|
|
365
|
+
# 최종 결과
|
|
366
|
+
result = {"input": user_input, "output": "처리 완료"}
|
|
367
|
+
artifact = new_text_artifact(
|
|
368
|
+
name="결과",
|
|
369
|
+
description="처리 결과",
|
|
370
|
+
text=json.dumps(result, ensure_ascii=False)
|
|
371
|
+
)
|
|
372
|
+
event_queue.enqueue_event(
|
|
373
|
+
TaskArtifactUpdateEvent(
|
|
374
|
+
artifact=artifact,
|
|
375
|
+
lastChunk=True,
|
|
376
|
+
contextId=context_id,
|
|
377
|
+
taskId=task_id,
|
|
378
|
+
)
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
@override
|
|
382
|
+
async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
|
|
383
|
+
pass
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## 🔧 실행 방법
|
|
387
|
+
|
|
388
|
+
### 개발 환경에서 실행
|
|
389
|
+
```bash
|
|
390
|
+
python sample_server/minimal_server.py
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### 실제 사용시
|
|
394
|
+
```bash
|
|
395
|
+
python my_server.py
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## 📚 레퍼런스
|
|
401
|
+
|
|
402
|
+
### 주요 함수들
|
|
403
|
+
- `ProcessGPTAgentServer.run()`: 서버 시작
|
|
404
|
+
- `new_agent_text_message(text, context_id, task_id)`: 상태 메시지 생성
|
|
405
|
+
- `new_text_artifact(name, desc, text)`: 결과 아티팩트 생성
|
|
406
|
+
|
|
407
|
+
### 이벤트 저장 규칙
|
|
408
|
+
- **TaskStatusUpdateEvent** → `events` 테이블 (`data` 컬럼)
|
|
409
|
+
- **TaskArtifactUpdateEvent** → `todolist` 테이블 (`output` 컬럼)
|
|
410
|
+
- 래퍼 자동 제거 후 순수 payload만 저장
|