flowent 0.1.2 → 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/agent.py +1 -0
- package/backend/src/flowent/cli.py +14 -2
- package/backend/src/flowent/compact.py +183 -0
- package/backend/src/flowent/main.py +405 -88
- package/backend/src/flowent/mcp.py +3 -1
- package/backend/src/flowent/paths.py +12 -0
- package/backend/src/flowent/permissions.py +259 -0
- package/backend/src/flowent/sandbox.py +105 -16
- 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 +218 -1
- package/backend/src/flowent/tools.py +24 -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_permissions.py +443 -0
- package/backend/tests/test_startup_requirements.py +42 -0
- package/backend/tests/test_workspace_chat.py +443 -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-BhHdc2d_.js +0 -81
- package/backend/src/flowent/static/assets/index-C89n9qe2.css +0 -2
- package/dist/frontend/assets/index-BhHdc2d_.js +0 -81
- package/dist/frontend/assets/index-C89n9qe2.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
|
|
@@ -39,6 +39,7 @@ Use tools deliberately:
|
|
|
39
39
|
- Search files when you need to find definitions, references, or related behavior.
|
|
40
40
|
- Apply structured patches for file edits.
|
|
41
41
|
- Run shell commands for diagnostics, builds, tests, and operations that require the local environment.
|
|
42
|
+
- When a shell command needs to write outside the current workspace, declare each needed writable directory with sandbox_permissions set to with_additional_permissions and additional_permissions.file_system.write.
|
|
42
43
|
- Search the web only when current external information is needed.
|
|
43
44
|
- Update the plan when a task has multiple meaningful steps.
|
|
44
45
|
|
|
@@ -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)
|