flowent 0.1.3 → 0.1.4
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/backend/pyproject.toml +1 -1
- package/backend/src/flowent/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/_version.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/agent.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/channels.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/cli.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/compact.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/context.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/llm.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/logging.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/main.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/mcp.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/mcp_import.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/patch.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/paths.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/permissions.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/sandbox.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/skills.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/storage.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/tools.cpython-313.pyc +0 -0
- package/backend/src/flowent/cli.py +14 -2
- package/backend/src/flowent/compact.py +183 -0
- package/backend/src/flowent/main.py +125 -50
- package/backend/src/flowent/mcp.py +3 -1
- package/backend/src/flowent/paths.py +12 -0
- package/backend/src/flowent/sandbox.py +91 -12
- package/backend/src/flowent/static/assets/index-BREidonU.css +2 -0
- package/backend/src/flowent/static/assets/index-DSniOrhL.js +81 -0
- package/backend/src/flowent/static/index.html +2 -2
- package/backend/src/flowent/storage.py +154 -1
- package/backend/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_agent_tools.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_channels.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_health.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_llm_providers.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_logging.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_mcp.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_permissions.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_persistence.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_skills.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_startup_requirements.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_workspace_chat.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/test_agent_tools.py +235 -0
- package/backend/tests/test_mcp.py +76 -10
- package/backend/tests/test_startup_requirements.py +42 -0
- package/backend/tests/test_workspace_chat.py +316 -9
- package/backend/uv.lock +1 -1
- package/dist/frontend/assets/index-BREidonU.css +2 -0
- package/dist/frontend/assets/index-DSniOrhL.js +81 -0
- package/dist/frontend/index.html +2 -2
- package/package.json +2 -2
- package/backend/src/flowent/static/assets/index-DjF2KBwE.js +0 -81
- package/backend/src/flowent/static/assets/index-P-bBpJG8.css +0 -2
- package/dist/frontend/assets/index-DjF2KBwE.js +0 -81
- package/dist/frontend/assets/index-P-bBpJG8.css +0 -2
package/backend/pyproject.toml
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -5,6 +5,8 @@ import os
|
|
|
5
5
|
import sys
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
+
from flowent.paths import WORKDIR_ENV_VAR, resolve_workdir
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
def main(argv: list[str] | None = None) -> None:
|
|
10
12
|
parser = argparse.ArgumentParser(
|
|
@@ -18,8 +20,8 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
18
20
|
parser.add_argument(
|
|
19
21
|
"--host",
|
|
20
22
|
"--hostname",
|
|
21
|
-
default=
|
|
22
|
-
help="Bind host (default:
|
|
23
|
+
default="127.0.0.1",
|
|
24
|
+
help="Bind host (default: 127.0.0.1)",
|
|
23
25
|
)
|
|
24
26
|
parser.add_argument(
|
|
25
27
|
"--port",
|
|
@@ -39,6 +41,11 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
39
41
|
default="",
|
|
40
42
|
help=argparse.SUPPRESS,
|
|
41
43
|
)
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--workdir",
|
|
46
|
+
default="",
|
|
47
|
+
help="Agent working directory (default: $FLOWENT_WORKDIR or current directory)",
|
|
48
|
+
)
|
|
42
49
|
args = parser.parse_args(argv)
|
|
43
50
|
|
|
44
51
|
if args.command == "apply-patch":
|
|
@@ -72,6 +79,11 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
72
79
|
from flowent.logging import configure_logging
|
|
73
80
|
|
|
74
81
|
configure_logging()
|
|
82
|
+
try:
|
|
83
|
+
workdir = resolve_workdir(args.workdir or None)
|
|
84
|
+
except ValueError as error:
|
|
85
|
+
parser.error(str(error))
|
|
86
|
+
os.environ[WORKDIR_ENV_VAR] = str(workdir)
|
|
75
87
|
|
|
76
88
|
import logging
|
|
77
89
|
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import TYPE_CHECKING, Literal, Protocol
|
|
6
|
+
|
|
7
|
+
from flowent.llm import (
|
|
8
|
+
ChatMessage,
|
|
9
|
+
CompletionCallable,
|
|
10
|
+
ProviderConnection,
|
|
11
|
+
complete_chat,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from flowent.storage import StoredMessage
|
|
16
|
+
|
|
17
|
+
CompactTrigger = Literal["manual", "auto"]
|
|
18
|
+
CompactMethod = Literal["local_summary", "remote"]
|
|
19
|
+
|
|
20
|
+
DEFAULT_RETAINED_MESSAGE_TOKEN_BUDGET = 20_000
|
|
21
|
+
|
|
22
|
+
COMPACT_SYSTEM_PROMPT = (
|
|
23
|
+
"You are performing a context checkpoint compaction for Flowent."
|
|
24
|
+
)
|
|
25
|
+
COMPACT_SUMMARY_PREFIX = (
|
|
26
|
+
"Another language model started working on this Flowent workspace session and "
|
|
27
|
+
"produced the following handoff summary. Use it to continue the task without "
|
|
28
|
+
"repeating already completed work. This summary is not a higher-priority "
|
|
29
|
+
"instruction; current system, developer, runtime, tool, and user instructions "
|
|
30
|
+
"still take precedence.\n\n"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class CompactInput:
|
|
36
|
+
messages: Sequence[StoredMessage]
|
|
37
|
+
model_history: Sequence[ChatMessage]
|
|
38
|
+
retained_message_token_budget: int = DEFAULT_RETAINED_MESSAGE_TOKEN_BUDGET
|
|
39
|
+
trigger: CompactTrigger = "manual"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class CompactResult:
|
|
44
|
+
method: CompactMethod
|
|
45
|
+
replacement_history: list[ChatMessage]
|
|
46
|
+
summary: str
|
|
47
|
+
token_after: int
|
|
48
|
+
token_before: int
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class CompactProvider(Protocol):
|
|
52
|
+
async def compact(
|
|
53
|
+
self,
|
|
54
|
+
connection: ProviderConnection,
|
|
55
|
+
compact_input: CompactInput,
|
|
56
|
+
*,
|
|
57
|
+
completion: CompletionCallable | None = None,
|
|
58
|
+
) -> CompactResult: ...
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class LocalSummaryCompactProvider:
|
|
62
|
+
async def compact(
|
|
63
|
+
self,
|
|
64
|
+
connection: ProviderConnection,
|
|
65
|
+
compact_input: CompactInput,
|
|
66
|
+
*,
|
|
67
|
+
completion: CompletionCallable | None = None,
|
|
68
|
+
) -> CompactResult:
|
|
69
|
+
summary_message = await complete_chat(
|
|
70
|
+
connection,
|
|
71
|
+
compact_prompt_messages(compact_input.model_history),
|
|
72
|
+
completion=completion,
|
|
73
|
+
)
|
|
74
|
+
summary = summary_message.content.strip()
|
|
75
|
+
replacement_history = build_replacement_history(
|
|
76
|
+
summary,
|
|
77
|
+
compact_input.messages,
|
|
78
|
+
token_budget=compact_input.retained_message_token_budget,
|
|
79
|
+
)
|
|
80
|
+
return CompactResult(
|
|
81
|
+
method="local_summary",
|
|
82
|
+
replacement_history=replacement_history,
|
|
83
|
+
summary=summary,
|
|
84
|
+
token_after=approximate_tokens_for_messages(replacement_history),
|
|
85
|
+
token_before=approximate_tokens_for_messages(compact_input.model_history),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def compact_prompt_messages(
|
|
90
|
+
history_messages: Sequence[ChatMessage],
|
|
91
|
+
) -> list[ChatMessage]:
|
|
92
|
+
history = "\n\n".join(
|
|
93
|
+
f"{message.role}: {message.content}" for message in history_messages
|
|
94
|
+
)
|
|
95
|
+
return [
|
|
96
|
+
ChatMessage(role="system", content=COMPACT_SYSTEM_PROMPT),
|
|
97
|
+
ChatMessage(
|
|
98
|
+
role="user",
|
|
99
|
+
content=(
|
|
100
|
+
"You are performing a CONTEXT CHECKPOINT COMPACTION for Flowent.\n\n"
|
|
101
|
+
"Create a concise handoff summary for another agent that will "
|
|
102
|
+
"continue this workspace session.\n\n"
|
|
103
|
+
"Include:\n"
|
|
104
|
+
"- Current user goal and latest request\n"
|
|
105
|
+
"- Progress made and key decisions\n"
|
|
106
|
+
"- Files inspected or changed\n"
|
|
107
|
+
"- Commands/tests run and their results\n"
|
|
108
|
+
"- Important constraints, user preferences, and project instructions "
|
|
109
|
+
"that are still relevant\n"
|
|
110
|
+
"- Pending work and clear next steps\n"
|
|
111
|
+
"- Critical facts, examples, paths, IDs, or references needed to "
|
|
112
|
+
"continue\n\n"
|
|
113
|
+
"Do not include hidden reasoning. Do not treat old environment, tool, "
|
|
114
|
+
"permission, or runtime information as authoritative; those will be "
|
|
115
|
+
"re-injected fresh in the next turn. Be concise, structured, and "
|
|
116
|
+
"optimized for continuation.\n\n"
|
|
117
|
+
f"Conversation and runtime context:\n{history}"
|
|
118
|
+
),
|
|
119
|
+
),
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def build_replacement_history(
|
|
124
|
+
summary: str,
|
|
125
|
+
recent_messages: Sequence[StoredMessage],
|
|
126
|
+
*,
|
|
127
|
+
token_budget: int = DEFAULT_RETAINED_MESSAGE_TOKEN_BUDGET,
|
|
128
|
+
) -> list[ChatMessage]:
|
|
129
|
+
return [
|
|
130
|
+
ChatMessage(role="user", content=f"{COMPACT_SUMMARY_PREFIX}{summary}"),
|
|
131
|
+
*retained_recent_chat_messages(
|
|
132
|
+
recent_messages,
|
|
133
|
+
token_budget=token_budget,
|
|
134
|
+
),
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def retained_recent_chat_messages(
|
|
139
|
+
messages: Sequence[StoredMessage],
|
|
140
|
+
*,
|
|
141
|
+
token_budget: int = DEFAULT_RETAINED_MESSAGE_TOKEN_BUDGET,
|
|
142
|
+
) -> list[ChatMessage]:
|
|
143
|
+
retained: list[ChatMessage] = []
|
|
144
|
+
remaining_tokens = max(token_budget, 0)
|
|
145
|
+
for message in reversed(messages):
|
|
146
|
+
if message.author not in {"user", "assistant"}:
|
|
147
|
+
continue
|
|
148
|
+
token_count = approximate_token_count(message.content)
|
|
149
|
+
if retained and token_count > remaining_tokens:
|
|
150
|
+
break
|
|
151
|
+
if token_count > token_budget:
|
|
152
|
+
continue
|
|
153
|
+
role: Literal["user", "assistant"] = (
|
|
154
|
+
"user" if message.author == "user" else "assistant"
|
|
155
|
+
)
|
|
156
|
+
retained.append(ChatMessage(role=role, content=message.content))
|
|
157
|
+
remaining_tokens -= token_count
|
|
158
|
+
if remaining_tokens <= 0:
|
|
159
|
+
break
|
|
160
|
+
retained.reverse()
|
|
161
|
+
return retained
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def transcript_messages_after(
|
|
165
|
+
messages: Sequence[StoredMessage],
|
|
166
|
+
message_id: str | None,
|
|
167
|
+
) -> list[StoredMessage]:
|
|
168
|
+
if message_id is None:
|
|
169
|
+
return list(messages)
|
|
170
|
+
for index, message in enumerate(messages):
|
|
171
|
+
if message.id == message_id:
|
|
172
|
+
return list(messages[index + 1 :])
|
|
173
|
+
return list(messages)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def approximate_tokens_for_messages(messages: Sequence[ChatMessage]) -> int:
|
|
177
|
+
return sum(approximate_token_count(message.content) for message in messages)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def approximate_token_count(content: str) -> int:
|
|
181
|
+
if not content:
|
|
182
|
+
return 0
|
|
183
|
+
return max(1, (len(content) + 3) // 4)
|
|
@@ -17,18 +17,23 @@ from pydantic import BaseModel, ConfigDict
|
|
|
17
17
|
from flowent._version import __version__
|
|
18
18
|
from flowent.agent import run_agent_stream
|
|
19
19
|
from flowent.channels import TelegramBotManager, TelegramTransport
|
|
20
|
+
from flowent.compact import (
|
|
21
|
+
CompactInput,
|
|
22
|
+
LocalSummaryCompactProvider,
|
|
23
|
+
transcript_messages_after,
|
|
24
|
+
)
|
|
20
25
|
from flowent.context import runtime_context_messages
|
|
21
26
|
from flowent.llm import (
|
|
22
27
|
ChatMessage,
|
|
23
28
|
CompletionCallable,
|
|
24
29
|
ProviderConnection,
|
|
25
30
|
ProviderFormat,
|
|
26
|
-
complete_chat,
|
|
27
31
|
list_provider_models,
|
|
28
32
|
)
|
|
29
33
|
from flowent.logging import TRACE_LEVEL, ensure_logging_configured
|
|
30
34
|
from flowent.mcp import McpManager, McpTransport
|
|
31
35
|
from flowent.mcp_import import McpImportDiscovery, discover_imported_mcp_servers
|
|
36
|
+
from flowent.paths import resolve_workdir
|
|
32
37
|
from flowent.permissions import WritablePathDecision, run_tool_with_path_permissions
|
|
33
38
|
from flowent.sandbox import ensure_sandbox_available
|
|
34
39
|
from flowent.skills import (
|
|
@@ -38,8 +43,10 @@ from flowent.skills import (
|
|
|
38
43
|
)
|
|
39
44
|
from flowent.storage import (
|
|
40
45
|
StateStore,
|
|
46
|
+
StoredCompactionCheckpoint,
|
|
41
47
|
StoredMcpServer,
|
|
42
48
|
StoredMessage,
|
|
49
|
+
StoredPermissionRequest,
|
|
43
50
|
StoredProvider,
|
|
44
51
|
StoredSettings,
|
|
45
52
|
StoredSkill,
|
|
@@ -56,7 +63,6 @@ logger = logging.getLogger("flowent.main")
|
|
|
56
63
|
|
|
57
64
|
DEFAULT_STATIC_DIR = Path(__file__).parent / "static"
|
|
58
65
|
COMPACTED_CONTEXT_MARKER = "Context compacted"
|
|
59
|
-
COMPACT_SYSTEM_PROMPT = "You are compacting Flowent workspace context."
|
|
60
66
|
|
|
61
67
|
|
|
62
68
|
class ProviderModelsRequest(BaseModel):
|
|
@@ -149,6 +155,8 @@ class WorkspacePermissionDecisionRequest(BaseModel):
|
|
|
149
155
|
class PendingWorkspacePermission:
|
|
150
156
|
future: asyncio.Future[WritablePathDecision]
|
|
151
157
|
path: Path
|
|
158
|
+
reason: str
|
|
159
|
+
tool_call_id: str | None = None
|
|
152
160
|
|
|
153
161
|
|
|
154
162
|
@dataclass
|
|
@@ -167,6 +175,17 @@ class WorkspaceRun:
|
|
|
167
175
|
def latest_event_index(self) -> int:
|
|
168
176
|
return self.events[-1][0] if self.events else 0
|
|
169
177
|
|
|
178
|
+
def permission_requests(self) -> list[StoredPermissionRequest]:
|
|
179
|
+
return [
|
|
180
|
+
StoredPermissionRequest(
|
|
181
|
+
id=permission_id,
|
|
182
|
+
path=str(permission.path),
|
|
183
|
+
reason=permission.reason,
|
|
184
|
+
tool_call_id=permission.tool_call_id,
|
|
185
|
+
)
|
|
186
|
+
for permission_id, permission in self.pending_permissions.items()
|
|
187
|
+
]
|
|
188
|
+
|
|
170
189
|
|
|
171
190
|
def stream_event(event: str, data: dict[str, object]) -> str:
|
|
172
191
|
return f"event: {event}\ndata: {json.dumps(data)}\n\n"
|
|
@@ -236,8 +255,34 @@ def latest_compacted_context_index(messages: list[StoredMessage]) -> int | None:
|
|
|
236
255
|
def workspace_chat_messages(
|
|
237
256
|
messages: list[StoredMessage],
|
|
238
257
|
compacted_context: str = "",
|
|
258
|
+
checkpoint: StoredCompactionCheckpoint | None = None,
|
|
239
259
|
) -> list[ChatMessage]:
|
|
240
260
|
chat_messages: list[ChatMessage] = []
|
|
261
|
+
|
|
262
|
+
if checkpoint is not None:
|
|
263
|
+
chat_messages.extend(checkpoint.replacement_history)
|
|
264
|
+
visible_messages = transcript_messages_after(
|
|
265
|
+
messages,
|
|
266
|
+
checkpoint.source_message_id,
|
|
267
|
+
)
|
|
268
|
+
for message in visible_messages:
|
|
269
|
+
if (
|
|
270
|
+
message.author == "system"
|
|
271
|
+
and message.content == COMPACTED_CONTEXT_MARKER
|
|
272
|
+
):
|
|
273
|
+
continue
|
|
274
|
+
if message.author not in ("user", "assistant"):
|
|
275
|
+
raise HTTPException(
|
|
276
|
+
status_code=400, detail="Message history is invalid."
|
|
277
|
+
)
|
|
278
|
+
checkpoint_role: Literal["user", "assistant"] = (
|
|
279
|
+
"user" if message.author == "user" else "assistant"
|
|
280
|
+
)
|
|
281
|
+
chat_messages.append(
|
|
282
|
+
ChatMessage(role=checkpoint_role, content=message.content)
|
|
283
|
+
)
|
|
284
|
+
return chat_messages
|
|
285
|
+
|
|
241
286
|
marker_index = latest_compacted_context_index(messages)
|
|
242
287
|
visible_messages = messages
|
|
243
288
|
|
|
@@ -269,43 +314,20 @@ def normalized_request_path(path: str, cwd: Path) -> Path:
|
|
|
269
314
|
return raw_path.resolve(strict=False)
|
|
270
315
|
|
|
271
316
|
|
|
272
|
-
def compact_prompt_messages(
|
|
273
|
-
messages: list[StoredMessage],
|
|
274
|
-
compacted_context: str,
|
|
275
|
-
runtime_messages: list[ChatMessage] | None = None,
|
|
276
|
-
) -> list[ChatMessage]:
|
|
277
|
-
history_messages = [
|
|
278
|
-
*(runtime_messages or []),
|
|
279
|
-
*workspace_chat_messages(messages, compacted_context),
|
|
280
|
-
]
|
|
281
|
-
history = "\n\n".join(
|
|
282
|
-
f"{message.role}: {message.content}" for message in history_messages
|
|
283
|
-
)
|
|
284
|
-
return [
|
|
285
|
-
ChatMessage(role="system", content=COMPACT_SYSTEM_PROMPT),
|
|
286
|
-
ChatMessage(
|
|
287
|
-
role="user",
|
|
288
|
-
content=(
|
|
289
|
-
"Compact the current Flowent workspace context for the next turn.\n\n"
|
|
290
|
-
"Keep the details needed to continue accurately, including decisions, "
|
|
291
|
-
"constraints, pending work, and referenced facts.\n\n"
|
|
292
|
-
f"Conversation:\n{history}"
|
|
293
|
-
),
|
|
294
|
-
),
|
|
295
|
-
]
|
|
296
|
-
|
|
297
|
-
|
|
298
317
|
def create_app(
|
|
299
318
|
*,
|
|
300
319
|
serve_frontend: bool = True,
|
|
301
320
|
chat_completion: CompletionCallable | None = None,
|
|
302
321
|
mcp_transport: McpTransport | None = None,
|
|
303
322
|
telegram_transport: TelegramTransport | None = None,
|
|
323
|
+
workdir: Path | str | None = None,
|
|
304
324
|
) -> FastAPI:
|
|
305
325
|
ensure_logging_configured()
|
|
306
326
|
ensure_sandbox_available()
|
|
307
327
|
|
|
328
|
+
cwd = resolve_workdir(workdir)
|
|
308
329
|
store = StateStore()
|
|
330
|
+
compact_provider = LocalSummaryCompactProvider()
|
|
309
331
|
mcp_manager = McpManager(store=store, transport=mcp_transport)
|
|
310
332
|
telegram_bot_manager: TelegramBotManager | None = None
|
|
311
333
|
workspace_runs: dict[str, WorkspaceRun] = {}
|
|
@@ -313,12 +335,12 @@ def create_app(
|
|
|
313
335
|
|
|
314
336
|
static_dir = frontend_static_directory().resolve(strict=False)
|
|
315
337
|
logger.debug("Flowent app created serve_frontend=%s", serve_frontend)
|
|
338
|
+
logger.info("Workdir: %s", cwd)
|
|
316
339
|
logger.info("Static directory: %s", static_dir)
|
|
317
340
|
|
|
318
341
|
async def run_workspace_turn(content: str) -> StoredMessage:
|
|
319
342
|
state = store.read_state()
|
|
320
343
|
connection = selected_connection(state)
|
|
321
|
-
cwd = Path.cwd()
|
|
322
344
|
user_message = StoredMessage(
|
|
323
345
|
author="user",
|
|
324
346
|
content=content,
|
|
@@ -329,6 +351,7 @@ def create_app(
|
|
|
329
351
|
chat_messages = workspace_chat_messages(
|
|
330
352
|
next_messages,
|
|
331
353
|
store.read_compacted_context(),
|
|
354
|
+
store.read_active_compaction_checkpoint(),
|
|
332
355
|
)
|
|
333
356
|
skill_messages = explicit_skill_messages(cwd, store, content)
|
|
334
357
|
request_messages = [
|
|
@@ -437,7 +460,10 @@ def create_app(
|
|
|
437
460
|
if active_run and not active_run.is_done
|
|
438
461
|
else None,
|
|
439
462
|
"mcp_servers": mcp_manager.servers_with_status(state.mcp_servers),
|
|
440
|
-
"
|
|
463
|
+
"permission_requests": active_run.permission_requests()
|
|
464
|
+
if active_run and not active_run.is_done
|
|
465
|
+
else [],
|
|
466
|
+
"skills": discover_skills(cwd, store),
|
|
441
467
|
}
|
|
442
468
|
if telegram_bot_manager is not None:
|
|
443
469
|
update["telegram_bot"] = telegram_bot_manager.bot_with_status(
|
|
@@ -462,12 +488,12 @@ def create_app(
|
|
|
462
488
|
async def preview_mcp_import(
|
|
463
489
|
request: McpImportPreviewRequest,
|
|
464
490
|
) -> McpImportDiscovery:
|
|
465
|
-
return discover_imported_mcp_servers(
|
|
491
|
+
return discover_imported_mcp_servers(cwd, source=request.source)
|
|
466
492
|
|
|
467
493
|
@app.post("/api/mcp/import")
|
|
468
494
|
async def import_mcp_servers(request: McpImportRequest) -> list[StoredMcpServer]:
|
|
469
495
|
imported_servers = discover_imported_mcp_servers(
|
|
470
|
-
|
|
496
|
+
cwd,
|
|
471
497
|
source=request.source,
|
|
472
498
|
).servers
|
|
473
499
|
existing_servers = {server.id for server in store.read_mcp_servers()}
|
|
@@ -498,7 +524,7 @@ def create_app(
|
|
|
498
524
|
|
|
499
525
|
@app.post("/api/skills/reload")
|
|
500
526
|
async def reload_skills() -> list[StoredSkill]:
|
|
501
|
-
return discover_skills(
|
|
527
|
+
return discover_skills(cwd, store)
|
|
502
528
|
|
|
503
529
|
@app.put("/api/skills/{skill_id:path}")
|
|
504
530
|
async def save_skill_settings(
|
|
@@ -506,7 +532,7 @@ def create_app(
|
|
|
506
532
|
request: SkillSettingsRequest,
|
|
507
533
|
) -> StoredSkill:
|
|
508
534
|
try:
|
|
509
|
-
return update_skill_enabled(
|
|
535
|
+
return update_skill_enabled(cwd, store, skill_id, request.enabled)
|
|
510
536
|
except KeyError as error:
|
|
511
537
|
raise HTTPException(status_code=404, detail="Skill not found.") from error
|
|
512
538
|
|
|
@@ -548,9 +574,7 @@ def create_app(
|
|
|
548
574
|
async def save_writable_path(
|
|
549
575
|
request: WritablePathRequest,
|
|
550
576
|
) -> StoredWritablePath:
|
|
551
|
-
return store.save_writable_path(
|
|
552
|
-
normalized_request_path(request.path, Path.cwd())
|
|
553
|
-
)
|
|
577
|
+
return store.save_writable_path(normalized_request_path(request.path, cwd))
|
|
554
578
|
|
|
555
579
|
@app.delete("/api/permissions/writable-paths")
|
|
556
580
|
async def delete_writable_path(
|
|
@@ -558,7 +582,7 @@ def create_app(
|
|
|
558
582
|
) -> WritablePathListResponse:
|
|
559
583
|
return WritablePathListResponse(
|
|
560
584
|
writable_paths=store.delete_writable_path(
|
|
561
|
-
normalized_request_path(request.path,
|
|
585
|
+
normalized_request_path(request.path, cwd)
|
|
562
586
|
)
|
|
563
587
|
)
|
|
564
588
|
|
|
@@ -610,7 +634,6 @@ def create_app(
|
|
|
610
634
|
nonlocal active_workspace_run_id
|
|
611
635
|
state = store.read_state()
|
|
612
636
|
connection = selected_connection(state)
|
|
613
|
-
cwd = Path.cwd()
|
|
614
637
|
|
|
615
638
|
user_message = StoredMessage(
|
|
616
639
|
author="user",
|
|
@@ -622,6 +645,7 @@ def create_app(
|
|
|
622
645
|
chat_messages = workspace_chat_messages(
|
|
623
646
|
next_messages,
|
|
624
647
|
store.read_compacted_context(),
|
|
648
|
+
store.read_active_compaction_checkpoint(),
|
|
625
649
|
)
|
|
626
650
|
request_messages = [
|
|
627
651
|
message.model_dump()
|
|
@@ -663,6 +687,7 @@ def create_app(
|
|
|
663
687
|
store.upsert_message(assistant_message)
|
|
664
688
|
|
|
665
689
|
try:
|
|
690
|
+
current_tool_id: str | None = None
|
|
666
691
|
|
|
667
692
|
async def request_writable_path(
|
|
668
693
|
path: Path, reason: str
|
|
@@ -672,7 +697,21 @@ def create_app(
|
|
|
672
697
|
run.pending_permissions[permission_id] = PendingWorkspacePermission(
|
|
673
698
|
future=future,
|
|
674
699
|
path=path,
|
|
700
|
+
reason=reason,
|
|
701
|
+
tool_call_id=current_tool_id,
|
|
675
702
|
)
|
|
703
|
+
if current_tool_id and current_tool_id in assistant_tools:
|
|
704
|
+
assistant_tools[current_tool_id] = (
|
|
705
|
+
StoredToolItem.model_validate(
|
|
706
|
+
{
|
|
707
|
+
**assistant_tools[current_tool_id].model_dump(
|
|
708
|
+
exclude_none=True
|
|
709
|
+
),
|
|
710
|
+
"status": "waiting",
|
|
711
|
+
}
|
|
712
|
+
)
|
|
713
|
+
)
|
|
714
|
+
persist_assistant()
|
|
676
715
|
await append_run_event(
|
|
677
716
|
run,
|
|
678
717
|
"permission_request",
|
|
@@ -680,9 +719,13 @@ def create_app(
|
|
|
680
719
|
"id": permission_id,
|
|
681
720
|
"path": str(path),
|
|
682
721
|
"reason": reason,
|
|
722
|
+
"tool_call_id": current_tool_id,
|
|
683
723
|
},
|
|
684
724
|
)
|
|
685
|
-
|
|
725
|
+
try:
|
|
726
|
+
return await future
|
|
727
|
+
finally:
|
|
728
|
+
run.pending_permissions.pop(permission_id, None)
|
|
686
729
|
|
|
687
730
|
async def tool_runner(
|
|
688
731
|
name: str,
|
|
@@ -719,6 +762,7 @@ def create_app(
|
|
|
719
762
|
if event.event == "tool_start":
|
|
720
763
|
tool = event.data.get("tool")
|
|
721
764
|
if isinstance(tool, dict) and isinstance(tool.get("id"), str):
|
|
765
|
+
current_tool_id = tool["id"]
|
|
722
766
|
assistant_tools[tool["id"]] = StoredToolItem.model_validate(
|
|
723
767
|
tool
|
|
724
768
|
)
|
|
@@ -726,6 +770,9 @@ def create_app(
|
|
|
726
770
|
if event.event in {"tool_done", "tool_error"}:
|
|
727
771
|
tool_id = event.data.get("id")
|
|
728
772
|
if isinstance(tool_id, str) and tool_id in assistant_tools:
|
|
773
|
+
current_tool_id = (
|
|
774
|
+
None if current_tool_id == tool_id else current_tool_id
|
|
775
|
+
)
|
|
729
776
|
assistant_tools[tool_id] = StoredToolItem.model_validate(
|
|
730
777
|
{
|
|
731
778
|
**assistant_tools[tool_id].model_dump(
|
|
@@ -843,19 +890,31 @@ def create_app(
|
|
|
843
890
|
|
|
844
891
|
@app.post("/api/workspace/compact")
|
|
845
892
|
async def compact_workspace() -> WorkspaceCompactResponse:
|
|
893
|
+
if active_workspace_run() is not None:
|
|
894
|
+
raise HTTPException(
|
|
895
|
+
status_code=409,
|
|
896
|
+
detail="Compact is unavailable while Flowent is responding.",
|
|
897
|
+
)
|
|
846
898
|
logger.info("Workspace compact requested")
|
|
847
899
|
state = store.read_state()
|
|
848
900
|
connection = selected_connection(state)
|
|
849
|
-
|
|
850
|
-
|
|
901
|
+
checkpoint = store.read_active_compaction_checkpoint()
|
|
902
|
+
model_history = [
|
|
903
|
+
*runtime_context_messages(cwd),
|
|
904
|
+
*workspace_chat_messages(
|
|
905
|
+
state.messages,
|
|
906
|
+
store.read_compacted_context(),
|
|
907
|
+
checkpoint,
|
|
908
|
+
),
|
|
909
|
+
]
|
|
851
910
|
|
|
852
911
|
try:
|
|
853
|
-
|
|
912
|
+
compact_result = await compact_provider.compact(
|
|
854
913
|
connection,
|
|
855
|
-
|
|
856
|
-
state.messages,
|
|
857
|
-
|
|
858
|
-
|
|
914
|
+
CompactInput(
|
|
915
|
+
messages=state.messages,
|
|
916
|
+
model_history=model_history,
|
|
917
|
+
trigger="manual",
|
|
859
918
|
),
|
|
860
919
|
completion=chat_completion,
|
|
861
920
|
)
|
|
@@ -873,12 +932,28 @@ def create_app(
|
|
|
873
932
|
content=COMPACTED_CONTEXT_MARKER,
|
|
874
933
|
id=str(uuid4()),
|
|
875
934
|
)
|
|
876
|
-
|
|
935
|
+
source_message_id = state.messages[-1].id if state.messages else None
|
|
936
|
+
store.save_compaction_checkpoint(
|
|
937
|
+
StoredCompactionCheckpoint(
|
|
938
|
+
id=str(uuid4()),
|
|
939
|
+
method=compact_result.method,
|
|
940
|
+
replacement_history=compact_result.replacement_history,
|
|
941
|
+
source_message_id=source_message_id,
|
|
942
|
+
summary=compact_result.summary,
|
|
943
|
+
token_after=compact_result.token_after,
|
|
944
|
+
token_before=compact_result.token_before,
|
|
945
|
+
trigger="manual",
|
|
946
|
+
)
|
|
947
|
+
)
|
|
877
948
|
store.save_messages([*state.messages, marker])
|
|
878
949
|
logger.info(
|
|
879
|
-
"Workspace compact completed summary_length=%s",
|
|
950
|
+
"Workspace compact completed method=%s summary_length=%s token_before=%s token_after=%s",
|
|
951
|
+
compact_result.method,
|
|
952
|
+
len(compact_result.summary),
|
|
953
|
+
compact_result.token_before,
|
|
954
|
+
compact_result.token_after,
|
|
880
955
|
)
|
|
881
|
-
logger.log(TRACE_LEVEL, "Workspace compact summary=%r", summary
|
|
956
|
+
logger.log(TRACE_LEVEL, "Workspace compact summary=%r", compact_result.summary)
|
|
882
957
|
return WorkspaceCompactResponse(message=marker)
|
|
883
958
|
|
|
884
959
|
@app.post("/api/workspace/respond")
|